Commit 5f603436 authored by Sam Bowen's avatar Sam Bowen Committed by Commit Bot

Get media feed items from a schema org entity and validate against spec.

Not done in this CL, but intended in the future:
* Genre should be able to store multiple values.
* TV episode images.
* Play next episode.
* Storage of the media feed items in media history.
* End to end testing using a full JSON example.

Change-Id: Ib4c6d9421c61ed98426aabc5b6056f0437195782
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2112957Reviewed-by: default avatarBecca Hughes <beccahughes@chromium.org>
Commit-Queue: Sam Bowen <sgbowen@google.com>
Cr-Commit-Position: refs/heads/master@{#756355}
parent 547cf946
......@@ -682,6 +682,8 @@ jumbo_static_library("browser") {
"media/cast_remoting_connector.h",
"media/feeds/media_feeds_contents_observer.cc",
"media/feeds/media_feeds_contents_observer.h",
"media/feeds/media_feeds_converter.cc",
"media/feeds/media_feeds_converter.h",
"media/feeds/media_feeds_fetcher.cc",
"media/feeds/media_feeds_fetcher.h",
"media/feeds/media_feeds_service.cc",
......
// Copyright 2020 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/browser/media/feeds/media_feeds_converter.h"
#include <numeric>
#include <vector>
#include "base/containers/flat_set.h"
#include "base/no_destructor.h"
#include "base/optional.h"
#include "base/strings/string16.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/utf_string_conversions.h"
#include "chrome/browser/media/feeds/media_feeds_store.mojom-forward.h"
#include "chrome/browser/media/feeds/media_feeds_store.mojom-shared.h"
#include "chrome/browser/media/feeds/media_feeds_store.mojom.h"
#include "components/autofill/core/browser/validation.h"
#include "components/schema_org/common/improved_metadata.mojom-forward.h"
#include "components/schema_org/common/improved_metadata.mojom.h"
#include "components/schema_org/schema_org_entity_names.h"
#include "components/schema_org/schema_org_enums.h"
#include "components/schema_org/schema_org_property_names.h"
namespace media_feeds {
using schema_org::improved::mojom::Entity;
using schema_org::improved::mojom::EntityPtr;
using schema_org::improved::mojom::Property;
using schema_org::improved::mojom::PropertyPtr;
using schema_org::improved::mojom::ValuesPtr;
static int constexpr kMaxRatings = 5;
static int constexpr kMaxGenres = 3;
static int constexpr kMaxInteractionStatistics = 3;
static int constexpr kMaxImages = 5;
// Gets the property of entity with corresponding name. May be null if not found
// or if the property has no values.
Property* GetProperty(Entity* entity, const std::string& name) {
auto property = std::find_if(
entity->properties.begin(), entity->properties.end(),
[&name](const PropertyPtr& property) { return property->name == name; });
if (property == entity->properties.end())
return nullptr;
if (!(*property)->values)
return nullptr;
return property->get();
}
// Converts a property named property_name and store the result in the
// converted_item struct using the convert_property callback. Returns true only
// is the conversion was successful. If is_required is set, the property must be
// found and be valid. If is_required is not set, returns false only if the
// property is found and is invalid.
template <typename T>
bool ConvertProperty(
Entity* entity,
T* converted_item,
const std::string& property_name,
bool is_required,
base::OnceCallback<bool(const Property& property, T*)> convert_property) {
auto* property = GetProperty(entity, property_name);
if (!property)
return !is_required;
return std::move(convert_property).Run(*property, converted_item);
}
// Validates a property identified by name using the provided callback. Returns
// true only if the property is valid.
bool ValidateProperty(
Entity* entity,
const std::string& name,
base::OnceCallback<bool(const Property& property)> property_is_valid) {
auto property = std::find_if(
entity->properties.begin(), entity->properties.end(),
[&name](const PropertyPtr& property) { return property->name == name; });
if (property == entity->properties.end())
return false;
if (!(*property)->values)
return false;
return std::move(property_is_valid).Run(**property);
}
// Checks that the property contains at least one URL and that all URLs it
// contains are valid.
bool IsUrl(const Property& property) {
return !property.values->url_values.empty() &&
std::accumulate(property.values->url_values.begin(),
property.values->url_values.end(), true,
[](auto& accumulation, auto& url_value) {
return accumulation && url_value.is_valid();
});
}
// Checks that the property contains at least positive integer and that all
// numbers it contains are positive integers.
bool IsPositiveInteger(const Property& property) {
return !property.values->long_values.empty() &&
std::accumulate(property.values->long_values.begin(),
property.values->long_values.end(), true,
[](auto& accumulation, auto& long_value) {
return accumulation && long_value > 0;
});
}
// Checks that the property contains at least one non-empty string and that all
// strings it contains are non-empty.
bool IsNonEmptyString(const Property& property) {
return (!property.values->string_values.empty() &&
std::accumulate(property.values->string_values.begin(),
property.values->string_values.end(), true,
[](auto& accumulation, auto& string_value) {
return accumulation && !string_value.empty();
}));
}
// Checks that the property contains at least one valid email address.
bool IsEmail(const Property& email) {
if (email.values->string_values.empty())
return false;
return autofill::IsValidEmailAddress(
base::ASCIIToUTF16(email.values->string_values[0]));
}
// Checks whether the media item type is supported.
bool IsMediaItemType(const std::string& type) {
static const base::NoDestructor<base::flat_set<base::StringPiece>>
kSupportedTypes(base::flat_set<base::StringPiece>(
{schema_org::entity::kVideoObject, schema_org::entity::kMovie,
schema_org::entity::kTVSeries}));
return kSupportedTypes->find(type) != kSupportedTypes->end();
}
// Checks that the property contains at least one valid date / date-time.
bool IsDateOrDateTime(const Property& property) {
return !property.values->date_time_values.empty();
}
// Gets a number from the property which may be stored either as a long or a
// string.
base::Optional<uint64_t> GetNumber(const Property& property) {
if (!property.values->long_values.empty())
return property.values->long_values[0];
if (!property.values->string_values.empty()) {
uint64_t number;
bool parsed_number =
base::StringToUint64(property.values->string_values[0], &number);
if (parsed_number)
return number;
}
return base::nullopt;
}
// Gets a list of media images from the property. The property should have at
// least one media image and no more than kMaxImages. A media image is either a
// valid URL string or an ImageObject entity containing a width, height, and
// URL.
base::Optional<std::vector<media_session::MediaImage>> GetMediaImage(
const Property& property) {
if (property.values->url_values.empty() &&
property.values->entity_values.empty()) {
return base::nullopt;
}
std::vector<media_session::MediaImage> images;
for (const auto& url : property.values->url_values) {
if (!url.is_valid())
continue;
media_session::MediaImage image;
image.src = url;
images.push_back(std::move(image));
if (images.size() == kMaxImages)
return images;
}
for (const auto& image_object : property.values->entity_values) {
if (image_object->type != schema_org::entity::kImageObject)
continue;
auto* width = GetProperty(image_object.get(), schema_org::property::kWidth);
if (!width || !IsPositiveInteger(*width))
continue;
auto* height =
GetProperty(image_object.get(), schema_org::property::kWidth);
if (!height || !IsPositiveInteger(*height))
continue;
auto* url = GetProperty(image_object.get(), schema_org::property::kUrl);
if (!url)
url = GetProperty(image_object.get(), schema_org::property::kEmbedUrl);
if (!IsUrl(*url))
continue;
media_session::MediaImage image;
image.src = url->values->url_values[0];
image.sizes.push_back(gfx::Size(width->values->long_values[0],
height->values->long_values[0]));
images.push_back(std::move(image));
if (images.size() == kMaxImages)
return images;
}
return images;
}
// Validates the provider property of an entity. Outputs the name and images
// properties.
bool ValidateProvider(const Property& provider,
std::string* display_name,
std::vector<media_session::MediaImage>* images) {
if (provider.values->entity_values.empty())
return false;
auto organization = std::find_if(
provider.values->entity_values.begin(),
provider.values->entity_values.end(), [](const EntityPtr& value) {
return value->type == schema_org::entity::kOrganization;
});
if (organization == provider.values->entity_values.end())
return false;
auto* name = GetProperty(organization->get(), schema_org::property::kName);
if (!name || !IsNonEmptyString(*name))
return false;
*display_name = name->values->string_values[0];
auto* logo = GetProperty(organization->get(), schema_org::property::kLogo);
if (!logo)
return false;
auto maybe_images = GetMediaImage(*logo);
if (!maybe_images.has_value() || maybe_images.value().empty())
return false;
*images = maybe_images.value();
return true;
}
// Gets the author property and stores the result in item. Returns true if the
// author was valid.
bool GetMediaItemAuthor(const Property& author, mojom::MediaFeedItem* item) {
item->author = mojom::Author::New();
if (IsNonEmptyString(author)) {
item->author->name = author.values->string_values[0];
return true;
}
if (author.values->entity_values.empty())
return false;
auto person = std::find_if(
author.values->entity_values.begin(), author.values->entity_values.end(),
[](const EntityPtr& value) {
return value->type == schema_org::entity::kPerson;
});
auto* name = GetProperty(person->get(), schema_org::property::kName);
if (!name || !IsNonEmptyString(*name))
return false;
item->author->name = name->values->string_values[0];
auto* url = GetProperty(person->get(), schema_org::property::kUrl);
if (url) {
if (!IsUrl(*url))
return false;
item->author->url = url->values->url_values[0];
}
return true;
}
// Gets the ratings property and stores the result in item. Returns true if the
// ratings were valid.
bool GetContentRatings(const Property& property, mojom::MediaFeedItem* item) {
if (property.values->entity_values.empty() ||
property.values->entity_values.size() > kMaxRatings)
return false;
for (const auto& rating : property.values->entity_values) {
mojom::ContentRatingPtr converted_rating = mojom::ContentRating::New();
if (rating->type != schema_org::entity::kRating)
return false;
auto* author = GetProperty(rating.get(), schema_org::property::kAuthor);
if (!author || !IsNonEmptyString(*author))
return false;
static const base::NoDestructor<base::flat_set<base::StringPiece>>
kRatingAgencies(base::flat_set<base::StringPiece>(
{"TVPG", "MPAA", "BBFC", "CSA", "AGCOM", "FSK", "SETSI", "ICAA",
"NA", "EIRIN", "KMRB", "CLASSIND", "MKRF", "CBFC", "KPI", "LSF",
"RTC"}));
if (!kRatingAgencies->contains(author->values->string_values[0]))
return false;
converted_rating->agency = author->values->string_values[0];
auto* rating_value =
GetProperty(rating.get(), schema_org::property::kRatingValue);
if (!rating_value || !IsNonEmptyString(*rating_value))
return false;
converted_rating->value = rating_value->values->string_values[0];
item->content_ratings.push_back(std::move(converted_rating));
}
return true;
}
// Gets the identifiers property and stores the result in item. Item should be a
// struct with an identifiers field. Returns true if the identifiers were valid.
template <typename T>
bool GetIdentifiers(const Property& property, T* item) {
if (property.values->entity_values.empty())
return false;
std::vector<mojom::IdentifierPtr> identifiers;
for (const auto& identifier : property.values->entity_values) {
mojom::IdentifierPtr converted_identifier = mojom::Identifier::New();
if (identifier->type != schema_org::entity::kPropertyValue)
return false;
auto* property_id =
GetProperty(identifier.get(), schema_org::property::kPropertyID);
if (!property_id || !IsNonEmptyString(*property_id))
return false;
std::string property_id_str = property_id->values->string_values[0];
if (property_id_str == "TMS_ROOT_ID") {
converted_identifier->type = mojom::Identifier::Type::kTMSRootId;
} else if (property_id_str == "TMS_ID") {
converted_identifier->type = mojom::Identifier::Type::kTMSId;
} else if (property_id_str == "_PARTNER_ID_") {
converted_identifier->type = mojom::Identifier::Type::kPartnerId;
} else {
return false;
}
auto* value = GetProperty(identifier.get(), schema_org::property::kValue);
if (!value || !IsNonEmptyString(*value))
return false;
converted_identifier->value = value->values->string_values[0];
item->identifiers.push_back(std::move(converted_identifier));
}
return true;
}
// Gets the interaction type from a property containing an interaction type
// string.
base::Optional<mojom::InteractionCounterType> GetInteractionType(
const Property& property) {
if (property.values->string_values.empty())
return base::nullopt;
GURL type = GURL(property.values->string_values[0]);
if (!type.SchemeIsHTTPOrHTTPS() || type.host() != "schema.org")
return base::nullopt;
std::string type_path = type.path().substr(1);
if (type_path == schema_org::entity::kWatchAction) {
return mojom::InteractionCounterType::kWatch;
} else if (type_path == schema_org::entity::kLikeAction) {
return mojom::InteractionCounterType::kLike;
} else if (type_path == schema_org::entity::kDislikeAction) {
return mojom::InteractionCounterType::kDislike;
}
return base::nullopt;
}
// Gets the interaction statistics property and stores the result in item.
// Returns true if the statistics were valid.
bool GetInteractionStatistics(const Property& property,
mojom::MediaFeedItem* item) {
if (property.values->entity_values.empty() ||
property.values->entity_values.size() > kMaxInteractionStatistics) {
return false;
}
for (const auto& stat : property.values->entity_values) {
if (stat->type != schema_org::entity::kInteractionCounter)
return false;
auto* interaction_type =
GetProperty(stat.get(), schema_org::property::kInteractionType);
if (!interaction_type)
return false;
auto type = GetInteractionType(*interaction_type);
if (!type.has_value() || item->interaction_counters.count(type.value()) > 0)
return false;
auto* user_interaction_count =
GetProperty(stat.get(), schema_org::property::kUserInteractionCount);
if (!user_interaction_count)
return false;
base::Optional<uint64_t> count = GetNumber(*user_interaction_count);
if (!count.has_value())
return false;
item->interaction_counters.insert(
std::pair<mojom::InteractionCounterType, uint64_t>(type.value(),
count.value()));
}
if (item->interaction_counters.empty())
return false;
return true;
}
base::Optional<mojom::MediaFeedItemType> GetMediaItemType(
const std::string& schema_org_type) {
if (schema_org_type == schema_org::entity::kVideoObject) {
return mojom::MediaFeedItemType::kVideo;
} else if (schema_org_type == schema_org::entity::kMovie) {
return mojom::MediaFeedItemType::kMovie;
} else if (schema_org_type == schema_org::entity::kTVSeries) {
return mojom::MediaFeedItemType::kTVSeries;
}
return base::nullopt;
}
// Gets the isFamilyFriendly property and stores the result in item.
bool GetIsFamilyFriendly(const Property& property, mojom::MediaFeedItem* item) {
if (property.values->bool_values.empty()) {
return false;
}
item->is_family_friendly = property.values->bool_values[0];
return true;
}
// Gets the watchAction and actionStatus properties from an embedded entity and
// stores the result in item. Returns true if both the action and the action
// status were valid.
bool GetActionAndStatus(const Property& property, mojom::MediaFeedItem* item) {
if (property.values->entity_values.empty())
return false;
EntityPtr& action = property.values->entity_values[0];
if (action->type != schema_org::entity::kWatchAction)
return false;
item->action = mojom::Action::New();
auto* target = GetProperty(action.get(), schema_org::property::kTarget);
if (!target || !IsUrl(*target))
return false;
item->action->url = target->values->url_values[0];
auto* action_status =
GetProperty(action.get(), schema_org::property::kActionStatus);
if (action_status) {
if (!IsUrl(*action_status))
return false;
auto status = schema_org::enums::CheckValidEnumString(
"http://schema.org/ActionStatusType",
action_status->values->url_values[0]);
if (status == base::nullopt) {
return false;
} else if (status.value() ==
static_cast<int>(
schema_org::enums::ActionStatusType::kActiveActionStatus)) {
item->action_status = mojom::MediaFeedItemActionStatus::kActive;
auto* start_time =
GetProperty(action.get(), schema_org::property::kStartTime);
if (!start_time || start_time->values->time_values.empty())
return false;
item->action->start_time = start_time->values->time_values[0];
} else if (status.value() ==
static_cast<int>(schema_org::enums::ActionStatusType::
kPotentialActionStatus)) {
item->action_status = mojom::MediaFeedItemActionStatus::kPotential;
} else if (status.value() ==
static_cast<int>(schema_org::enums::ActionStatusType::
kCompletedActionStatus)) {
item->action_status = mojom::MediaFeedItemActionStatus::kCompleted;
}
}
return true;
}
// Gets the TV episode stored in an embedded entity and stores the result in
// item. Returns true if the TV episode was valid.
bool GetEpisode(const Property& property, mojom::MediaFeedItem* item) {
if (property.values->entity_values.empty())
return false;
EntityPtr& episode = property.values->entity_values[0];
if (episode->type != schema_org::entity::kTVEpisode)
return false;
if (!item->tv_episode)
item->tv_episode = mojom::TVEpisode::New();
auto* episode_number =
GetProperty(episode.get(), schema_org::property::kEpisodeNumber);
if (!episode_number || !IsPositiveInteger(*episode_number))
return false;
item->tv_episode->episode_number = episode_number->values->long_values[0];
auto* name = GetProperty(episode.get(), schema_org::property::kName);
if (!name || !IsNonEmptyString(*name))
return false;
item->tv_episode->name = name->values->string_values[0];
if (!ConvertProperty<mojom::TVEpisode>(
episode.get(), item->tv_episode.get(),
schema_org::property::kIdentifier, false,
base::BindOnce(&GetIdentifiers<mojom::TVEpisode>))) {
return false;
}
auto* image = GetProperty(episode.get(), schema_org::property::kImage);
if (image) {
auto converted_images = GetMediaImage(*image);
if (!converted_images.has_value())
return false;
// TODO(sgbowen): Add an images field to TV episodes and store the converted
// images here.
}
if (!ConvertProperty<mojom::MediaFeedItem>(
episode.get(), item, schema_org::property::kPotentialAction, true,
base::BindOnce(&GetActionAndStatus))) {
return false;
}
return true;
}
// Gets the TV season stored in an embedded entity and stores the result in
// item. Returns true if the TV season was valid.
bool GetSeason(const Property& property, mojom::MediaFeedItem* item) {
if (property.values->entity_values.empty())
return false;
EntityPtr& season = property.values->entity_values[0];
if (season->type != schema_org::entity::kTVSeason)
return false;
if (!item->tv_episode)
item->tv_episode = mojom::TVEpisode::New();
auto* season_number =
GetProperty(season.get(), schema_org::property::kSeasonNumber);
if (!season_number || !IsPositiveInteger(*season_number))
return false;
item->tv_episode->season_number = season_number->values->long_values[0];
auto* number_episodes =
GetProperty(season.get(), schema_org::property::kNumberOfEpisodes);
if (!number_episodes || !IsPositiveInteger(*number_episodes))
return false;
if (!ConvertProperty<mojom::MediaFeedItem>(
season.get(), item, schema_org::property::kEpisode, false,
base::BindOnce(&GetIdentifiers<mojom::MediaFeedItem>))) {
return false;
}
return true;
}
// Gets the broadcastEvent entity from the property and store the result in item
// as LiveDetails. Returns true if the broadcastEven was valid.
bool GetLiveDetails(const Property& property, mojom::MediaFeedItem* item) {
if (property.values->entity_values.empty())
return false;
EntityPtr& publication = property.values->entity_values[0];
if (publication->type != schema_org::entity::kBroadcastEvent)
return false;
item->live = mojom::LiveDetails::New();
auto* start_date =
GetProperty(publication.get(), schema_org::property::kStartDate);
if (!start_date || !IsDateOrDateTime(*start_date))
return false;
item->live->start_time = start_date->values->date_time_values[0];
auto* end_date =
GetProperty(publication.get(), schema_org::property::kEndDate);
if (end_date) {
if (!IsDateOrDateTime(*end_date))
return false;
item->live->end_time = end_date->values->date_time_values[0];
}
return true;
}
// Gets the duration from the property and store the result in item. Returns
// true if the duration was valid.
bool GetDuration(const Property& property, mojom::MediaFeedItem* item) {
if (property.values->time_values.empty())
return false;
item->duration = property.values->time_values[0];
return true;
}
// Given the schema.org data_feed_items, iterate through and convert all feed
// items into MediaFeedItemPtr types. Store the converted items in
// converted_feed_items. Skips invalid feed items.
void GetDataFeedItems(
const PropertyPtr& data_feed_items,
std::vector<mojom::MediaFeedItemPtr>* converted_feed_items) {
if (data_feed_items->values->entity_values.empty())
return;
base::flat_set<std::string> item_ids;
for (const auto& item : data_feed_items->values->entity_values) {
mojom::MediaFeedItemPtr converted_item = mojom::MediaFeedItem::New();
auto convert_property =
base::BindRepeating(&ConvertProperty<mojom::MediaFeedItem>, item.get(),
converted_item.get());
auto type = GetMediaItemType(item->type);
if (!type.has_value())
continue;
converted_item->type = type.value();
// Check that the id is present and unique. This does not get converted.
if (item->id == "" || item_ids.find(item->id) != item_ids.end()) {
continue;
}
item_ids.insert(item->id);
auto* name = GetProperty(item.get(), schema_org::property::kName);
if (name && IsNonEmptyString(*name)) {
converted_item->name = base::ASCIIToUTF16(name->values->string_values[0]);
} else {
continue;
}
auto* date_published =
GetProperty(item.get(), schema_org::property::kDatePublished);
if (date_published && !date_published->values->date_time_values.empty()) {
converted_item->date_published =
date_published->values->date_time_values[0];
} else {
continue;
}
if (!convert_property.Run(schema_org::property::kIsFamilyFriendly, true,
base::BindOnce(&GetIsFamilyFriendly))) {
continue;
}
auto* image = GetProperty(item.get(), schema_org::property::kImage);
if (!image)
continue;
auto converted_images = GetMediaImage(*image);
if (!converted_images.has_value())
continue;
converted_item->images = converted_images.value();
bool has_embedded_action =
item->type == schema_org::entity::kTVSeries &&
GetProperty(item.get(), schema_org::property::kEpisode);
if (!convert_property.Run(schema_org::property::kPotentialAction,
!has_embedded_action,
base::BindOnce(&GetActionAndStatus))) {
continue;
}
if (!convert_property.Run(schema_org::property::kInteractionStatistic,
false,
base::BindOnce(&GetInteractionStatistics))) {
continue;
}
if (!convert_property.Run(schema_org::property::kContentRating, false,
base::BindOnce(&GetContentRatings))) {
continue;
}
auto* genre = GetProperty(item.get(), schema_org::property::kGenre);
if (genre) {
if (!IsNonEmptyString(*genre))
continue;
for (const auto& genre_value : genre->values->string_values) {
converted_item->genre.push_back(genre_value);
if (converted_item->genre.size() >= kMaxGenres)
continue;
}
}
if (!convert_property.Run(schema_org::property::kPublication, false,
base::BindOnce(&GetLiveDetails))) {
continue;
}
if (!convert_property.Run(
schema_org::property::kIdentifier, false,
base::BindOnce(&GetIdentifiers<mojom::MediaFeedItem>))) {
continue;
}
if (converted_item->type == mojom::MediaFeedItemType::kVideo) {
if (!convert_property.Run(schema_org::property::kAuthor, true,
base::BindOnce(&GetMediaItemAuthor))) {
continue;
}
if (!convert_property.Run(schema_org::property::kDuration,
!converted_item->live,
base::BindOnce(&GetDuration))) {
continue;
}
}
if (converted_item->type == mojom::MediaFeedItemType::kTVSeries) {
auto* num_episodes =
GetProperty(item.get(), schema_org::property::kNumberOfEpisodes);
if (!num_episodes || !IsPositiveInteger(*num_episodes))
continue;
auto* num_seasons =
GetProperty(item.get(), schema_org::property::kNumberOfSeasons);
if (!num_seasons || !IsPositiveInteger(*num_seasons))
continue;
if (!convert_property.Run(schema_org::property::kEpisode, false,
base::BindOnce(&GetEpisode))) {
continue;
}
if (!convert_property.Run(schema_org::property::kContainsSeason, false,
base::BindOnce(&GetSeason))) {
continue;
}
}
converted_feed_items->push_back(std::move(converted_item));
}
}
// static
base::Optional<std::vector<mojom::MediaFeedItemPtr>> GetMediaFeeds(
EntityPtr entity) {
if (entity->type != "CompleteDataFeed")
return base::nullopt;
auto* provider = GetProperty(entity.get(), schema_org::property::kProvider);
std::string display_name;
std::vector<media_session::MediaImage> logos;
if (!ValidateProvider(*provider, &display_name, &logos))
return base::nullopt;
std::vector<mojom::MediaFeedItemPtr> media_feed_items;
auto data_feed_items = std::find_if(
entity->properties.begin(), entity->properties.end(),
[](const PropertyPtr& property) {
return property->name == schema_org::property::kDataFeedElement;
});
if (data_feed_items != entity->properties.end() &&
(*data_feed_items)->values) {
GetDataFeedItems(*data_feed_items, &media_feed_items);
}
return media_feed_items;
}
} // namespace media_feeds
// Copyright 2020 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifndef CHROME_BROWSER_MEDIA_FEEDS_MEDIA_FEEDS_CONVERTER_H_
#define CHROME_BROWSER_MEDIA_FEEDS_MEDIA_FEEDS_CONVERTER_H_
#include "chrome/browser/media/feeds/media_feeds_store.mojom.h"
#include "components/schema_org/common/improved_metadata.mojom.h"
namespace media_feeds {
// Given a schema_org entity of type CompleteDataFeed, converts all items
// contained in the feed to MediaFeedItemPtr type and returns them in a vector.
// The feed should be valid according to https://wicg.github.io/media-feeds/. If
// not, GetMediaFeeds returns an empty result. If the feed is valid, but some of
// its feed items are not, GetMediaFeeds excludes the invalid feed items from
// the returned result.
base::Optional<std::vector<mojom::MediaFeedItemPtr>> GetMediaFeeds(
schema_org::improved::mojom::EntityPtr schema_org_entity);
} // namespace media_feeds
#endif // CHROME_BROWSER_MEDIA_FEEDS_MEDIA_FEEDS_CONVERTER_H_
// Copyright 2020 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/browser/media/feeds/media_feeds_converter.h"
#include "base/strings/utf_string_conversions.h"
#include "chrome/browser/media/feeds/media_feeds_store.mojom-forward.h"
#include "chrome/browser/media/feeds/media_feeds_store.mojom-shared.h"
#include "chrome/browser/media/feeds/media_feeds_store.mojom.h"
#include "components/schema_org/common/improved_metadata.mojom.h"
#include "components/schema_org/extractor.h"
#include "components/schema_org/schema_org_entity_names.h"
#include "components/schema_org/schema_org_property_names.h"
#include "services/network/test/test_utils.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "url/gurl.h"
namespace media_feeds {
using mojom::MediaFeedItem;
using mojom::MediaFeedItemPtr;
using schema_org::improved::mojom::Entity;
using schema_org::improved::mojom::EntityPtr;
using schema_org::improved::mojom::Property;
using schema_org::improved::mojom::PropertyPtr;
using schema_org::improved::mojom::Values;
using schema_org::improved::mojom::ValuesPtr;
class MediaFeedsConverterTest : public testing::Test {
public:
MediaFeedsConverterTest()
: extractor_({schema_org::entity::kCompleteDataFeed,
schema_org::entity::kMovie,
schema_org::entity::kWatchAction}) {}
protected:
Property* GetProperty(Entity* entity, const std::string& name);
PropertyPtr CreateStringProperty(const std::string& name,
const std::string& value);
PropertyPtr CreateLongProperty(const std::string& name, long value);
PropertyPtr CreateUrlProperty(const std::string& name, const GURL& value);
PropertyPtr CreateTimeProperty(const std::string& name, int hours);
PropertyPtr CreateDateTimeProperty(const std::string& name,
const std::string& value);
PropertyPtr CreateEntityProperty(const std::string& name, EntityPtr value);
EntityPtr ConvertJSONToEntityPtr(const std::string& json);
EntityPtr ValidWatchAction();
EntityPtr ValidMediaFeed();
EntityPtr ValidMediaFeedItem();
mojom::MediaFeedItemPtr ExpectedFeedItem();
EntityPtr AddItemToFeed(EntityPtr feed, EntityPtr item);
private:
schema_org::Extractor extractor_;
};
Property* MediaFeedsConverterTest::GetProperty(Entity* entity,
const std::string& name) {
auto property = std::find_if(
entity->properties.begin(), entity->properties.end(),
[&name](const PropertyPtr& property) { return property->name == name; });
DCHECK(property != entity->properties.end());
DCHECK((*property)->values);
return property->get();
}
PropertyPtr MediaFeedsConverterTest::CreateStringProperty(
const std::string& name,
const std::string& value) {
PropertyPtr property = Property::New();
property->name = name;
property->values = Values::New();
property->values->string_values.push_back(value);
return property;
}
PropertyPtr MediaFeedsConverterTest::CreateLongProperty(const std::string& name,
long value) {
PropertyPtr property = Property::New();
property->name = name;
property->values = Values::New();
property->values->long_values.push_back(value);
return property;
}
PropertyPtr MediaFeedsConverterTest::CreateUrlProperty(const std::string& name,
const GURL& value) {
PropertyPtr property = Property::New();
property->name = name;
property->values = Values::New();
property->values->url_values.push_back(value);
return property;
}
PropertyPtr MediaFeedsConverterTest::CreateTimeProperty(const std::string& name,
int hours) {
PropertyPtr property = Property::New();
property->name = name;
property->values = Values::New();
property->values->time_values.push_back(base::TimeDelta::FromHours(hours));
return property;
}
PropertyPtr MediaFeedsConverterTest::CreateDateTimeProperty(
const std::string& name,
const std::string& value) {
PropertyPtr property = Property::New();
property->name = name;
property->values = Values::New();
base::Time time;
bool got_time = base::Time::FromString(value.c_str(), &time);
DCHECK(got_time);
property->values->date_time_values.push_back(time);
return property;
}
PropertyPtr MediaFeedsConverterTest::CreateEntityProperty(
const std::string& name,
EntityPtr value) {
PropertyPtr property = Property::New();
property->name = name;
property->values = Values::New();
property->values->entity_values.push_back(std::move(value));
return property;
}
EntityPtr MediaFeedsConverterTest::ConvertJSONToEntityPtr(
const std::string& json) {
return extractor_.Extract(json);
}
EntityPtr MediaFeedsConverterTest::ValidWatchAction() {
return extractor_.Extract(
R"END(
{
"@type": "WatchAction",
"target": "https://www.example.org",
"actionStatus": "https://schema.org/ActiveActionStatus",
"startTime": "01:00:00"
}
)END");
}
EntityPtr MediaFeedsConverterTest::ValidMediaFeed() {
return extractor_.Extract(
R"END(
{
"@type": "CompleteDataFeed",
"provider": {
"@type": "Organization",
"name": "Media Site",
"logo": "https://www.example.org/logo.jpg",
"member": {
"@type": "Person",
"name": "Becca Hughes",
"image": "https://www.example.org/profile_pic.jpg",
"email": "beccahughes@chromium.org"
}
}
}
)END");
}
EntityPtr MediaFeedsConverterTest::ValidMediaFeedItem() {
EntityPtr item = extractor_.Extract(
R"END(
{
"@type": "Movie",
"@id": "12345",
"name": "media feed",
"datePublished": "1970-01-01",
"image": "https://www.example.com/image.jpg",
"isFamilyFriendly": "https://schema.org/True"
}
)END");
item->properties.push_back(CreateEntityProperty(
schema_org::property::kPotentialAction, ValidWatchAction()));
return item;
}
mojom::MediaFeedItemPtr MediaFeedsConverterTest::ExpectedFeedItem() {
mojom::MediaFeedItemPtr expected_item = mojom::MediaFeedItem::New();
expected_item->type = mojom::MediaFeedItemType::kMovie;
expected_item->name = base::ASCIIToUTF16("media feed");
media_session::MediaImage expected_image;
expected_image.src = GURL("https://www.example.com/image.jpg");
expected_item->images.push_back(std::move(expected_image));
base::Time time;
bool got_time = base::Time::FromString("1970-01-01", &time);
DCHECK(got_time);
expected_item->date_published = time;
expected_item->is_family_friendly = true;
expected_item->action_status = mojom::MediaFeedItemActionStatus::kActive;
expected_item->action = mojom::Action::New();
expected_item->action->url = GURL("https://www.example.org");
expected_item->action->start_time = base::TimeDelta::FromHours(1);
return expected_item;
}
EntityPtr MediaFeedsConverterTest::AddItemToFeed(EntityPtr feed,
EntityPtr item) {
PropertyPtr data_feed_items = Property::New();
data_feed_items->name = schema_org::property::kDataFeedElement;
data_feed_items->values = Values::New();
data_feed_items->values->entity_values.push_back(std::move(item));
feed->properties.push_back(std::move(data_feed_items));
return feed;
}
TEST_F(MediaFeedsConverterTest, SucceedsOnValidCompleteDataFeed) {
std::vector<MediaFeedItemPtr> items;
EntityPtr entity = ValidMediaFeed();
auto result = GetMediaFeeds(std::move(entity));
EXPECT_TRUE(result.has_value());
EXPECT_TRUE(result.value().empty());
}
TEST_F(MediaFeedsConverterTest, SucceedsOnValidCompleteDataFeedWithItem) {
EntityPtr entity = AddItemToFeed(ValidMediaFeed(), ValidMediaFeedItem());
auto result = GetMediaFeeds(std::move(entity));
EXPECT_TRUE(result.has_value());
ASSERT_EQ(result.value().size(), 1u);
EXPECT_EQ(ExpectedFeedItem(), result.value()[0]);
}
TEST_F(MediaFeedsConverterTest, FailsWrongType) {
EntityPtr entity = Entity::New();
entity->type = "something else";
EXPECT_FALSE(GetMediaFeeds(std::move(entity)).has_value());
}
TEST_F(MediaFeedsConverterTest, FailsInvalidProviderOrganizationName) {
EntityPtr entity = ValidMediaFeed();
Property* organization =
GetProperty(entity.get(), schema_org::property::kProvider);
Property* organization_name =
GetProperty(organization->values->entity_values[0].get(),
schema_org::property::kName);
organization_name->values->string_values = {""};
EXPECT_FALSE(GetMediaFeeds(std::move(entity)).has_value());
}
TEST_F(MediaFeedsConverterTest, FailsInvalidProviderOrganizationLogo) {
EntityPtr entity = ValidMediaFeed();
Property* organization =
GetProperty(entity.get(), schema_org::property::kProvider);
Property* organization_name =
GetProperty(organization->values->entity_values[0].get(),
schema_org::property::kLogo);
organization_name->values->url_values = {GURL("")};
EXPECT_FALSE(GetMediaFeeds(std::move(entity)).has_value());
}
// Fails because the media feed item name is empty.
TEST_F(MediaFeedsConverterTest, FailsOnInvalidMediaFeedItemName) {
EntityPtr item = ValidMediaFeedItem();
auto* name = GetProperty(item.get(), schema_org::property::kName);
name->values->string_values[0] = "";
EntityPtr entity = AddItemToFeed(ValidMediaFeed(), std::move(item));
auto result = GetMediaFeeds(std::move(entity));
EXPECT_TRUE(result.has_value());
EXPECT_TRUE(result.value().empty());
}
// Fails because the date published is the wrong type (string instead of
// base::Time).
TEST_F(MediaFeedsConverterTest, FailsInvalidDatePublished) {
EntityPtr item = ValidMediaFeedItem();
auto* date_published =
GetProperty(item.get(), schema_org::property::kDatePublished);
auto& dates = date_published->values->date_time_values;
dates.erase(dates.begin());
date_published->values->string_values.push_back("1970-01-01");
EntityPtr entity = AddItemToFeed(ValidMediaFeed(), std::move(item));
auto result = GetMediaFeeds(std::move(entity));
EXPECT_TRUE(result.has_value());
EXPECT_TRUE(result.value().empty());
}
// Fails because the value of isFamilyFriendly property is not a parseable
// boolean type.
TEST_F(MediaFeedsConverterTest, FailsInvalidIsFamilyFriendly) {
EntityPtr item = ValidMediaFeedItem();
auto* is_family_friendly =
GetProperty(item.get(), schema_org::property::kIsFamilyFriendly);
is_family_friendly->values->string_values = {"True"};
is_family_friendly->values->bool_values.clear();
EntityPtr entity = AddItemToFeed(ValidMediaFeed(), std::move(item));
auto result = GetMediaFeeds(std::move(entity));
EXPECT_TRUE(result.has_value());
EXPECT_TRUE(result.value().empty());
}
// Fails because an active action does not contain a start time.
TEST_F(MediaFeedsConverterTest, FailsInvalidPotentialAction) {
EntityPtr item = ValidMediaFeedItem();
auto* action =
GetProperty(item.get(), schema_org::property::kPotentialAction);
auto* start_time = GetProperty(action->values->entity_values[0].get(),
schema_org::property::kStartTime);
start_time->values->time_values = {};
EntityPtr entity = AddItemToFeed(ValidMediaFeed(), std::move(item));
auto result = GetMediaFeeds(std::move(entity));
EXPECT_TRUE(result.has_value());
EXPECT_TRUE(result.value().empty());
}
// Succeeds with a valid author and duration on a video object. For other types
// of media, these fields are ignored, but they must be valid on video type.
TEST_F(MediaFeedsConverterTest, SucceedsItemWithAuthorAndDuration) {
EntityPtr item = ValidMediaFeedItem();
item->type = schema_org::entity::kVideoObject;
EntityPtr author = Entity::New();
author->type = schema_org::entity::kPerson;
author->properties.push_back(
CreateStringProperty(schema_org::property::kName, "Becca Hughes"));
author->properties.push_back(CreateUrlProperty(
schema_org::property::kUrl, GURL("https://www.google.com")));
item->properties.push_back(
CreateEntityProperty(schema_org::property::kAuthor, std::move(author)));
item->properties.push_back(
CreateTimeProperty(schema_org::property::kDuration, 1));
EntityPtr entity = AddItemToFeed(ValidMediaFeed(), std::move(item));
mojom::MediaFeedItemPtr expected_item = ExpectedFeedItem();
expected_item->type = mojom::MediaFeedItemType::kVideo;
expected_item->author = mojom::Author::New();
expected_item->author->name = "Becca Hughes";
expected_item->author->url = GURL("https://www.google.com");
expected_item->duration = base::TimeDelta::FromHours(1);
auto result = GetMediaFeeds(std::move(entity));
EXPECT_TRUE(result.has_value());
ASSERT_EQ(result.value().size(), 1u);
EXPECT_EQ(expected_item, result.value()[0]);
}
// Fails because the author's name is empty.
TEST_F(MediaFeedsConverterTest, FailsInvalidAuthor) {
EntityPtr item = ValidMediaFeedItem();
item->type = schema_org::entity::kVideoObject;
EntityPtr author = Entity::New();
author->type = schema_org::entity::kPerson;
author->properties.push_back(
CreateStringProperty(schema_org::property::kName, ""));
author->properties.push_back(CreateUrlProperty(
schema_org::property::kUrl, GURL("https://www.google.com")));
item->properties.push_back(
CreateEntityProperty(schema_org::property::kAuthor, std::move(author)));
item->properties.push_back(
CreateTimeProperty(schema_org::property::kDuration, 1));
EntityPtr entity = AddItemToFeed(ValidMediaFeed(), std::move(item));
auto result = GetMediaFeeds(std::move(entity));
EXPECT_TRUE(result.has_value());
EXPECT_TRUE(result.value().empty());
}
TEST_F(MediaFeedsConverterTest, SucceedsItemWithInteractionStatistic) {
EntityPtr item = ValidMediaFeedItem();
EntityPtr interaction_statistic = Entity::New();
interaction_statistic->type = schema_org::entity::kInteractionCounter;
interaction_statistic->properties.push_back(
CreateStringProperty(schema_org::property::kInteractionType,
"https://schema.org/WatchAction"));
interaction_statistic->properties.push_back(
CreateStringProperty(schema_org::property::kUserInteractionCount, "1"));
item->properties.push_back(
CreateEntityProperty(schema_org::property::kInteractionStatistic,
std::move(interaction_statistic)));
EntityPtr entity = AddItemToFeed(ValidMediaFeed(), std::move(item));
mojom::MediaFeedItemPtr expected_item = ExpectedFeedItem();
expected_item->interaction_counters = {
{mojom::InteractionCounterType::kWatch, 1}};
auto result = GetMediaFeeds(std::move(entity));
EXPECT_TRUE(result.has_value());
ASSERT_EQ(result.value().size(), 1u);
EXPECT_EQ(expected_item, result.value()[0]);
}
// Fails because the interaction statistic property has a duplicate of the watch
// interaction type.
TEST_F(MediaFeedsConverterTest, FailsInvalidInteractionStatistic) {
EntityPtr item = ValidMediaFeedItem();
PropertyPtr stats_property = Property::New();
stats_property->values = Values::New();
stats_property->name = schema_org::property::kInteractionStatistic;
{
EntityPtr interaction_statistic = Entity::New();
interaction_statistic->type = schema_org::entity::kInteractionCounter;
interaction_statistic->properties.push_back(
CreateStringProperty(schema_org::property::kInteractionType,
"https://schema.org/WatchAction"));
interaction_statistic->properties.push_back(
CreateStringProperty(schema_org::property::kUserInteractionCount, "1"));
stats_property->values->entity_values.push_back(
std::move(interaction_statistic));
}
{
EntityPtr interaction_statistic = Entity::New();
interaction_statistic->type = schema_org::entity::kInteractionCounter;
interaction_statistic->properties.push_back(
CreateStringProperty(schema_org::property::kInteractionType,
"https://schema.org/WatchAction"));
interaction_statistic->properties.push_back(
CreateStringProperty(schema_org::property::kUserInteractionCount, "3"));
stats_property->values->entity_values.push_back(
std::move(interaction_statistic));
}
item->properties.push_back(std::move(stats_property));
EntityPtr entity = AddItemToFeed(ValidMediaFeed(), std::move(item));
auto result = GetMediaFeeds(std::move(entity));
EXPECT_TRUE(result.has_value());
EXPECT_TRUE(result.value().empty());
}
TEST_F(MediaFeedsConverterTest, SucceedsItemWithRating) {
EntityPtr item = ValidMediaFeedItem();
{
EntityPtr rating = Entity::New();
rating->type = schema_org::entity::kRating;
rating->properties.push_back(
CreateStringProperty(schema_org::property::kAuthor, "MPAA"));
rating->properties.push_back(
CreateStringProperty(schema_org::property::kRatingValue, "G"));
item->properties.push_back(CreateEntityProperty(
schema_org::property::kContentRating, std::move(rating)));
}
EntityPtr entity = AddItemToFeed(ValidMediaFeed(), std::move(item));
mojom::MediaFeedItemPtr expected_item = ExpectedFeedItem();
mojom::ContentRatingPtr rating = mojom::ContentRating::New();
rating->agency = "MPAA";
rating->value = "G";
expected_item->content_ratings.push_back(std::move(rating));
auto result = GetMediaFeeds(std::move(entity));
EXPECT_TRUE(result.has_value());
ASSERT_EQ(result.value().size(), 1u);
EXPECT_EQ(expected_item, result.value()[0]);
}
// Fails because the rating property has a rating from an unknown agency.
TEST_F(MediaFeedsConverterTest, FailsInvalidRating) {
EntityPtr item = ValidMediaFeedItem();
EntityPtr rating = Entity::New();
rating->type = schema_org::entity::kRating;
rating->properties.push_back(
CreateStringProperty(schema_org::property::kAuthor, "Google"));
rating->properties.push_back(
CreateStringProperty(schema_org::property::kRatingValue, "Googley"));
item->properties.push_back(CreateEntityProperty(
schema_org::property::kContentRating, std::move(rating)));
EntityPtr entity = AddItemToFeed(ValidMediaFeed(), std::move(item));
auto result = GetMediaFeeds(std::move(entity));
EXPECT_TRUE(result.has_value());
EXPECT_TRUE(result.value().empty());
}
TEST_F(MediaFeedsConverterTest, SucceedsItemWithGenre) {
EntityPtr item = ValidMediaFeedItem();
item->properties.push_back(
CreateStringProperty(schema_org::property::kGenre, "Action"));
EntityPtr entity = AddItemToFeed(ValidMediaFeed(), std::move(item));
mojom::MediaFeedItemPtr expected_item = ExpectedFeedItem();
expected_item->genre.push_back("Action");
auto result = GetMediaFeeds(std::move(entity));
EXPECT_TRUE(result.has_value());
ASSERT_EQ(result.value().size(), 1u);
EXPECT_EQ(expected_item, result.value()[0]);
}
TEST_F(MediaFeedsConverterTest, FailsItemWithInvalidGenre) {
EntityPtr item = ValidMediaFeedItem();
item->properties.push_back(
CreateStringProperty(schema_org::property::kGenre, ""));
EntityPtr entity = AddItemToFeed(ValidMediaFeed(), std::move(item));
auto result = GetMediaFeeds(std::move(entity));
EXPECT_TRUE(result.has_value());
EXPECT_TRUE(result.value().empty());
}
TEST_F(MediaFeedsConverterTest, SucceedsItemWithLiveDetails) {
EntityPtr item = ValidMediaFeedItem();
EntityPtr publication = Entity::New();
publication->type = schema_org::entity::kBroadcastEvent;
publication->properties.push_back(
CreateDateTimeProperty(schema_org::property::kStartDate, "2020-03-22"));
publication->properties.push_back(
CreateDateTimeProperty(schema_org::property::kEndDate, "2020-03-23"));
item->properties.push_back(CreateEntityProperty(
schema_org::property::kPublication, std::move(publication)));
EntityPtr entity = AddItemToFeed(ValidMediaFeed(), std::move(item));
mojom::MediaFeedItemPtr expected_item = ExpectedFeedItem();
expected_item->live = mojom::LiveDetails::New();
base::Time start_time, end_time;
bool parsed_start = base::Time::FromString("2020-03-22", &start_time);
bool parsed_end = base::Time::FromString("2020-03-23", &end_time);
DCHECK(parsed_start && parsed_end);
expected_item->live->start_time = start_time;
expected_item->live->end_time = end_time;
auto result = GetMediaFeeds(std::move(entity));
EXPECT_TRUE(result.has_value());
ASSERT_EQ(result.value().size(), 1u);
EXPECT_EQ(expected_item, result.value()[0]);
}
// Fails because the end date is string type instead of date type.
TEST_F(MediaFeedsConverterTest, FailsItemWithInvalidLiveDetails) {
EntityPtr item = ValidMediaFeedItem();
EntityPtr publication = Entity::New();
publication->type = schema_org::entity::kBroadcastEvent;
publication->properties.push_back(
CreateDateTimeProperty(schema_org::property::kStartTime, "2020-03-22"));
publication->properties.push_back(
CreateStringProperty(schema_org::property::kEndTime, "2020-03-23"));
item->properties.push_back(CreateEntityProperty(
schema_org::property::kPublication, std::move(publication)));
EntityPtr entity = AddItemToFeed(ValidMediaFeed(), std::move(item));
auto result = GetMediaFeeds(std::move(entity));
EXPECT_TRUE(result.has_value());
EXPECT_TRUE(result.value().empty());
}
TEST_F(MediaFeedsConverterTest, SucceedsItemWithIdentifier) {
EntityPtr item = ValidMediaFeedItem();
{
EntityPtr identifier = Entity::New();
identifier->type = schema_org::entity::kPropertyValue;
identifier->properties.push_back(
CreateStringProperty(schema_org::property::kPropertyID, "TMS_ROOT_ID"));
identifier->properties.push_back(
CreateStringProperty(schema_org::property::kValue, "1"));
item->properties.push_back(CreateEntityProperty(
schema_org::property::kIdentifier, std::move(identifier)));
}
EntityPtr entity = AddItemToFeed(ValidMediaFeed(), std::move(item));
mojom::MediaFeedItemPtr expected_item = ExpectedFeedItem();
mojom::IdentifierPtr identifier = mojom::Identifier::New();
identifier->type = mojom::Identifier::Type::kTMSRootId;
identifier->value = "1";
expected_item->identifiers.push_back(std::move(identifier));
auto result = GetMediaFeeds(std::move(entity));
EXPECT_TRUE(result.has_value());
ASSERT_EQ(result.value().size(), 1u);
EXPECT_EQ(expected_item, result.value()[0]);
}
TEST_F(MediaFeedsConverterTest, SucceedsItemWithInvalidIdentifier) {
EntityPtr item = ValidMediaFeedItem();
{
EntityPtr identifier = Entity::New();
identifier->type = schema_org::entity::kPropertyValue;
identifier->properties.push_back(
CreateStringProperty(schema_org::property::kPropertyID, "Unknown"));
identifier->properties.push_back(
CreateStringProperty(schema_org::property::kValue, "1"));
item->properties.push_back(CreateEntityProperty(
schema_org::property::kPublication, std::move(identifier)));
}
EntityPtr entity = AddItemToFeed(ValidMediaFeed(), std::move(item));
auto result = GetMediaFeeds(std::move(entity));
EXPECT_TRUE(result.has_value());
EXPECT_TRUE(result.value().empty());
}
// Successfully converts a TV episode with embedded watch action and optional
// identifiers.
TEST_F(MediaFeedsConverterTest, SucceedsItemWithTVEpisode) {
EntityPtr item = ValidMediaFeedItem();
item->type = schema_org::entity::kTVSeries;
// Ignore the item's action field by changing the name. Use the action
// embedded in the TV episode instead.
GetProperty(item.get(), schema_org::property::kPotentialAction)->name =
"not an action";
item->properties.push_back(
CreateLongProperty(schema_org::property::kNumberOfEpisodes, 20));
item->properties.push_back(
CreateLongProperty(schema_org::property::kNumberOfSeasons, 6));
{
EntityPtr episode = Entity::New();
episode->type = schema_org::entity::kTVEpisode;
episode->properties.push_back(
CreateLongProperty(schema_org::property::kEpisodeNumber, 1));
episode->properties.push_back(
CreateStringProperty(schema_org::property::kName, "Pilot"));
EntityPtr identifier = Entity::New();
identifier->type = schema_org::entity::kPropertyValue;
identifier->properties.push_back(
CreateStringProperty(schema_org::property::kPropertyID, "TMS_ROOT_ID"));
identifier->properties.push_back(
CreateStringProperty(schema_org::property::kValue, "1"));
episode->properties.push_back(CreateEntityProperty(
schema_org::property::kIdentifier, std::move(identifier)));
episode->properties.push_back(CreateEntityProperty(
schema_org::property::kPotentialAction, ValidWatchAction()));
item->properties.push_back(CreateEntityProperty(
schema_org::property::kEpisode, std::move(episode)));
}
EntityPtr entity = AddItemToFeed(ValidMediaFeed(), std::move(item));
mojom::MediaFeedItemPtr expected_item = ExpectedFeedItem();
expected_item->type = mojom::MediaFeedItemType::kTVSeries;
expected_item->tv_episode = mojom::TVEpisode::New();
expected_item->tv_episode->episode_number = 1;
expected_item->tv_episode->name = "Pilot";
mojom::IdentifierPtr identifier = mojom::Identifier::New();
identifier->type = mojom::Identifier::Type::kTMSRootId;
identifier->value = "1";
expected_item->tv_episode->identifiers.push_back(std::move(identifier));
auto result = GetMediaFeeds(std::move(entity));
EXPECT_TRUE(result.has_value());
ASSERT_EQ(result.value().size(), 1u);
EXPECT_EQ(expected_item, result.value()[0]);
}
// Fails because TV episode is present, but TV episode name is empty.
TEST_F(MediaFeedsConverterTest, FailsItemWithInvalidTVEpisode) {
EntityPtr item = ValidMediaFeedItem();
item->type = schema_org::entity::kTVSeries;
item->properties.push_back(
CreateLongProperty(schema_org::property::kNumberOfEpisodes, 20));
item->properties.push_back(
CreateLongProperty(schema_org::property::kNumberOfSeasons, 6));
EntityPtr episode = Entity::New();
episode->type = schema_org::entity::kTVEpisode;
episode->properties.push_back(
CreateLongProperty(schema_org::property::kEpisodeNumber, 1));
episode->properties.push_back(
CreateStringProperty(schema_org::property::kName, ""));
episode->properties.push_back(CreateEntityProperty(
schema_org::property::kPotentialAction, ValidWatchAction()));
item->properties.push_back(
CreateEntityProperty(schema_org::property::kEpisode, std::move(episode)));
EntityPtr entity = AddItemToFeed(ValidMediaFeed(), std::move(item));
auto result = GetMediaFeeds(std::move(entity));
EXPECT_TRUE(result.has_value());
EXPECT_TRUE(result.value().empty());
}
TEST_F(MediaFeedsConverterTest, SucceedsItemWithTVSeason) {
EntityPtr item = ValidMediaFeedItem();
item->type = schema_org::entity::kTVSeries;
item->properties.push_back(
CreateLongProperty(schema_org::property::kNumberOfEpisodes, 20));
item->properties.push_back(
CreateLongProperty(schema_org::property::kNumberOfSeasons, 6));
{
EntityPtr season = Entity::New();
season->type = schema_org::entity::kTVSeason;
season->properties.push_back(
CreateLongProperty(schema_org::property::kSeasonNumber, 1));
season->properties.push_back(
CreateLongProperty(schema_org::property::kNumberOfEpisodes, 20));
item->properties.push_back(CreateEntityProperty(
schema_org::property::kContainsSeason, std::move(season)));
}
EntityPtr entity = AddItemToFeed(ValidMediaFeed(), std::move(item));
mojom::MediaFeedItemPtr expected_item = ExpectedFeedItem();
expected_item->type = mojom::MediaFeedItemType::kTVSeries;
expected_item->tv_episode = mojom::TVEpisode::New();
expected_item->tv_episode->season_number = 1;
auto result = GetMediaFeeds(std::move(entity));
EXPECT_TRUE(result.has_value());
ASSERT_EQ(result.value().size(), 1u);
EXPECT_EQ(expected_item, result.value()[0]);
}
TEST_F(MediaFeedsConverterTest, FailsItemWithInvalidTVSeason) {
EntityPtr item = ValidMediaFeedItem();
item->type = schema_org::entity::kTVSeries;
item->properties.push_back(
CreateLongProperty(schema_org::property::kNumberOfEpisodes, 20));
item->properties.push_back(
CreateLongProperty(schema_org::property::kNumberOfSeasons, 6));
{
EntityPtr season = Entity::New();
season->type = schema_org::entity::kTVSeason;
season->properties.push_back(
CreateLongProperty(schema_org::property::kSeasonNumber, 1));
season->properties.push_back(
CreateLongProperty(schema_org::property::kNumberOfEpisodes, -1));
item->properties.push_back(CreateEntityProperty(
schema_org::property::kContainsSeason, std::move(season)));
}
EntityPtr entity = AddItemToFeed(ValidMediaFeed(), std::move(item));
auto result = GetMediaFeeds(std::move(entity));
EXPECT_TRUE(result.has_value());
EXPECT_TRUE(result.value().empty());
}
} // namespace media_feeds
......@@ -3158,6 +3158,7 @@ test("unit_tests") {
"../browser/mac/keystone_glue_unittest.mm",
"../browser/media/android/router/media_router_android_unittest.cc",
"../browser/media/cast_mirroring_service_host_unittest.cc",
"../browser/media/feeds/media_feeds_converter_unittest.cc",
"../browser/media/feeds/media_feeds_fetcher_unittest.cc",
"../browser/media/feeds/media_feeds_service_unittest.cc",
"../browser/media/history/media_history_keyed_service_unittest.cc",
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment