ソースを参照

Merge pull request #19 from magodo/itunes_namespace

RSS feed add element with "itunes:" prefix (for podcast)
master
Wito Chandra 7年前
committed by GitHub
コミット
cf3bb97ade
この署名に対応する既知のキーがデータベースに存在しません GPGキーID: 4AEE18F83AFDEB23
12個のファイルの変更415行の追加19行の削除
  1. +5
    -0
      lib/domain/rss_feed.dart
  2. +5
    -0
      lib/domain/rss_item.dart
  3. +83
    -0
      lib/domain/rss_item_itunes.dart
  4. +69
    -0
      lib/domain/rss_itunes.dart
  5. +24
    -0
      lib/domain/rss_itunes_category.dart
  6. +19
    -0
      lib/domain/rss_itunes_episode_type.dart
  7. +14
    -0
      lib/domain/rss_itunes_image.dart
  8. +18
    -0
      lib/domain/rss_itunes_owner.dart
  9. +17
    -0
      lib/domain/rss_itunes_type.dart
  10. +19
    -2
      lib/util/helpers.dart
  11. +86
    -17
      test/rss_test.dart
  12. +56
    -0
      test/xml/RSS-Itunes.xml

+ 5
- 0
lib/domain/rss_feed.dart ファイルの表示

@@ -8,6 +8,8 @@ import 'package:webfeed/domain/rss_item.dart';
import 'package:webfeed/util/helpers.dart'; import 'package:webfeed/util/helpers.dart';
import 'package:xml/xml.dart'; import 'package:xml/xml.dart';


import 'rss_itunes.dart';

class RssFeed { class RssFeed {
final String title; final String title;
final String author; final String author;
@@ -30,6 +32,7 @@ class RssFeed {
final String webMaster; final String webMaster;
final int ttl; final int ttl;
final DublinCore dc; final DublinCore dc;
final RssItunes itunes;


RssFeed({ RssFeed({
this.title, this.title,
@@ -52,6 +55,7 @@ class RssFeed {
this.webMaster, this.webMaster,
this.ttl, this.ttl,
this.dc, this.dc,
this.itunes,
}); });


factory RssFeed.parse(String xmlString) { factory RssFeed.parse(String xmlString) {
@@ -98,6 +102,7 @@ class RssFeed {
webMaster: findElementOrNull(channelElement, "webMaster")?.text, webMaster: findElementOrNull(channelElement, "webMaster")?.text,
ttl: int.tryParse(findElementOrNull(channelElement, "ttl")?.text ?? "0"), ttl: int.tryParse(findElementOrNull(channelElement, "ttl")?.text ?? "0"),
dc: DublinCore.parse(channelElement), dc: DublinCore.parse(channelElement),
itunes: RssItunes.parse(channelElement),
); );
} }
} }

+ 5
- 0
lib/domain/rss_item.dart ファイルの表示

@@ -7,6 +7,8 @@ import 'package:webfeed/domain/rss_source.dart';
import 'package:webfeed/util/helpers.dart'; import 'package:webfeed/util/helpers.dart';
import 'package:xml/xml.dart'; import 'package:xml/xml.dart';


import 'rss_item_itunes.dart';

class RssItem { class RssItem {
final String title; final String title;
final String description; final String description;
@@ -22,6 +24,7 @@ class RssItem {
final Media media; final Media media;
final RssEnclosure enclosure; final RssEnclosure enclosure;
final DublinCore dc; final DublinCore dc;
final RssItemItunes itunes;


RssItem({ RssItem({
this.title, this.title,
@@ -37,6 +40,7 @@ class RssItem {
this.media, this.media,
this.enclosure, this.enclosure,
this.dc, this.dc,
this.itunes,
}); });


factory RssItem.parse(XmlElement element) { factory RssItem.parse(XmlElement element) {
@@ -56,6 +60,7 @@ class RssItem {
media: Media.parse(element), media: Media.parse(element),
enclosure: RssEnclosure.parse(findElementOrNull(element, "enclosure")), enclosure: RssEnclosure.parse(findElementOrNull(element, "enclosure")),
dc: DublinCore.parse(element), dc: DublinCore.parse(element),
itunes: RssItemItunes.parse(element),
); );
} }
} }

+ 83
- 0
lib/domain/rss_item_itunes.dart ファイルの表示

@@ -0,0 +1,83 @@
import 'package:webfeed/util/helpers.dart';
import 'package:xml/xml.dart';

import 'rss_itunes_category.dart';
import 'rss_itunes_episode_type.dart';
import 'rss_itunes_image.dart';

class RssItemItunes {
final String title;
final int episode;
final int season;
final Duration duration;
final RssItunesEpisodeType episodeType;
final String author;
final String summary;
final bool explicit;
final String subtitle;
final List<String> keywords;
final RssItunesImage image;
final RssItunesCategory category;
final bool block;

RssItemItunes({
this.title,
this.episode,
this.season,
this.duration,
this.episodeType,
this.author,
this.summary,
this.explicit,
this.subtitle,
this.keywords,
this.image,
this.category,
this.block,
});

factory RssItemItunes.parse(XmlElement element) {
if (element == null) {
return null;
}
var episodeStr = findElementOrNull(element, "itunes:episode")?.text?.trim();
var seasonStr = findElementOrNull(element, "itunes:season")?.text?.trim();
var durationStr = findElementOrNull(element, "itunes:duration")?.text?.trim();

return RssItemItunes(
title: findElementOrNull(element, "itunes:title")?.text?.trim(),
episode: episodeStr == null ? null : int.parse(episodeStr),
season: seasonStr == null ? null : int.parse(seasonStr),
duration: durationStr == null ? null : parseDuration(durationStr),
episodeType: newRssItunesEpisodeType(findElementOrNull(element, "itunes:episodeType")),
author: findElementOrNull(element, "itunes:author")?.text?.trim(),
summary: findElementOrNull(element, "itunes:summary")?.text?.trim(),
explicit: parseBoolLiteral(element, "itunes:explicit"),
subtitle: findElementOrNull(element, "itunes:subtitle")?.text?.trim(),
keywords: findElementOrNull(element, "itunes:keywords")?.text?.split(",")?.map((keyword) => keyword.trim())?.toList(),
image: RssItunesImage.parse(findElementOrNull(element, "itunes:image")),
category: RssItunesCategory.parse(
findElementOrNull(element, "itunes:category")),
block: parseBoolLiteral(element, "itunes:block"),
);
}
}

Duration parseDuration(String s) {
var hours = 0;
var minutes = 0;
var seconds = 0;
var parts = s.split(':');
if (parts.length > 2) {
hours = int.parse(parts[parts.length - 3]);
}
if (parts.length > 1) {
minutes = int.parse(parts[parts.length - 2]);
}
seconds = int.parse(parts[parts.length - 1]);
return Duration(
hours: hours,
minutes: minutes,
seconds: seconds,
);
}

+ 69
- 0
lib/domain/rss_itunes.dart ファイルの表示

@@ -0,0 +1,69 @@
import 'package:xml/xml.dart';

import 'package:webfeed/util/helpers.dart';

import 'rss_itunes_category.dart';
import 'rss_itunes_image.dart';
import 'rss_itunes_owner.dart';
import 'rss_itunes_type.dart';

class RssItunes {
final String author;
final String summary;
final bool explicit;
final String title;
final String subtitle;
final RssItunesOwner owner;
final List<String> keywords;
final RssItunesImage image;
final List<RssItunesCategory> categories;
final RssItunesType type;
final String newFeedUrl;
final bool block;
final bool complete;

RssItunes({
this.author,
this.summary,
this.explicit,
this.title,
this.subtitle,
this.owner,
this.keywords,
this.image,
this.categories,
this.type,
this.newFeedUrl,
this.block,
this.complete,
});

factory RssItunes.parse(XmlElement element) {
if (element == null) {
return null;
}
return RssItunes(
author: findElementOrNull(element, "itunes:author")?.text?.trim(),
summary: findElementOrNull(element, "itunes:summary")?.text?.trim(),
explicit: parseBoolLiteral(element, "itunes:explicit"),
title: findElementOrNull(element, "itunes:title")?.text?.trim(),
subtitle: findElementOrNull(element, "itunes:subtitle")?.text?.trim(),
owner: RssItunesOwner.parse(findElementOrNull(element, "itunes:owner")),
keywords: findElementOrNull(element, "itunes:keywords")
?.text
?.split(",")
?.map((keyword) => keyword.trim())
?.toList(),
image: RssItunesImage.parse(findElementOrNull(element, "itunes:image")),
categories: findAllDirectElementsOrNull(element, "itunes:category")
.map((ele) => RssItunesCategory.parse(ele))
.toList(),
type: newRssItunesType(findElementOrNull(element, "itunes:type")),
newFeedUrl:
findElementOrNull(element, "itunes:new-feed-url")?.text?.trim(),
block: parseBoolLiteral(element, "itunes:block"),
complete: parseBoolLiteral(element, "itunes:complete"),
);
}
}


+ 24
- 0
lib/domain/rss_itunes_category.dart ファイルの表示

@@ -0,0 +1,24 @@
import 'package:xml/xml.dart';

class RssItunesCategory {
final String category;
final List<String> subCategories;

RssItunesCategory({this.category, this.subCategories});

factory RssItunesCategory.parse(XmlElement element) {
if (element == null) return null;

Iterable<XmlElement> subCategories;
try {
subCategories = element.findElements("itunes:category");
} on StateError {
subCategories = null;
}
return RssItunesCategory(
category: element.getAttribute("text")?.trim(),
subCategories:
subCategories?.map((ele) => ele.getAttribute("text")?.trim())?.toList(),
);
}
}

+ 19
- 0
lib/domain/rss_itunes_episode_type.dart ファイルの表示

@@ -0,0 +1,19 @@
import 'package:xml/xml.dart';

enum RssItunesEpisodeType {full, trailer, bonus}

RssItunesEpisodeType newRssItunesEpisodeType(XmlElement element) {
// "full" is default type
if (element == null) return RssItunesEpisodeType.full;

switch (element.text) {
case "full":
return RssItunesEpisodeType.full;
case "trailer":
return RssItunesEpisodeType.trailer;
case "bonus":
return RssItunesEpisodeType.bonus;
default:
return null;
}
}

+ 14
- 0
lib/domain/rss_itunes_image.dart ファイルの表示

@@ -0,0 +1,14 @@
import 'package:xml/xml.dart';

class RssItunesImage {
final String href;

RssItunesImage({this.href});

factory RssItunesImage.parse(XmlElement element) {
if (element == null) return null;
return RssItunesImage(
href: element.getAttribute("href")?.trim(),
);
}
}

+ 18
- 0
lib/domain/rss_itunes_owner.dart ファイルの表示

@@ -0,0 +1,18 @@
import 'package:xml/xml.dart';

import '../util/helpers.dart';

class RssItunesOwner {
final String name;
final String email;

RssItunesOwner({this.name, this.email});

factory RssItunesOwner.parse(XmlElement element) {
if (element == null) return null;
return RssItunesOwner(
name: findElementOrNull(element, "itunes:name")?.text?.trim(),
email: findElementOrNull(element, "itunes:email")?.text?.trim(),
);
}
}

+ 17
- 0
lib/domain/rss_itunes_type.dart ファイルの表示

@@ -0,0 +1,17 @@
import 'package:xml/xml.dart';

enum RssItunesType { episodic, serial }

RssItunesType newRssItunesType(XmlElement element) {
// "episodic" is default type
if (element == null) return RssItunesType.episodic;

switch (element.text) {
case "episodic":
return RssItunesType.episodic;
case "serial":
return RssItunesType.serial;
default:
return null;
}
}

+ 19
- 2
lib/util/helpers.dart ファイルの表示

@@ -2,10 +2,27 @@ import 'dart:core';


import 'package:xml/xml.dart'; import 'package:xml/xml.dart';


XmlElement findElementOrNull(XmlElement element, String name) {
XmlElement findElementOrNull(XmlElement element, String name,
{String namespace}) {
try { try {
return element.findAllElements(name).first;
return element.findAllElements(name, namespace: namespace).first;
} on StateError { } on StateError {
return null; return null;
} }
} }

List<XmlElement> findAllDirectElementsOrNull(XmlElement element, String name,
{String namespace}) {
try {
return element.findElements(name, namespace: namespace).toList();
} on StateError {
return null;
}
}

bool parseBoolLiteral(XmlElement element, String tagName) {
var v = findElementOrNull(element, tagName)?.text?.toLowerCase()?.trim();
if (v == null) return null;
return ["yes", "true"].contains(v);
}


+ 86
- 17
test/rss_test.dart ファイルの表示

@@ -4,6 +4,9 @@ import 'dart:io';
import 'package:test/test.dart'; import 'package:test/test.dart';
import 'package:webfeed/webfeed.dart'; import 'package:webfeed/webfeed.dart';


import 'package:webfeed/domain/rss_itunes_type.dart';
import 'package:webfeed/domain/rss_itunes_episode_type.dart';

void main() { void main() {
test("parse Invalid.xml", () { test("parse Invalid.xml", () {
var xmlString = new File("test/xml/Invalid.xml").readAsStringSync(); var xmlString = new File("test/xml/Invalid.xml").readAsStringSync();
@@ -19,7 +22,8 @@ void main() {
var feed = new RssFeed.parse(xmlString); var feed = new RssFeed.parse(xmlString);


expect(feed.title, "News - Foo bar News"); expect(feed.title, "News - Foo bar News");
expect(feed.description, "Foo bar News and Updates feed provided by Foo bar, Inc.");
expect(feed.description,
"Foo bar News and Updates feed provided by Foo bar, Inc.");
expect(feed.link, "https://foo.bar.news/"); expect(feed.link, "https://foo.bar.news/");
expect(feed.author, "hello@world.net"); expect(feed.author, "hello@world.net");
expect(feed.language, "en-US"); expect(feed.language, "en-US");
@@ -62,8 +66,10 @@ void main() {


expect(feed.items.length, 2); expect(feed.items.length, 2);


expect(feed.items.first.title, "The standard Lorem Ipsum passage, used since the 1500s");
expect(feed.items.first.description, "Lorem ipsum dolor sit amet, consectetur adipiscing elit");
expect(feed.items.first.title,
"The standard Lorem Ipsum passage, used since the 1500s");
expect(feed.items.first.description,
"Lorem ipsum dolor sit amet, consectetur adipiscing elit");
expect(feed.items.first.link, "https://foo.bar.news/1"); expect(feed.items.first.link, "https://foo.bar.news/1");
expect(feed.items.first.guid, "https://foo.bar.news/1?guid"); expect(feed.items.first.guid, "https://foo.bar.news/1?guid");
expect(feed.items.first.pubDate, "Mon, 26 Mar 2018 14:00:00 PDT"); expect(feed.items.first.pubDate, "Mon, 26 Mar 2018 14:00:00 PDT");
@@ -73,22 +79,26 @@ void main() {
expect(feed.items.first.source.url, "https://foo.bar.news/1?source"); expect(feed.items.first.source.url, "https://foo.bar.news/1?source");
expect(feed.items.first.source.value, "Foo Bar"); expect(feed.items.first.source.value, "Foo Bar");
expect(feed.items.first.comments, "https://foo.bar.news/1/comments"); expect(feed.items.first.comments, "https://foo.bar.news/1/comments");
expect(feed.items.first.enclosure.url, "http://www.scripting.com/mp3s/weatherReportSuite.mp3");
expect(feed.items.first.enclosure.url,
"http://www.scripting.com/mp3s/weatherReportSuite.mp3");
expect(feed.items.first.enclosure.length, 12216320); expect(feed.items.first.enclosure.length, 12216320);
expect(feed.items.first.enclosure.type, "audio/mpeg"); expect(feed.items.first.enclosure.type, "audio/mpeg");


expect(feed.items.first.content.value, "<img width=\"1000\" height=\"690\" src=\"https://test.com/image_link\"/> Test content<br />");
expect(feed.items.first.content.images.first, "https://test.com/image_link");
expect(feed.items.first.content.value,
"<img width=\"1000\" height=\"690\" src=\"https://test.com/image_link\"/> Test content<br />");
expect(
feed.items.first.content.images.first, "https://test.com/image_link");
}); });
test("parse RSS-Media.xml", (){
test("parse RSS-Media.xml", () {
var xmlString = new File("test/xml/RSS-Media.xml").readAsStringSync(); var xmlString = new File("test/xml/RSS-Media.xml").readAsStringSync();


var feed = new RssFeed.parse(xmlString); var feed = new RssFeed.parse(xmlString);
expect(feed.title, "Song Site"); expect(feed.title, "Song Site");
expect(feed.description, "Media RSS example with new fields added in v1.5.0");
expect(
feed.description, "Media RSS example with new fields added in v1.5.0");


expect(feed.items.length, 1); expect(feed.items.length, 1);
var item = feed.items.first; var item = feed.items.first;
expect(item.title, null); expect(item.title, null);
expect(item.link, "http://www.foo.com"); expect(item.link, "http://www.foo.com");
@@ -118,7 +128,8 @@ void main() {
expect(mediaCredit.scheme, "urn:yvs"); expect(mediaCredit.scheme, "urn:yvs");
expect(mediaCredit.value, "copyright holder of the entity"); expect(mediaCredit.value, "copyright holder of the entity");


expect(item.media.category.scheme, "http://search.yahoo.com/mrss/category_ schema");
expect(item.media.category.scheme,
"http://search.yahoo.com/mrss/category_ schema");
expect(item.media.category.label, "Music"); expect(item.media.category.label, "Music");
expect(item.media.category.value, "music/artist/album/song"); expect(item.media.category.value, "music/artist/album/song");


@@ -129,10 +140,11 @@ void main() {
expect(item.media.title.value, "The Judy's -- The Moo Song"); expect(item.media.title.value, "The Judy's -- The Moo Song");


expect(item.media.description.type, "plain"); expect(item.media.description.type, "plain");
expect(item.media.description.value, "This was some really bizarre band I listened to as a young lad.");
expect(item.media.description.value,
"This was some really bizarre band I listened to as a young lad.");

expect(item.media.keywords, "kitty, cat, big dog, yarn, fluffy"); expect(item.media.keywords, "kitty, cat, big dog, yarn, fluffy");
expect(item.media.thumbnails.length, 2); expect(item.media.thumbnails.length, 2);
var mediaThumbnail = item.media.thumbnails.first; var mediaThumbnail = item.media.thumbnails.first;
expect(mediaThumbnail.url, "http://www.foo.com/keyframe1.jpg"); expect(mediaThumbnail.url, "http://www.foo.com/keyframe1.jpg");
@@ -179,7 +191,8 @@ void main() {
expect(item.media.embed.height, 323); expect(item.media.embed.height, 323);
expect(item.media.embed.params.length, 5); expect(item.media.embed.params.length, 5);
expect(item.media.embed.params.first.name, "type"); expect(item.media.embed.params.first.name, "type");
expect(item.media.embed.params.first.value, "application/x-shockwave-flash");
expect(
item.media.embed.params.first.value, "application/x-shockwave-flash");


expect(item.media.responses.length, 2); expect(item.media.responses.length, 2);
expect(item.media.responses.first, "http://www.response1.com"); expect(item.media.responses.first, "http://www.response1.com");
@@ -195,7 +208,8 @@ void main() {
expect(item.media.prices.length, 2); expect(item.media.prices.length, 2);
expect(item.media.prices.first.price, 19.99); expect(item.media.prices.first.price, 19.99);
expect(item.media.prices.first.type, "rent"); expect(item.media.prices.first.type, "rent");
expect(item.media.prices.first.info, "http://www.dummy.jp/package_info.html");
expect(
item.media.prices.first.info, "http://www.dummy.jp/package_info.html");
expect(item.media.prices.first.currency, "EUR"); expect(item.media.prices.first.currency, "EUR");


expect(item.media.license.type, "text/html"); expect(item.media.license.type, "text/html");
@@ -214,7 +228,7 @@ void main() {
expect(item.media.scenes.first.startTime, "00:15"); expect(item.media.scenes.first.startTime, "00:15");
expect(item.media.scenes.first.endTime, "00:45"); expect(item.media.scenes.first.endTime, "00:45");
}); });
test("parse RSS-DC.xml", (){
test("parse RSS-DC.xml", () {
var xmlString = File("test/xml/RSS-DC.xml").readAsStringSync(); var xmlString = File("test/xml/RSS-DC.xml").readAsStringSync();


var feed = RssFeed.parse(xmlString); var feed = RssFeed.parse(xmlString);
@@ -256,7 +270,7 @@ void main() {
var xmlString = File("test/xml/RSS-Empty.xml").readAsStringSync(); var xmlString = File("test/xml/RSS-Empty.xml").readAsStringSync();


var feed = RssFeed.parse(xmlString); var feed = RssFeed.parse(xmlString);
expect(feed.title, null); expect(feed.title, null);
expect(feed.description, null); expect(feed.description, null);
expect(feed.link, null); expect(feed.link, null);
@@ -295,4 +309,59 @@ void main() {


expect(feed.items.first.content, null); expect(feed.items.first.content, null);
}); });

test("parse RSS-Itunes.xml", () {
var xmlString = File("test/xml/RSS-Itunes.xml").readAsStringSync();

var feed = RssFeed.parse(xmlString);

expect(feed.itunes.author, "Changelog Media");
expect(feed.itunes.summary, "Foo");
expect(feed.itunes.explicit, false);
expect(feed.itunes.image.href,
"https://cdn.changelog.com/uploads/covers/go-time-original.png?v=63725770357");
expect(feed.itunes.keywords,
"go,golang,open source,software,development".split(","));
expect(feed.itunes.owner.name, "Changelog Media");
expect(feed.itunes.owner.email, "editors@changelog.com");
expect(
Set.from([
feed.itunes.categories[0].category,
feed.itunes.categories[1].category
]),
["Technology", "Foo"]);
for (var category in feed.itunes.categories) {
switch (category.category) {
case "Foo":
expect(category.subCategories, ["Bar", "Baz"]);
break;
case "Technology":
expect(category.subCategories, ["Software How-To", "Tech News"]);
break;
}
}
expect(feed.itunes.title, "Go Time");
expect(feed.itunes.type, RssItunesType.serial);
expect(feed.itunes.newFeedUrl, "wubawuba");
expect(feed.itunes.block, true);
expect(feed.itunes.complete, true);

var item = feed.items[0];
expect(item.itunes.episodeType, RssItunesEpisodeType.full);
expect(item.itunes.episode, 1);
expect(item.itunes.season, 1);
expect(item.itunes.image.href,
"https://cdn.changelog.com/uploads/covers/go-time-original.png?v=63725770357");
expect(item.itunes.duration, Duration(minutes: 32, seconds: 30));
expect(item.itunes.explicit, false);
expect(item.itunes.keywords,
"go,golang,open source,software,development".split(","));
expect(item.itunes.subtitle, "with Erik, Carlisia, and Brian");
expect(item.itunes.summary, "Foo");
expect(item.itunes.author,
"Erik St. Martin, Carlisia Pinto, and Brian Ketelsen");
expect(item.itunes.explicit, false);
expect(item.itunes.title, "awesome title");
expect(item.itunes.block, false);
});
} }

+ 56
- 0
test/xml/RSS-Itunes.xml ファイルの表示

@@ -0,0 +1,56 @@
<rss xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd" version="2.0" nighteye="disabled">
<channel>
<title>Go Time</title>
<itunes:title>Go Time</itunes:title>
<itunes:type>serial</itunes:type>
<itunes:block>Yes</itunes:block>
<itunes:complete>TRUE</itunes:complete>
<itunes:new-feed-url>wubawuba</itunes:new-feed-url>
<copyright>All rights reserved</copyright>
<link>https://changelog.com/gotime</link>
<atom:link href="https://changelog.com/gotime/feed" rel="self" type="application/rss+xml"/>
<atom:link href="https://changelog.com/gotime/feed" rel="first" type="application/rss+xml"/>
<atom:link href="https://changelog.com/gotime/feed?page=1" rel="last" type="application/rss+xml"/>
<atom:link href="https://changelog.com/gotime" rel="alternate" type="text/html"/>
<language>en-us</language>
<description>
A diverse panel and special guests discuss cloud infrastructure, distributed systems, microservices, Kubernetes, Docker…oh and also Go! This show records LIVE every Thursday at 3pm US Eastern. A diverse panel and special guests discuss cloud infrastructure, distributed systems, microservices, Kubernetes, Docker…oh and also Go!
</description>
<itunes:author>Changelog Media</itunes:author>
<itunes:summary>Foo</itunes:summary>
<itunes:explicit>no</itunes:explicit>
<itunes:image href="https://cdn.changelog.com/uploads/covers/go-time-original.png?v=63725770357"/>
<itunes:keywords>go, golang, open source, software, development</itunes:keywords>
<itunes:owner>
<itunes:name>Changelog Media</itunes:name>
<itunes:email>editors@changelog.com</itunes:email>
</itunes:owner>
<itunes:category text="Technology">
<itunes:category text="Software How-To"/>
<itunes:category text="Tech News"/>
</itunes:category>
<itunes:category text="Foo">
<itunes:category text="Bar"/>
<itunes:category text="Baz"/>
</itunes:category>
<item>
<itunes:title>awesome title</itunes:title>
<itunes:episodeType>full</itunes:episodeType>
<itunes:episode>1</itunes:episode>
<itunes:season>1</itunes:season>
<itunes:block>xxx</itunes:block>
<itunes:image href="https://cdn.changelog.com/uploads/covers/go-time-original.png?v=63725770357"/>
<itunes:duration>32:30</itunes:duration>
<itunes:explicit>no</itunes:explicit>
<itunes:keywords>go, golang, open source, software, development</itunes:keywords>
<itunes:subtitle>with Erik, Carlisia, and Brian</itunes:subtitle>
<itunes:summary>Foo</itunes:summary>
<dc:creator>
Erik St. Martin, Carlisia Pinto, and Brian Ketelsen
</dc:creator>
<itunes:author>
Erik St. Martin, Carlisia Pinto, and Brian Ketelsen
</itunes:author>
</item>
</channel>
</rss>

読み込み中…
キャンセル
保存