You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@sling.apache.org by di...@apache.org on 2021/12/31 13:23:00 UTC

[sling-org-apache-sling-sitemap] branch issue/SLING-10561 created (now 4e67ab7)

This is an automated email from the ASF dual-hosted git repository.

diru pushed a change to branch issue/SLING-10561
in repository https://gitbox.apache.org/repos/asf/sling-org-apache-sling-sitemap.git.


      at 4e67ab7  SLING-10561: add google video extension

This branch includes the following new commits:

     new 4e67ab7  SLING-10561: add google video extension

The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


[sling-org-apache-sling-sitemap] 01/01: SLING-10561: add google video extension

Posted by di...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

diru pushed a commit to branch issue/SLING-10561
in repository https://gitbox.apache.org/repos/asf/sling-org-apache-sling-sitemap.git

commit 4e67ab75766cdb32d9dfdd78d28871e60ab19651
Author: Dirk Rudolph <di...@apache.org>
AuthorDate: Fri Dec 31 14:22:47 2021 +0100

    SLING-10561: add google video extension
---
 pom.xml                                            |   1 +
 .../builder/extensions/GoogleVideoExtension.java   | 198 +++++++
 .../extensions/GoogleVideoExtensionProvider.java   | 410 +++++++++++++
 .../sitemap/impl/builder/AbstractBuilderTest.java  |   4 +-
 .../extensions/GoogleVideoExtensionTest.java       | 204 +++++++
 src/test/resources/sitemap-video-1.1.xsd           | 643 +++++++++++++++++++++
 6 files changed, 1459 insertions(+), 1 deletion(-)

diff --git a/pom.xml b/pom.xml
index 7b6fee6..3972db5 100644
--- a/pom.xml
+++ b/pom.xml
@@ -67,6 +67,7 @@
                         <!-- Obtained from https://www.google.com/ -->
                         <exclude>src/test/resources/sitemap-image-1.1.xsd</exclude>
                         <exclude>src/test/resources/sitemap-news-0.9.xsd</exclude>
+                        <exclude>src/test/resources/sitemap-video-1.1.xsd</exclude>
                     </excludes>
                 </configuration>
             </plugin>
diff --git a/src/main/java/org/apache/sling/sitemap/builder/extensions/GoogleVideoExtension.java b/src/main/java/org/apache/sling/sitemap/builder/extensions/GoogleVideoExtension.java
new file mode 100644
index 0000000..aacafb4
--- /dev/null
+++ b/src/main/java/org/apache/sling/sitemap/builder/extensions/GoogleVideoExtension.java
@@ -0,0 +1,198 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sling.sitemap.builder.extensions;
+
+import org.apache.sling.sitemap.builder.Extension;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+import org.osgi.annotation.versioning.ProviderType;
+
+import java.time.LocalDate;
+import java.time.OffsetDateTime;
+import java.util.Collection;
+
+/**
+ * An extension to add video links and metadata to an {@link org.apache.sling.sitemap.builder.Url}.
+ *
+ * @see <a href="https://developers.google.com/search/docs/advanced/sitemaps/video-sitemaps">Video sitemaps and alternatives</a>
+ */
+@ProviderType
+public interface GoogleVideoExtension extends Extension {
+
+    enum Platform {
+        WEB("web"),
+        MOBILE("mobile"),
+        TV("tv");
+
+        private final String value;
+
+        Platform(String value) {
+            this.value = value;
+        }
+
+        public String getValue() {
+            return value;
+        }
+    }
+
+    enum Access {
+        ALLOW("allow"),
+        DENY("deny");
+
+        private final String value;
+
+        Access(String value) {
+            this.value = value;
+        }
+
+        public String getValue() {
+            return value;
+        }
+    }
+
+    enum PriceType {
+        PURCHASE("purchase"),
+        RENT("rent");
+
+        private final String value;
+
+        PriceType(String value) {
+            this.value = value;
+        }
+
+        public String getValue() {
+            return value;
+        }
+    }
+
+    enum Resolution {
+        STANDARD_DEFINITION("sd"),
+        HIGH_DEFINITION("hd");
+
+        private final String value;
+
+        Resolution(String value) {
+            this.value = value;
+        }
+
+        public String getValue() {
+            return value;
+        }
+    }
+
+    /**
+     * {@code thumbnail_loc}
+     *
+     * @param thumbnailLocation
+     * @return
+     */
+    @NotNull
+    GoogleVideoExtension setThumbnail(@NotNull String thumbnailLocation);
+
+    @NotNull
+    GoogleVideoExtension setTitle(@NotNull String title);
+
+    @NotNull
+    GoogleVideoExtension setDescription(@NotNull String description);
+
+    /**
+     * {@code content_loc}
+     *
+     * @param contentLocation
+     * @return
+     */
+    @NotNull
+    GoogleVideoExtension setUrl(String contentLocation);
+
+    /**
+     * {@code player_loc}
+     *
+     * @param playerLocation
+     * @return
+     */
+    @NotNull
+    GoogleVideoExtension setPlayerUrl(String playerLocation);
+
+    @NotNull
+    GoogleVideoExtension setDuration(Integer duration);
+
+    @NotNull
+    GoogleVideoExtension setExpirationDate(LocalDate date);
+
+    @NotNull
+    GoogleVideoExtension setExpirationDate(OffsetDateTime date);
+
+    @NotNull
+    GoogleVideoExtension setRating(Float rating);
+
+    @NotNull
+    GoogleVideoExtension setViewCount(Integer viewCount);
+
+    @NotNull
+    GoogleVideoExtension setPublicationDate(LocalDate date);
+
+    @NotNull
+    GoogleVideoExtension setPublicationDate(OffsetDateTime date);
+
+    @NotNull
+    GoogleVideoExtension setTags(Collection<String> tags);
+
+    @NotNull
+    GoogleVideoExtension setCategory(String category);
+
+    @NotNull
+    GoogleVideoExtension setFamilyFriendly(Boolean familyFriendly);
+
+    /**
+     * {@code restriction}
+     *
+     * @param restriction
+     * @param countryCodes
+     * @return
+     */
+    @NotNull
+    GoogleVideoExtension setAccessRestriction(Access restriction, Collection<String> countryCodes);
+
+    /**
+     * {@code platform}
+     *
+     * @param restriction
+     * @param platforms
+     * @return
+     */
+    @NotNull
+    GoogleVideoExtension setPlatformRestriction(Access restriction, Collection<Platform> platforms);
+
+    @NotNull
+    GoogleVideoExtension addPrice(float price, String currency, @Nullable PriceType type, @Nullable Resolution resolution);
+
+    @NotNull
+    GoogleVideoExtension setRequiresSubscription(Boolean requiresSubscription);
+
+    @NotNull
+    GoogleVideoExtension setUploader(String uploader);
+
+    @NotNull
+    GoogleVideoExtension setUploaderUrl(String uploaderInfo);
+
+    @NotNull
+    GoogleVideoExtension setLive(Boolean live);
+
+
+}
diff --git a/src/main/java/org/apache/sling/sitemap/impl/builder/extensions/GoogleVideoExtensionProvider.java b/src/main/java/org/apache/sling/sitemap/impl/builder/extensions/GoogleVideoExtensionProvider.java
new file mode 100644
index 0000000..aac06a4
--- /dev/null
+++ b/src/main/java/org/apache/sling/sitemap/impl/builder/extensions/GoogleVideoExtensionProvider.java
@@ -0,0 +1,410 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sling.sitemap.impl.builder.extensions;
+
+import org.apache.sling.sitemap.builder.extensions.GoogleVideoExtension;
+import org.apache.sling.sitemap.spi.builder.AbstractExtension;
+import org.apache.sling.sitemap.spi.builder.SitemapExtensionProvider;
+import org.jetbrains.annotations.NotNull;
+import org.osgi.service.component.annotations.Component;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.xml.stream.XMLStreamException;
+import javax.xml.stream.XMLStreamWriter;
+import java.time.LocalDate;
+import java.time.OffsetDateTime;
+import java.time.format.DateTimeFormatter;
+import java.time.temporal.TemporalAccessor;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.Locale;
+import java.util.stream.Collectors;
+
+@Component(
+        property = {
+                SitemapExtensionProvider.PROPERTY_INTERFACE + "=org.apache.sling.sitemap.builder.extensions.GoogleVideoExtension",
+                SitemapExtensionProvider.PROPERTY_PREFIX + "=video",
+                SitemapExtensionProvider.PROPERTY_NAMESPACE + "=http://www.google.com/schemas/sitemap-video/1.1",
+                SitemapExtensionProvider.PROPERTY_LOCAL_NAME + "=video"
+        }
+)
+public class GoogleVideoExtensionProvider implements SitemapExtensionProvider {
+
+    private static final Logger LOG = LoggerFactory.getLogger(GoogleVideoExtensionProvider.class);
+
+    @Override
+    @NotNull
+    public AbstractExtension newInstance() {
+        return new ExtensionImpl();
+    }
+
+    private static class PriceImpl {
+        private String currency;
+        private String type;
+        private String resolution;
+        private String price;
+    }
+
+    private static class ExtensionImpl extends AbstractExtension implements GoogleVideoExtension {
+
+        private String thumbnailLocation;
+        private String title;
+        private String description;
+        private String contentLocation;
+        private String playerLocation;
+        private String duration;
+        private TemporalAccessor expirationDate;
+        private String rating;
+        private String viewCount;
+        private TemporalAccessor publicationDate;
+        private List<String> tags;
+        private String category;
+        private String familyFriendly;
+        private String accessRestrictions;
+        private String accessRestrictionsRel;
+        private String platformRestrictions;
+        private String platformRestrictionsRel;
+        private Collection<PriceImpl> prices;
+        private String requiresSubscription;
+        private String uploader;
+        private String uploaderInfo;
+        private String live;
+
+        private static String booleanToString(Boolean bool) {
+            return bool != null ? (bool ? "yes" : "no") : null;
+        }
+
+        private static String required(String object, String message) throws XMLStreamException {
+            if (object == null) {
+                throw new XMLStreamException(message);
+            }
+            return object;
+        }
+
+        private static void write(XMLStreamWriter writer, String value, String tag) throws XMLStreamException {
+            writer.writeStartElement(tag);
+            writer.writeCharacters(value);
+            writer.writeEndElement();
+        }
+
+        private static void write(XMLStreamWriter writer, TemporalAccessor date, String tag) throws XMLStreamException {
+            try {
+                if (date instanceof LocalDate) {
+                    write(writer, DateTimeFormatter.ISO_LOCAL_DATE.format(date), tag);
+                } else {
+                    write(writer, DateTimeFormatter.ISO_OFFSET_DATE_TIME.format(date), tag);
+                }
+            } catch (RuntimeException ex) {
+                throw new XMLStreamException("failed to write " + tag, ex);
+            }
+        }
+
+        private static void writeCond(XMLStreamWriter writer, String value, String tag) throws XMLStreamException {
+            if (value != null) {
+                write(writer, value, tag);
+            }
+        }
+
+        private static void writeCond(XMLStreamWriter writer, TemporalAccessor value, String tag) throws XMLStreamException {
+            if (value != null) {
+                write(writer, value, tag);
+            }
+        }
+
+        private static void writeReq(XMLStreamWriter writer, String value, String tag, String msg) throws XMLStreamException {
+            write(writer, required(value, msg), tag);
+        }
+
+        @Override
+        @NotNull
+        public GoogleVideoExtension setThumbnail(@NotNull String thumbnailLocation) {
+            this.thumbnailLocation = thumbnailLocation;
+            return this;
+        }
+
+        @Override
+        @NotNull
+        public GoogleVideoExtension setTitle(@NotNull String title) {
+            this.title = title;
+            return this;
+        }
+
+        @Override
+        @NotNull
+        public GoogleVideoExtension setDescription(@NotNull String description) {
+            this.description = description;
+            return this;
+        }
+
+        @Override
+        @NotNull
+        public GoogleVideoExtension setUrl(String contentLocation) {
+            this.contentLocation = contentLocation;
+            return this;
+        }
+
+        @Override
+        @NotNull
+        public GoogleVideoExtension setPlayerUrl(String playerLocation) {
+            this.playerLocation = playerLocation;
+            return this;
+        }
+
+        @Override
+        @NotNull
+        public GoogleVideoExtension setDuration(Integer duration) {
+            if (duration != null) {
+                if (duration < 0 || duration > 28800) {
+                    LOG.warn("Adjusting duration as it is out of bounds (0, 28800): {}", duration);
+                    duration = Math.max(0, Math.min(duration, 28800));
+                }
+                this.duration = String.valueOf(duration);
+            } else {
+                this.duration = null;
+            }
+            return this;
+        }
+
+        @Override
+        @NotNull
+        public GoogleVideoExtension setExpirationDate(LocalDate date) {
+            this.expirationDate = date;
+            return this;
+        }
+
+        @Override
+        @NotNull
+        public GoogleVideoExtension setExpirationDate(OffsetDateTime date) {
+            this.expirationDate = date;
+            return this;
+        }
+
+        @Override
+        @NotNull
+        public GoogleVideoExtension setRating(Float rating) {
+            if (rating != null) {
+                if (rating < 0 || rating > 5) {
+                    LOG.warn("Adjusting rating as it is out of bounds (0,5): {}", rating);
+                    rating = Math.max(0.0f, Math.min(rating, 5.0f));
+                }
+                this.rating = String.valueOf(rating);
+            } else {
+                this.rating = null;
+            }
+            return this;
+        }
+
+        @Override
+        @NotNull
+        public GoogleVideoExtension setViewCount(Integer viewCount) {
+            if (viewCount != null) {
+                if (viewCount < 0) {
+                    LOG.warn("Adjusting negative view count: {}", viewCount);
+                    viewCount = 0;
+                }
+                this.viewCount = String.valueOf(viewCount);
+            } else {
+                this.viewCount = null;
+            }
+            return this;
+        }
+
+        @Override
+        @NotNull
+        public GoogleVideoExtension setPublicationDate(LocalDate date) {
+            this.publicationDate = date;
+            return this;
+        }
+
+        @Override
+        @NotNull
+        public GoogleVideoExtension setPublicationDate(OffsetDateTime date) {
+            this.publicationDate = date;
+            return this;
+        }
+
+        @Override
+        @NotNull
+        public GoogleVideoExtension setTags(Collection<String> tags) {
+            this.tags = new ArrayList<>(tags);
+            return this;
+        }
+
+        @Override
+        @NotNull
+        public GoogleVideoExtension setCategory(String category) {
+            this.category = category;
+            return this;
+        }
+
+        @Override
+        @NotNull
+        public GoogleVideoExtension setFamilyFriendly(Boolean familyFriendly) {
+            this.familyFriendly = booleanToString(familyFriendly);
+            return this;
+        }
+
+        @Override
+        @NotNull
+        public GoogleVideoExtension setAccessRestriction(Access restriction, Collection<String> countryCodes) {
+            String accessRestrictions = countryCodes.stream()
+                    .map(countryCode -> countryCode.toUpperCase(Locale.ROOT))
+                    .filter(countryCode -> countryCode.length() == 2)
+                    .collect(Collectors.joining(" "));
+            if (accessRestrictions.length() > 0) {
+                this.accessRestrictions = accessRestrictions;
+                this.accessRestrictionsRel = restriction.getValue();
+            } else {
+                this.accessRestrictions = null;
+                this.accessRestrictionsRel = null;
+            }
+            return this;
+        }
+
+        @Override
+        @NotNull
+        public GoogleVideoExtension setPlatformRestriction(Access restriction, Collection<Platform> platforms) {
+            this.platformRestrictions = platforms.stream().map(Platform::getValue).collect(Collectors.joining(" "));
+            this.platformRestrictionsRel = restriction.getValue();
+            return this;
+        }
+
+        @Override
+        @NotNull
+        public GoogleVideoExtension addPrice(float price, String currency, PriceType type, Resolution resolution) {
+            if (prices == null) {
+                this.prices = new ArrayList<>();
+            }
+            PriceImpl newPrice = new PriceImpl();
+            newPrice.price = String.valueOf(price);
+            newPrice.currency = currency;
+            newPrice.type = type != null ? type.getValue() : null;
+            newPrice.resolution = resolution != null ? resolution.getValue() : null;
+            prices.add(newPrice);
+            return this;
+        }
+
+        @Override
+        @NotNull
+        public GoogleVideoExtension setRequiresSubscription(Boolean requiresSubscription) {
+            this.requiresSubscription = booleanToString(requiresSubscription);
+            return this;
+        }
+
+        @Override
+        @NotNull
+        public GoogleVideoExtension setUploader(String uploader) {
+            this.uploader = uploader;
+            return this;
+        }
+
+        @Override
+        @NotNull
+        public GoogleVideoExtension setUploaderUrl(String uploaderInfo) {
+            this.uploaderInfo = uploaderInfo;
+            return this;
+        }
+
+        @Override
+        @NotNull
+        public GoogleVideoExtension setLive(Boolean live) {
+            this.live = booleanToString(live);
+            return this;
+        }
+
+        @Override
+        public void writeTo(@NotNull XMLStreamWriter writer) throws XMLStreamException {
+            writeReq(writer, thumbnailLocation, "thumbnail_loc", "thumbnail location missing");
+            writeReq(writer, title, "title", "title missing");
+            writeReq(writer, description, "description", "description missing");
+
+            if (contentLocation == null && playerLocation == null) {
+                throw new XMLStreamException("either content location or player location is required");
+            } else if (contentLocation != null) {
+                write(writer, contentLocation, "content_loc");
+            } else {
+                write(writer, playerLocation, "player_loc");
+            }
+
+            writeCond(writer, duration, "duration");
+            writeCond(writer, expirationDate, "expiration_date");
+            writeCond(writer, rating, "rating");
+            writeCond(writer, viewCount, "view_count");
+            writeCond(writer, publicationDate, "publication_date");
+
+            if (tags != null) {
+                if (tags.size() > 32) {
+                    LOG.warn("Truncating tags as more then 32 were given: {}", tags.size());
+                }
+                for (String tag : tags.subList(0, Math.min(tags.size(), 32))) {
+                    writer.writeStartElement("tag");
+                    writer.writeCharacters(tag);
+                    writer.writeEndElement();
+                }
+            }
+
+            writeCond(writer, category, "category");
+            writeCond(writer, familyFriendly, "family_friendly");
+
+            if (accessRestrictions != null) {
+                writer.writeStartElement("restriction");
+                writer.writeAttribute("relationship", accessRestrictionsRel);
+                writer.writeCharacters(accessRestrictions);
+                writer.writeEndElement();
+            }
+
+            if (prices != null) {
+                for (PriceImpl price : prices) {
+                    writer.writeStartElement("price");
+                    writer.writeAttribute("currency", price.currency);
+                    if (price.type != null) {
+                        writer.writeAttribute("type", price.type);
+                    }
+                    if (price.resolution != null) {
+                        writer.writeAttribute("resolution", price.resolution);
+                    }
+                    writer.writeCharacters(price.price);
+                    writer.writeEndElement();
+                }
+            }
+
+            writeCond(writer, requiresSubscription, "requires_subscription");
+
+            if (uploader != null) {
+                writer.writeStartElement("uploader");
+                if (uploaderInfo != null) {
+                    writer.writeAttribute("info", uploaderInfo);
+                }
+                writer.writeCharacters(uploader);
+                writer.writeEndElement();
+            }
+
+            if (platformRestrictions != null) {
+                writer.writeStartElement("platform");
+                writer.writeAttribute("relationship", platformRestrictionsRel);
+                writer.writeCharacters(platformRestrictions);
+                writer.writeEndElement();
+            }
+
+            writeCond(writer, live, "live");
+        }
+    }
+}
diff --git a/src/test/java/org/apache/sling/sitemap/impl/builder/AbstractBuilderTest.java b/src/test/java/org/apache/sling/sitemap/impl/builder/AbstractBuilderTest.java
index 2da65a7..2383648 100644
--- a/src/test/java/org/apache/sling/sitemap/impl/builder/AbstractBuilderTest.java
+++ b/src/test/java/org/apache/sling/sitemap/impl/builder/AbstractBuilderTest.java
@@ -44,9 +44,11 @@ public abstract class AbstractBuilderTest {
     private static final URL SITEMAP_INDEX_XSD = AbstractBuilderTest.class.getClassLoader().getResource("siteindex-0.9.xsd");
     private static final URL SITEMAP_IMAGE_XSD = AbstractBuilderTest.class.getClassLoader().getResource("sitemap-image-1.1.xsd");
     private static final URL SITEMAP_NEWS_XSD = AbstractBuilderTest.class.getClassLoader().getResource("sitemap-news-0.9.xsd");
+    private static final URL SITEMAP_VIDEO_XSD = AbstractBuilderTest.class.getClassLoader().getResource("sitemap-video-1.1.xsd");
 
     protected void assertSitemap(String expected, String given) {
-        assertEqualsAndValid(expected, given, XML_XSD, XHTML_XSD, SITEMAP_XSD, SITEMAP_IMAGE_XSD, SITEMAP_NEWS_XSD);
+        assertEqualsAndValid(expected, given, XML_XSD, XHTML_XSD, SITEMAP_XSD, SITEMAP_IMAGE_XSD, SITEMAP_NEWS_XSD,
+                SITEMAP_VIDEO_XSD);
     }
 
     protected void assertSitemapIndex(String expected, String given) {
diff --git a/src/test/java/org/apache/sling/sitemap/impl/builder/extensions/GoogleVideoExtensionTest.java b/src/test/java/org/apache/sling/sitemap/impl/builder/extensions/GoogleVideoExtensionTest.java
new file mode 100644
index 0000000..0e04c27
--- /dev/null
+++ b/src/test/java/org/apache/sling/sitemap/impl/builder/extensions/GoogleVideoExtensionTest.java
@@ -0,0 +1,204 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sling.sitemap.impl.builder.extensions;
+
+import org.apache.sling.sitemap.SitemapException;
+import org.apache.sling.sitemap.builder.Url;
+import org.apache.sling.sitemap.builder.extensions.GoogleImageExtension;
+import org.apache.sling.sitemap.builder.extensions.GoogleVideoExtension;
+import org.apache.sling.sitemap.impl.builder.AbstractBuilderTest;
+import org.apache.sling.sitemap.impl.builder.SitemapImpl;
+import org.apache.sling.testing.mock.sling.junit5.SlingContext;
+import org.apache.sling.testing.mock.sling.junit5.SlingContextExtension;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+
+import java.io.IOException;
+import java.io.StringWriter;
+import java.time.LocalDate;
+import java.time.OffsetDateTime;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
+
+@ExtendWith({SlingContextExtension.class})
+class GoogleVideoExtensionTest extends AbstractBuilderTest {
+
+    final SlingContext context = new SlingContext();
+
+    private ExtensionProviderManager extensionProviderManager;
+
+    @BeforeEach
+    void setup() {
+        context.registerInjectActivateService(new GoogleVideoExtensionProvider());
+        extensionProviderManager = context.registerInjectActivateService(new ExtensionProviderManager());
+    }
+
+    @Test
+    void testGoogleImageCombinations() throws SitemapException, IOException {
+        // given
+        StringWriter writer = new StringWriter();
+        SitemapImpl sitemap = new SitemapImpl(writer, extensionProviderManager);
+
+        // when
+        Url url = sitemap.addUrl("http://example.ch/de.html");
+        url.addExtension(GoogleVideoExtension.class)
+                .setUrl("http://example.ch/dam/de/videos/hero.mov")
+                .setThumbnail("http://example.ch/dam/de/images/hero_thumbnail.jpg")
+                .setTitle("Hero Video")
+                .setDescription("foo bar");
+        url.addExtension(GoogleVideoExtension.class)
+                .setUrl("http://example.ch/dam/de/videos/hero2.mov")
+                .setThumbnail("http://example.ch/dam/de/images/hero2_thumbnail.jpg")
+                .setTitle("Hero Video 2")
+                .setDescription("foo bar 2")
+                .setExpirationDate(LocalDate.parse("2022-12-31"))
+                .setPublicationDate(LocalDate.parse("2021-12-31"));
+        url.addExtension(GoogleVideoExtension.class)
+                .setPlayerUrl("http://example.ch/player.swf?video=hero3")
+                .setThumbnail("http://example.ch/dam/de/images/hero3_thumbnail.jpg")
+                .setTitle("Hero Video 3")
+                .setDescription("foo bar 3")
+                .setExpirationDate(OffsetDateTime.parse("2022-12-31T12:34:56.000+01:00"))
+                .setPublicationDate(OffsetDateTime.parse("2021-12-31T12:34:56.000+01:00"))
+                .setCategory("category")
+                .setAccessRestriction(GoogleVideoExtension.Access.ALLOW, Arrays.asList("de", "ch", "at"))
+                .setDuration(123)
+                .setViewCount(200)
+                .setFamilyFriendly(true)
+                .setLive(false)
+                .setPlatformRestriction(GoogleVideoExtension.Access.DENY, Collections.singleton(GoogleVideoExtension.Platform.TV))
+                .setRating(4.7f)
+                .setTags(Arrays.asList("foo", "bar"))
+                .addPrice(9.99f, "EUR", GoogleVideoExtension.PriceType.RENT, GoogleVideoExtension.Resolution.HIGH_DEFINITION)
+                .addPrice(19.99f, "EUR", null, null)
+                .setRequiresSubscription(true)
+                .setUploader("Foo Bar")
+                .setUploaderUrl("http://example.ch/people/foo-bar");
+        url.addExtension(GoogleVideoExtension.class)
+                .setPlayerUrl("http://example.ch/player.swf?video=hero4")
+                .setThumbnail("http://example.ch/dam/de/images/hero4_thumbnail.jpg")
+                .setTitle("Hero Video 4")
+                .setDescription("foo bar 4")
+                .setAccessRestriction(GoogleVideoExtension.Access.ALLOW, Collections.singleton("invalid"))
+                .setDuration(Integer.MAX_VALUE)
+                .setViewCount(-5)
+                .setRating(100.0f)
+                .setTags(IntStream.range(1, 50).mapToObj(String::valueOf).collect(Collectors.toList()));
+        sitemap.close();
+
+        // then
+        assertSitemap(
+                AbstractBuilderTest.XML_HEADER + "<urlset xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\" " +
+                        "xmlns:video=\"http://www.google.com/schemas/sitemap-video/1.1\">"
+                        + "<url>"
+                        + "<loc>http://example.ch/de.html</loc>"
+
+                        + "<video:video>"
+                        + "<video:thumbnail_loc>http://example.ch/dam/de/images/hero_thumbnail.jpg</video:thumbnail_loc>"
+                        + "<video:title>Hero Video</video:title>"
+                        + "<video:description>foo bar</video:description>"
+                        + "<video:content_loc>http://example.ch/dam/de/videos/hero.mov</video:content_loc>"
+                        + "</video:video>"
+
+                        + "<video:video>"
+                        + "<video:thumbnail_loc>http://example.ch/dam/de/images/hero2_thumbnail.jpg</video:thumbnail_loc>"
+                        + "<video:title>Hero Video 2</video:title>"
+                        + "<video:description>foo bar 2</video:description>"
+                        + "<video:content_loc>http://example.ch/dam/de/videos/hero2.mov</video:content_loc>"
+                        + "<video:expiration_date>2022-12-31</video:expiration_date>"
+                        + "<video:publication_date>2021-12-31</video:publication_date>"
+                        + "</video:video>"
+
+                        + "<video:video>"
+                        + "<video:thumbnail_loc>http://example.ch/dam/de/images/hero3_thumbnail.jpg</video:thumbnail_loc>"
+                        + "<video:title>Hero Video 3</video:title>"
+                        + "<video:description>foo bar 3</video:description>"
+                        + "<video:player_loc>http://example.ch/player.swf?video=hero3</video:player_loc>"
+                        + "<video:duration>123</video:duration>"
+                        + "<video:expiration_date>2022-12-31T12:34:56+01:00</video:expiration_date>"
+                        + "<video:rating>4.7</video:rating>"
+                        + "<video:view_count>200</video:view_count>"
+                        + "<video:publication_date>2021-12-31T12:34:56+01:00</video:publication_date>"
+                        + "<video:tag>foo</video:tag>"
+                        + "<video:tag>bar</video:tag>"
+                        + "<video:category>category</video:category>"
+                        + "<video:family_friendly>yes</video:family_friendly>"
+                        + "<video:restriction relationship=\"allow\">DE CH AT</video:restriction>"
+                        + "<video:price currency=\"EUR\" type=\"rent\" resolution=\"hd\">9.99</video:price>"
+                        + "<video:price currency=\"EUR\">19.99</video:price>"
+                        + "<video:requires_subscription>yes</video:requires_subscription>"
+                        + "<video:uploader info=\"http://example.ch/people/foo-bar\">Foo Bar</video:uploader>"
+                        + "<video:platform relationship=\"deny\">tv</video:platform>"
+                        + "<video:live>no</video:live>"
+                        + "</video:video>"
+
+                        + "<video:video>"
+                        + "<video:thumbnail_loc>http://example.ch/dam/de/images/hero4_thumbnail.jpg</video:thumbnail_loc>"
+                        + "<video:title>Hero Video 4</video:title>"
+                        + "<video:description>foo bar 4</video:description>"
+                        + "<video:player_loc>http://example.ch/player.swf?video=hero4</video:player_loc>"
+                        + "<video:duration>28800</video:duration>"
+                        + "<video:rating>5.0</video:rating>"
+                        + "<video:view_count>0</video:view_count>"
+                        + IntStream.range(1, 33)
+                        .mapToObj(i -> "<video:tag>" + i + "</video:tag>")
+                        .collect(Collectors.joining())
+                        + "</video:video>"
+
+                        + "</url>"
+                        + "</urlset>",
+                writer.toString()
+        );
+    }
+
+    @Test
+    void testNothingWrittenWhenExtensionMissesMandatoryProperties() throws SitemapException, IOException {
+        // given
+        StringWriter writer = new StringWriter();
+        SitemapImpl sitemap = new SitemapImpl(writer, extensionProviderManager);
+
+        // when
+        Url url = sitemap.addUrl("http://example.ch/de.html");
+        url.addExtension(GoogleVideoExtension.class);
+        url.addExtension(GoogleVideoExtension.class)
+                .setThumbnail("foobar");
+        url.addExtension(GoogleVideoExtension.class)
+                .setThumbnail("thumbnail location")
+                .setTitle("title");
+        url.addExtension(GoogleVideoExtension.class)
+                .setThumbnail("thumbnail location")
+                .setTitle("title")
+                .setDescription("description");
+        sitemap.close();
+
+        // then
+        assertSitemap(
+                AbstractBuilderTest.XML_HEADER + "<urlset xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\" " +
+                        "xmlns:video=\"http://www.google.com/schemas/sitemap-video/1.1\">"
+                        + "<url>"
+                        + "<loc>http://example.ch/de.html</loc>"
+                        + "</url>"
+                        + "</urlset>",
+                writer.toString()
+        );
+    }
+}
diff --git a/src/test/resources/sitemap-video-1.1.xsd b/src/test/resources/sitemap-video-1.1.xsd
new file mode 100644
index 0000000..4bac217
--- /dev/null
+++ b/src/test/resources/sitemap-video-1.1.xsd
@@ -0,0 +1,643 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<xsd:schema
+    xmlns:xsd="http://www.w3.org/2001/XMLSchema"
+    targetNamespace="http://www.google.com/schemas/sitemap-video/1.1"
+    xmlns="http://www.google.com/schemas/sitemap-video/1.1"
+    elementFormDefault="qualified">
+
+<xsd:annotation>
+  <xsd:documentation>
+    XML Schema for the Video Sitemap extension. This schema defines the
+    Video-specific elements only; the core Sitemap elements are defined
+    separately.
+
+    Help Center documentation for the Video Sitemap extension:
+
+      http://www.google.com/support/webmasters/bin/topic.py?topic=10079
+
+    Copyright 2010 Google Inc. All Rights Reserved.
+  </xsd:documentation>
+</xsd:annotation>
+
+<xsd:simpleType name="tYesNo">
+  <xsd:annotation>
+    <xsd:documentation>
+      A value that can be yes or no. Permitted cases are all-lowercase (yes/no),
+      all-uppercase (YES/NO) or starting with capital (Yes/No).
+    </xsd:documentation>
+  </xsd:annotation>
+  <xsd:restriction base="xsd:string">
+    <xsd:enumeration value="yes"/>
+    <xsd:enumeration value="Yes"/>
+    <xsd:enumeration value="YES"/>
+    <xsd:enumeration value="no"/>
+    <xsd:enumeration value="No"/>
+    <xsd:enumeration value="NO"/>
+  </xsd:restriction>
+</xsd:simpleType>
+
+<xsd:simpleType name="tCountryList">
+  <xsd:annotation>
+    <xsd:documentation>
+      Space-separated country codes in ISO 3166 format.
+
+      Country codes:
+      http://www.iso.org/iso/english_country_names_and_code_elements
+    </xsd:documentation>
+  </xsd:annotation>
+  <xsd:restriction base="xsd:string">
+    <xsd:pattern value="([A-Z]{2}( +[A-Z]{2})*)?"/>
+  </xsd:restriction>
+</xsd:simpleType>
+
+<xsd:simpleType name="tPlatformList">
+  <xsd:annotation>
+    <xsd:documentation>
+      Space-separated platform names.
+
+      Platform names:
+      web - desktop and laptop browsers.
+      mobile - mobile devices such as phones and tablets.
+      tv - tv platforms such as GoogleTV.
+    </xsd:documentation>
+  </xsd:annotation>
+  <xsd:restriction base="xsd:string">
+    <xsd:pattern value="((web|mobile|tv)( (web|mobile|tv))*)?"/>
+  </xsd:restriction>
+</xsd:simpleType>
+
+<xsd:element name="video">
+  <xsd:complexType>
+    <xsd:sequence>
+      <xsd:element name="thumbnail_loc" type="xsd:anyURI">
+        <xsd:annotation>
+          <xsd:documentation>
+            A URL pointing to the URL for the video thumbnail image file. We can
+            accept most image sizes/types but recommend your thumbnails are at
+            least 120x90 pixels in .jpg, .png, or. gif formats.
+          </xsd:documentation>
+        </xsd:annotation>
+      </xsd:element>
+
+      <xsd:element name="title">
+        <xsd:annotation>
+          <xsd:documentation>
+            The title of the video.
+          </xsd:documentation>
+        </xsd:annotation>
+        <xsd:simpleType>
+          <xsd:restriction base="xsd:string">
+            <xsd:maxLength value="100"/>
+          </xsd:restriction>
+        </xsd:simpleType>
+      </xsd:element>
+
+      <xsd:element name="description">
+        <xsd:annotation>
+          <xsd:documentation>
+            The description of the video.
+          </xsd:documentation>
+        </xsd:annotation>
+        <xsd:simpleType>
+          <xsd:restriction base="xsd:string">
+            <xsd:maxLength value="2048"/>
+          </xsd:restriction>
+        </xsd:simpleType>
+      </xsd:element>
+
+      <xsd:element name="content_loc" minOccurs="0" type="xsd:anyURI">
+        <xsd:annotation>
+          <xsd:documentation>
+            At least one of &lt;video:player_loc&gt; and
+            &lt;video:content_loc&gt; is required.
+
+            This should be a .mpg, .mpeg, .mp4, .m4v, .mov, .wmv, .asf, .avi,
+            .ra, .ram, .rm, .flv, or other video file format, and can be omitted
+            if &lt;video:player_loc&gt; is specified. However, because Google
+            needs to be able to check that the Flash object is actually a player
+            for video (as opposed to some other use of Flash, e.g. games and
+            animations), it's helpful to provide both.
+          </xsd:documentation>
+        </xsd:annotation>
+      </xsd:element>
+
+      <xsd:element name="player_loc" minOccurs="0">
+        <xsd:annotation>
+          <xsd:documentation>
+            At least one of &lt;video:player_loc&gt; and
+            &lt;video:content_loc&gt; is required.
+
+            A URL pointing to a Flash player for a specific video. In general,
+            this is the information in the src element of an &lt;embed&gt; tag
+            and should not be the same as the content of the &lt;loc&gt; tag.
+            ​Since each video is uniquely identified by its content URL (the
+            location of the actual video file) or, if a content URL is not
+            present, a player URL (a URL pointing to a player for the video),
+            you must include either the &lt;video:player_loc&gt; or
+            &lt;video:content_loc&gt; tags. If these tags are omitted and we
+            can't find this information, we'll be unable to index your video.
+          </xsd:documentation>
+        </xsd:annotation>
+        <xsd:complexType>
+          <xsd:simpleContent>
+            <xsd:extension base="xsd:anyURI">
+              <xsd:attribute name="allow_embed" type="tYesNo">
+                <xsd:annotation>
+                  <xsd:documentation>
+                    Attribute allow_embed specifies whether Google can embed the
+                    video in search results. Allowed values are "Yes" or "No".
+                    The default value is "Yes".
+                  </xsd:documentation>
+                </xsd:annotation>
+              </xsd:attribute>
+              <xsd:attribute name="autoplay" type="xsd:string">
+                <xsd:annotation>
+                  <xsd:documentation>
+                    User-defined string that Google may append (if appropriate)
+                    to the flashvars parameter to enable autoplay of the video.
+                  </xsd:documentation>
+                </xsd:annotation>
+              </xsd:attribute>
+            </xsd:extension>
+          </xsd:simpleContent>
+        </xsd:complexType>
+      </xsd:element>
+
+      <xsd:element name="duration" minOccurs="0">
+        <xsd:annotation>
+          <xsd:documentation>
+            The duration of the video in seconds.
+          </xsd:documentation>
+        </xsd:annotation>
+        <xsd:simpleType>
+          <xsd:restriction base="xsd:nonNegativeInteger">
+            <xsd:maxInclusive value="28800"/>
+          </xsd:restriction>
+        </xsd:simpleType>
+      </xsd:element>
+
+      <xsd:element name="expiration_date" minOccurs="0">
+        <xsd:annotation>
+          <xsd:documentation>
+            The date after which the video will no longer be available, in
+            W3C format. Acceptable values are complete date (YYYY-MM-DD) and
+            complete date plus hours, minutes and seconds, and timezone
+            (YYYY-MM-DDThh:mm:ss+TZD). For example, 2007-07-16T19:20:30+08:00.
+            Don't supply this information if your video does not expire.
+          </xsd:documentation>
+        </xsd:annotation>
+        <xsd:simpleType>
+          <xsd:union>
+            <xsd:simpleType>
+              <xsd:restriction base="xsd:date"/>
+            </xsd:simpleType>
+            <xsd:simpleType>
+              <xsd:restriction base="xsd:dateTime"/>
+            </xsd:simpleType>
+          </xsd:union>
+        </xsd:simpleType>
+      </xsd:element>
+
+      <xsd:element name="rating" minOccurs="0">
+        <xsd:annotation>
+          <xsd:documentation>
+            The rating of the video.
+          </xsd:documentation>
+        </xsd:annotation>
+        <xsd:simpleType>
+          <xsd:restriction base="xsd:decimal">
+            <xsd:minInclusive value="0"/>
+            <xsd:maxInclusive value="5"/>
+          </xsd:restriction>
+        </xsd:simpleType>
+      </xsd:element>
+
+      <xsd:element name="content_segment_loc"
+          minOccurs="0"
+          maxOccurs="unbounded">
+        <xsd:annotation>
+          <xsd:documentation>
+            Use &lt;video:content_segment_loc&gt; only in conjunction with
+            &lt;video:player_loc&gt;.
+
+            If you publish your video as a series of raw videos (for example, if
+            you submit a full movie as a continuous series of shorter clips),
+            you can use the &lt;video:content_segment_loc&gt; to supply us with
+            a series of URLs, in the order in which they should be concatenated
+            to recreate the video in its entirety. Each URL should point to a
+            .mpg, .mpeg, .mp4, .m4v, .mov, .wmv, .asf, .avi, .ra, .ram, .rm,
+            .flv, or other video file format. It should not point to any Flash
+            content.
+          </xsd:documentation>
+        </xsd:annotation>
+        <xsd:complexType>
+          <xsd:simpleContent>
+            <xsd:extension base="xsd:anyURI">
+              <xsd:attribute name="duration">
+                <xsd:annotation>
+                  <xsd:documentation>
+                    The duration of the clip in seconds.
+                  </xsd:documentation>
+                </xsd:annotation>
+                <xsd:simpleType>
+                  <xsd:restriction base="xsd:nonNegativeInteger">
+                    <xsd:maxInclusive value="28800"/>
+                  </xsd:restriction>
+                </xsd:simpleType>
+              </xsd:attribute>
+            </xsd:extension>
+          </xsd:simpleContent>
+        </xsd:complexType>
+      </xsd:element>
+
+      <xsd:element name="view_count"
+          minOccurs="0"
+          type="xsd:nonNegativeInteger">
+        <xsd:annotation>
+          <xsd:documentation>
+            The number of times the video has been viewed.
+          </xsd:documentation>
+        </xsd:annotation>
+      </xsd:element>
+
+      <xsd:element name="publication_date" minOccurs="0">
+        <xsd:annotation>
+          <xsd:documentation>
+            The date the video was first published, in W3C format. Acceptable
+            values are complete date (YYYY-MM-DD) and complete date plus hours,
+            minutes and seconds, and timezone (YYYY-MM-DDThh:mm:ss+TZD).
+            For example, 2007-07-16T19:20:30+08:00.
+          </xsd:documentation>
+        </xsd:annotation>
+        <xsd:simpleType>
+          <xsd:union>
+            <xsd:simpleType>
+              <xsd:restriction base="xsd:date"/>
+            </xsd:simpleType>
+            <xsd:simpleType>
+              <xsd:restriction base="xsd:dateTime"/>
+            </xsd:simpleType>
+          </xsd:union>
+        </xsd:simpleType>
+      </xsd:element>
+
+      <xsd:element name="tag" type="xsd:string" minOccurs="0" maxOccurs="32">
+        <xsd:annotation>
+          <xsd:documentation>
+            A tag associated with the video. Tags are generally very short
+            descriptions of key concepts associated with a video or piece of
+            content. A single video could have several tags, although it might
+            belong to only one category. For example, a video about grilling
+            food may belong in the Grilling category, but could be tagged
+            "steak", "meat", "summer", and "outdoor". Create a new
+            &lt;video:tag&gt; element for each tag associated with a video.
+          </xsd:documentation>
+        </xsd:annotation>
+      </xsd:element>
+
+      <xsd:element name="category" minOccurs="0">
+        <xsd:annotation>
+          <xsd:documentation>
+            The video's category - for example, cooking. In general, categories
+            are broad groupings of content by subject. For example, a site about
+            cooking could have categories for Broiling, Baking, and Grilling.
+          </xsd:documentation>
+        </xsd:annotation>
+        <xsd:simpleType>
+          <xsd:restriction base="xsd:string">
+            <xsd:maxLength value="256"/>
+          </xsd:restriction>
+        </xsd:simpleType>
+      </xsd:element>
+
+      <xsd:element name="family_friendly" minOccurs="0" type="tYesNo">
+        <xsd:annotation>
+          <xsd:documentation>
+            Whether the video is suitable for viewing by children. No if the
+            video should be available only to users with SafeSearch turned off.
+          </xsd:documentation>
+        </xsd:annotation>
+      </xsd:element>
+
+      <xsd:element name="restriction" minOccurs="0">
+        <xsd:annotation>
+          <xsd:documentation>
+            A list of countries where the video may or may not be played.
+            If there is no &lt;video:restriction&gt; tag, it is assumed that
+            the video can be played in all territories.
+          </xsd:documentation>
+        </xsd:annotation>
+        <xsd:complexType>
+          <xsd:simpleContent>
+            <xsd:extension base="tCountryList">
+              <xsd:attribute name="relationship" use="required">
+                <xsd:annotation>
+                  <xsd:documentation>
+                    Attribute "relationship" specifies whether the video is
+                    restricted or permitted for the specified countries.
+                  </xsd:documentation>
+                </xsd:annotation>
+                <xsd:simpleType>
+                  <xsd:restriction base="xsd:string">
+                    <xsd:enumeration value="allow"/>
+                    <xsd:enumeration value="deny"/>
+                  </xsd:restriction>
+                </xsd:simpleType>
+              </xsd:attribute>
+            </xsd:extension>
+          </xsd:simpleContent>
+        </xsd:complexType>
+      </xsd:element>
+
+      <xsd:element name="gallery_loc" minOccurs="0">
+        <xsd:annotation>
+          <xsd:documentation>
+            A link to the gallery (collection of videos) in which this video
+            appears.
+          </xsd:documentation>
+        </xsd:annotation>
+        <xsd:complexType>
+          <xsd:simpleContent>
+            <xsd:extension base="xsd:anyURI">
+              <xsd:attribute name="title" type="xsd:string">
+                <xsd:annotation>
+                  <xsd:documentation>
+                    The title of the gallery.
+                  </xsd:documentation>
+                </xsd:annotation>
+              </xsd:attribute>
+            </xsd:extension>
+          </xsd:simpleContent>
+        </xsd:complexType>
+      </xsd:element>
+
+      <xsd:element name="price" minOccurs="0" maxOccurs="unbounded">
+        <xsd:annotation>
+          <xsd:documentation>
+            The price to download or view the video. More than one
+            &lt;video:price&gt; element can be listed (for example, in order to
+            specify various currencies). The price value must either be a
+            non-negative decimal or be empty. If a price value is specified, the
+            currency attribute is required. If no price value is specified, the
+            type attribute must be valid and present. The resolution attribute
+            is optional.
+          </xsd:documentation>
+        </xsd:annotation>
+        <xsd:complexType>
+          <xsd:simpleContent>
+            <xsd:extension base="xsd:string">
+              <xsd:attribute name="currency">
+                <xsd:annotation>
+                  <xsd:documentation>
+                    The currency in ISO 4217 format. This attribute is required
+                    if a value is given for price.
+                  </xsd:documentation>
+                </xsd:annotation>
+                <xsd:simpleType>
+                  <xsd:restriction base="xsd:string">
+                    <xsd:pattern value="[A-Z]{3}"/>
+                  </xsd:restriction>
+                </xsd:simpleType>
+              </xsd:attribute>
+              <xsd:attribute name="type">
+                <xsd:annotation>
+                  <xsd:documentation>
+                    The type (purchase or rent) of price. This value is required
+                    if there is no value given for price.
+                  </xsd:documentation>
+                </xsd:annotation>
+                <xsd:simpleType>
+                  <xsd:restriction base="xsd:string">
+                    <xsd:enumeration value="purchase"/>
+                    <xsd:enumeration value="PURCHASE"/>
+                    <xsd:enumeration value="rent"/>
+                    <xsd:enumeration value="RENT"/>
+                  </xsd:restriction>
+                </xsd:simpleType>
+              </xsd:attribute>
+              <xsd:attribute name="resolution">
+                <xsd:annotation>
+                  <xsd:documentation>
+                    The resolution of the video at this price (SD or HD).
+                  </xsd:documentation>
+                </xsd:annotation>
+                <xsd:simpleType>
+                  <xsd:restriction base="xsd:string">
+                    <xsd:enumeration value="sd"/>
+                    <xsd:enumeration value="SD"/>
+                    <xsd:enumeration value="hd"/>
+                    <xsd:enumeration value="HD"/>
+                  </xsd:restriction>
+                </xsd:simpleType>
+              </xsd:attribute>
+            </xsd:extension>
+          </xsd:simpleContent>
+        </xsd:complexType>
+      </xsd:element>
+
+      <xsd:element name="requires_subscription" minOccurs="0" type="tYesNo">
+        <xsd:annotation>
+          <xsd:documentation>
+            Indicates whether a subscription (either paid or free) is required
+            to view the video.
+          </xsd:documentation>
+        </xsd:annotation>
+      </xsd:element>
+
+      <xsd:element name="uploader" minOccurs="0">
+        <xsd:annotation>
+          <xsd:documentation>
+            A name or handle of the video’s uploader.
+          </xsd:documentation>
+        </xsd:annotation>
+        <xsd:complexType>
+          <xsd:simpleContent>
+            <xsd:extension base="xsd:string">
+              <xsd:attribute name="info" type="xsd:anyURI">
+                <xsd:annotation>
+                  <xsd:documentation>
+                    The URL of a webpage with additional information about this
+                    uploader. This URL must be on the same domain as the
+                    &lt;loc&gt; tag.
+                  </xsd:documentation>
+                </xsd:annotation>
+              </xsd:attribute>
+            </xsd:extension>
+          </xsd:simpleContent>
+        </xsd:complexType>
+      </xsd:element>
+
+      <xsd:element name="tvshow" minOccurs="0">
+        <xsd:annotation>
+          <xsd:documentation>
+            Encloses all information about a single TV video.
+          </xsd:documentation>
+        </xsd:annotation>
+        <xsd:complexType>
+          <xsd:sequence>
+            <xsd:element name="show_title" type="xsd:string">
+              <xsd:annotation>
+                <xsd:documentation>
+                  The title of the TV show. This should be the same for all
+                  episodes from the same series.
+                </xsd:documentation>
+              </xsd:annotation>
+            </xsd:element>
+            <xsd:element name="video_type">
+              <xsd:annotation>
+                <xsd:documentation>
+                  Describes the relationship of the video to the specified
+                  TV show/episode.
+                </xsd:documentation>
+              </xsd:annotation>
+              <xsd:simpleType>
+                <xsd:restriction base="xsd:string">
+                  <!-- Complete episode -->
+                  <xsd:enumeration value="full"/>
+                  <!-- Episode promo -->
+                  <xsd:enumeration value="preview"/>
+                  <!-- Episode clip -->
+                  <xsd:enumeration value="clip"/>
+                  <!-- Interview -->
+                  <xsd:enumeration value="interview"/>
+                  <!-- News related to the content -->
+                  <xsd:enumeration value="news"/>
+                  <!-- If none of the above options accurately describe
+                       the relationship -->
+                  <xsd:enumeration value="other"/>
+                </xsd:restriction>
+              </xsd:simpleType>
+            </xsd:element>
+            <xsd:element name="episode_title" type="xsd:string" minOccurs="0">
+              <xsd:annotation>
+                <xsd:documentation>
+                  The title of the episode—for example, "Flesh and Bone" is the
+                  title of the Season 1, Episode 8 episode of Battlestar
+                  Galactica. This tag is not necessary if the video is not
+                  related to a specific episode (for example, if it's a trailer
+                  for an entire series or season).
+                </xsd:documentation>
+              </xsd:annotation>
+            </xsd:element>
+            <xsd:element name="season_number" minOccurs="0">
+              <xsd:annotation>
+                <xsd:documentation>
+                  Only for shows with a per-season schedule.
+                </xsd:documentation>
+              </xsd:annotation>
+              <xsd:simpleType>
+                <xsd:restriction base="xsd:integer">
+                  <xsd:minInclusive value="1"/>
+                </xsd:restriction>
+              </xsd:simpleType>
+            </xsd:element>
+            <xsd:element name="episode_number" minOccurs="0">
+              <xsd:annotation>
+                <xsd:documentation>
+                  The episode number in number format. For TV shoes with a
+                  per-season schedule, the first episode of each series should
+                  be numbered 1.
+                </xsd:documentation>
+              </xsd:annotation>
+              <xsd:simpleType>
+                <xsd:restriction base="xsd:integer">
+                  <xsd:minInclusive value="1"/>
+                </xsd:restriction>
+              </xsd:simpleType>
+            </xsd:element>
+            <xsd:element name="premier_date" minOccurs="0">
+              <xsd:annotation>
+                <xsd:documentation>
+                  The date the content of the video was first broadcast, in
+                  W3C format (for example, 2010-11-05.)
+                </xsd:documentation>
+              </xsd:annotation>
+              <xsd:simpleType>
+                <xsd:union>
+                  <xsd:simpleType>
+                    <xsd:restriction base="xsd:date"/>
+                  </xsd:simpleType>
+                  <xsd:simpleType>
+                    <xsd:restriction base="xsd:dateTime"/>
+                  </xsd:simpleType>
+                </xsd:union>
+              </xsd:simpleType>
+            </xsd:element>
+          </xsd:sequence>
+        </xsd:complexType>
+      </xsd:element>
+
+      <xsd:element name="platform" minOccurs="0">
+        <xsd:annotation>
+          <xsd:documentation>
+            A list of platforms where the video may or may not be played.
+            If there is no &lt;video:platform&gt; tag, it is assumed that
+            the video can be played on all platforms.
+          </xsd:documentation>
+        </xsd:annotation>
+        <xsd:complexType>
+          <xsd:simpleContent>
+            <xsd:extension base="tPlatformList">
+              <xsd:attribute name="relationship" use="required">
+                <xsd:annotation>
+                  <xsd:documentation>
+                    Attribute "relationship" specifies whether the video is
+                    restricted or permitted for the specified platforms.
+                  </xsd:documentation>
+                </xsd:annotation>
+                <xsd:simpleType>
+                  <xsd:restriction base="xsd:string">
+                    <xsd:enumeration value="allow"/>
+                    <xsd:enumeration value="deny"/>
+                  </xsd:restriction>
+                </xsd:simpleType>
+              </xsd:attribute>
+            </xsd:extension>
+          </xsd:simpleContent>
+        </xsd:complexType>
+      </xsd:element>
+
+      <xsd:element name="live" minOccurs="0" type="tYesNo">
+        <xsd:annotation>
+          <xsd:documentation>
+            Whether the video is a live internet broadcast.
+          </xsd:documentation>
+        </xsd:annotation>
+      </xsd:element>
+
+      <xsd:element name="id" minOccurs="0" maxOccurs="unbounded">
+        <xsd:annotation>
+          <xsd:documentation>
+            An unambiguous identifier for the video within a given
+            identification context.
+          </xsd:documentation>
+        </xsd:annotation>
+        <xsd:complexType>
+          <xsd:simpleContent>
+            <xsd:extension base="xsd:string">
+              <xsd:attribute name="type" use="required">
+                <xsd:annotation>
+                  <xsd:documentation>
+                    The identification context.
+                  </xsd:documentation>
+                </xsd:annotation>
+                <xsd:simpleType>
+                  <xsd:restriction base="xsd:string">
+                    <xsd:enumeration value="tms:series"/>
+                    <xsd:enumeration value="tms:program"/>
+                    <xsd:enumeration value="rovi:series"/>
+                    <xsd:enumeration value="rovi:program"/>
+                    <xsd:enumeration value="freebase"/>
+                    <xsd:enumeration value="url"/>
+                  </xsd:restriction>
+                </xsd:simpleType>
+              </xsd:attribute>
+            </xsd:extension>
+          </xsd:simpleContent>
+        </xsd:complexType>
+      </xsd:element>
+    </xsd:sequence>
+  </xsd:complexType>
+</xsd:element>
+
+</xsd:schema>