Skip to content
This repository was archived by the owner on Oct 4, 2022. It is now read-only.
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion src/app/app.routing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import { Routes, RouterModule } from '@angular/router';
import { BuilderComponent, EpisodePickerComponent } from './builder';
import { EmbedComponent, ShareModalComponent } from './embed';
import { DemoComponent } from './demo';
import { AppLinksComponent } from './embed/app-links/app-links.component';
import { AppIconComponent } from './embed/app-links/app-icon.component';

export const routes: Routes = [
{ path: '', component: BuilderComponent },
Expand All @@ -16,7 +18,9 @@ export const routingComponents: any[] = [
DemoComponent,
EmbedComponent,
EpisodePickerComponent,
ShareModalComponent
ShareModalComponent,
AppLinksComponent,
AppIconComponent
];

export const routingProviders: any[] = [];
Expand Down
6 changes: 5 additions & 1 deletion src/app/embed/adapters/adapter.properties.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { Observable } from 'rxjs/Observable';
import { AppLinks } from './applinks';
export { AppLinks, toAppLinks } from './applinks';

export const PropNames = [
'audioUrl',
Expand All @@ -9,7 +11,8 @@ export const PropNames = [
'subscribeTarget',
'artworkUrl',
'feedArtworkUrl',
'episodes'
'episodes',
'appLinks'
];

export interface AdapterProperties {
Expand All @@ -22,6 +25,7 @@ export interface AdapterProperties {
artworkUrl?: string;
feedArtworkUrl?: string;
episodes?: Array<AdapterProperties>;
appLinks?: AppLinks;
index?: number;
}

Expand Down
55 changes: 55 additions & 0 deletions src/app/embed/adapters/applinks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
interface AppLinkMatchers {
apple: RegExp;
google: RegExp;
stitcher: RegExp;
iheartradio: RegExp;
podbean: RegExp;
tunein: RegExp;
soundcloud: RegExp;
anchor: RegExp;
breaker: RegExp;
spotify: RegExp;
overcast: RegExp;
castbox: RegExp;
googleplay: RegExp;
castro: RegExp;
pocketcasts: RegExp;
playerfm: RegExp;
radiopublic: RegExp;
}

const APP_MATCHERS: AppLinkMatchers = {
apple: /^https?:\/\/(?:itunes|podcasts)\.apple\.com\//i,
google: /^https:\/\/podcasts\.google\.com\//i,
stitcher: /^https?:\/\/(?:www\.)?stitcher\.com\//i,
iheartradio: /^https?:\/\/(?:www\.)?iheart\.com\//i,
podbean: /^https?:\/\/(?:www\.)?([a-z0-9]+)\.podbean\.com\//i,
tunein: /^https?:\/\/(?:www\.)?tunein\.com\//i,
soundcloud: /^https?:\/\/(?:www\.)?soundcloud\.com\//i,
anchor: /^https?:\/\/(?:www\.)?anchor\.fm\//i,
breaker: /^https?:\/\/(?:www\.)?breaker\.audio\//i,
spotify: /^https?:\/\/open\.spotify\.com\//i,
overcast: /^https?:\/\/(?:www\.)?overcast\.fm\//i,
castbox: /^https?:\/\/(?:www\.)?castbox\.fm\//i,
googleplay: /^https:\/\/play\.google\.com\//i,
castro: /^https?:\/\/(?:www\.)?castro\.fm\//i,
pocketcasts: /^https?:\/\/pca.st\//i,
playerfm: /^https?:\/\/(?:www\.)?player\.fm\//i,
radiopublic: /^https:\/\/(?:play\.)?radiopublic\.com\//i
};

export type AppLinks = { [A in keyof AppLinkMatchers]?: string } & { rss?: string };

export function toAppLinks(links: string[]): AppLinks | undefined {
const result: AppLinks = {};
for (const appKey of Object.keys(APP_MATCHERS)) {
const matchedLink = links.find(link => APP_MATCHERS[appKey].test(link));
if (matchedLink) {
result[appKey] = matchedLink;
}
}
if (Object.keys(result).some(key => result[key])) {
return result;
}
return;
}
31 changes: 19 additions & 12 deletions src/app/embed/adapters/draper.adapter.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { testService, injectHttp } from '../../../testing';
import { DraperAdapter } from './draper.adapter';
import { FeedAdapter } from './feed.adapter';
import { EMBED_FEED_ID_PARAM, EMBED_EPISODE_GUID_PARAM } from '../embed.constants';
import { AdapterProperties } from './adapter.properties';

describe('DraperAdapter', () => {

Expand All @@ -21,6 +22,7 @@ describe('DraperAdapter', () => {
<itunes:image href="http://channel/image.png"/>
<rp:image href="http://channel/rp/image.png"/>
<rp:program-id>foo</rp:program-id>
<rp:slug>agreatslug</rp:slug>
<atom10:link xmlns:atom10="http://www.w3.org/2005/Atom" rel="self" href="http://atom/self/link"/>
<atom10:link xmlns:atom10="http://www.w3.org/2005/Atom" rel="hub" href="http://pubsubhubbub.appspot.com/"/>
<item></item>
Expand All @@ -44,22 +46,21 @@ describe('DraperAdapter', () => {
`;

// helper to sync-get properties
const getProperties = (feed, feedId = null, guid = null): any => {
const params = {};
const getProperties = (feed: DraperAdapter, feedId?: string, guid?: string): AdapterProperties => {
const params = {
[EMBED_FEED_ID_PARAM]: feedId,
[EMBED_EPISODE_GUID_PARAM]: guid
};
const props = {};
if (feedId) { params[EMBED_FEED_ID_PARAM] = feedId; }
if (guid) { params[EMBED_EPISODE_GUID_PARAM] = guid; }
feed.getProperties(params).subscribe(result => {
Object.keys(result).forEach(k => props[k] = result[k]);
});
feed.getProperties(params).subscribe(result => Object.assign(props, result));
return props;
};

it('only runs when feedId is set', injectHttp((feed: DraperAdapter, mocker) => {
mocker(TEST_DRAPE);
expect(getProperties(feed, null, null)).toEqual({});
expect(getProperties(feed, 'http://some.where/feed.xml', null)).not.toEqual({});
expect(getProperties(feed, null, '1234')).toEqual({});
expect(getProperties(feed)).toEqual({});
expect(getProperties(feed, 'http://some.where/feed.xml')).not.toEqual({});
expect(getProperties(feed, undefined, '1234')).toEqual({});
expect(getProperties(feed, 'http://some.where/feed.xml', '1234')).not.toEqual({});
}));

Expand All @@ -69,12 +70,18 @@ describe('DraperAdapter', () => {
expect(props.audioUrl).toEqual('http://item1/original.mp3');
expect(props.title).toEqual('Title #1');
expect(props.subtitle).toEqual('The Channel Title');
expect(props.subscribeUrl).toEqual('https://play.radiopublic.com/foo/ep/s1!e661165c969fa6801bb8a7711daa73544b5149e9');
expect(props.subscribeUrl).toEqual('https://radiopublic.com/agreatslug/ep/s1!e661165c969fa6801bb8a7711daa73544b5149e9');
expect(props.subscribeTarget).toEqual('_top');
expect(props.artworkUrl).toEqual('http://item1/rp/image.png');
expect(props.feedArtworkUrl).toEqual('http://channel/rp/image.png');
}));

it('always includes RadioPublic in appLinks', injectHttp((feed: DraperAdapter, mocker) => {
mocker(TEST_DRAPE);
const props = getProperties(feed, 'http://some.where/feed.xml');
expect(props.appLinks.radiopublic).toEqual('https://radiopublic.com/agreatslug');
}));

it('falls back to the itunes:image if no rp:image', injectHttp((feed: DraperAdapter, mocker) => {
mocker(TEST_DRAPE);
const props = getProperties(feed, 'http://some.where/feed.xml', 'guid-2');
Expand All @@ -87,7 +94,7 @@ describe('DraperAdapter', () => {
expect(props.audioUrl).toBeUndefined();
expect(props.title).toBeUndefined();
expect(props.subtitle).toEqual('The Channel Title');
expect(props.subscribeUrl).toEqual('https://play.radiopublic.com/foo/ep/s1!f0ac9c9a4b7ad98f1663f828eb6b5587dfce3434');
expect(props.subscribeUrl).toEqual('https://radiopublic.com/agreatslug/ep/s1!f0ac9c9a4b7ad98f1663f828eb6b5587dfce3434');
expect(props.subscribeTarget).toEqual('_top');
expect(props.artworkUrl).toBeUndefined();
expect(props.feedArtworkUrl).toEqual('http://channel/rp/image.png');
Expand Down
14 changes: 11 additions & 3 deletions src/app/embed/adapters/draper.adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { Injectable } from '@angular/core';
import { Http } from '@angular/http';
import { Observable } from 'rxjs';
import { EMBED_FEED_ID_PARAM, EMBED_EPISODE_GUID_PARAM } from './../embed.constants';
import { AdapterProperties } from './adapter.properties';
import { AdapterProperties, AppLinks } from './adapter.properties';
import { FeedAdapter } from './feed.adapter';

const RADIOPUBLIC_NAMESPACE = 'https://www.w3id.org/rp/v1';
Expand Down Expand Up @@ -41,11 +41,19 @@ export class DraperAdapter extends FeedAdapter {
});
}

protected getAppLinks(doc: XMLDocument, requestedUrl?: string): AppLinks {
const appLinks = super.getAppLinks(doc) || {};
if (!appLinks.radiopublic) {
appLinks.radiopublic = `https://radiopublic.com/${this.getTagTextNS(doc, RADIOPUBLIC_NAMESPACE, 'slug')}`;
}
return appLinks;
}

processDoc(doc: XMLDocument, props: AdapterProperties = {}): AdapterProperties {
props = super.processDoc(doc, props);
props.feedArtworkUrl = this.getTagAttributeNS(doc, RADIOPUBLIC_NAMESPACE, 'image', 'href')
|| props.feedArtworkUrl;
props.subscribeUrl = `https://play.radiopublic.com/${this.getTagTextNS(doc, RADIOPUBLIC_NAMESPACE, 'program-id')}`;
props.subscribeUrl = `https://radiopublic.com/${this.getTagTextNS(doc, RADIOPUBLIC_NAMESPACE, 'slug')}`;
return props;
}

Expand All @@ -58,7 +66,7 @@ export class DraperAdapter extends FeedAdapter {
}

proxyUrl(feedId: string): string {
return `https://draper.radiopublic.com/transform?program_id=${feedId}`;
return `https://draper.radiopublic.com/transform?program_id=${feedId}&target=radiopublic/embed`;
}

protected getItemGuid(el: Element | XMLDocument): string {
Expand Down
96 changes: 78 additions & 18 deletions src/app/embed/adapters/feed.adapter.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ describe('FeedAdapter', () => {
<itunes:image href="http://channel/image.png"/>
<atom10:link xmlns:atom10="http://www.w3.org/2005/Atom" rel="self" href="http://atom/self/link"/>
<atom10:link xmlns:atom10="http://www.w3.org/2005/Atom" rel="hub" href="http://pubsubhubbub.appspot.com/"/>
<atom:link rel="me" href="https://podcasts.apple.com/us/podcast/the-adventure-zone/id947899573"/>
<atom:link rel="me"
href="https://podcasts.google.com/?feed=aHR0cDovL2ZlZWRzLjk5cGVyY2VudGludmlzaWJsZS5vcmcvOTlwZXJjZW50aW52aXNpYmxl" />
<item>
<guid isPermaLink="false">guid-1</guid>
<title>Title #1</title>
Expand All @@ -43,25 +46,58 @@ describe('FeedAdapter', () => {
</rss>
`;

const TEST_FEED_MULTIPLE_SELF_LINKS = `<?xml version="1.0" encoding="UTF-8" ?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd">
<channel>
<title>The Channel Title</title>
<itunes:image href="http://channel/image.png"/>
<atom:link rel="self" href="http://atom/self/link/html"/>
<atom:link rel="self" type="application/rss+xml" href="http://atom/self/link/xml"/>
<item>
<guid isPermaLink="false">guid-1</guid>
<title>Title #1</title>
<itunes:image href="http://item1/image.png"/>
<itunes:duration>1:00</itunes:duration>
<enclosure url="http://item1/enclosure.mp3"/>
</item>
</channel>
</rss>
`;

const TEST_FEED_NO_SELF_LINKS = `<?xml version="1.0" encoding="UTF-8" ?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd">
<channel>
<title>The Channel Title</title>
<itunes:image href="http://channel/image.png"/>
<item>
<guid isPermaLink="false">guid-1</guid>
<title>Title #1</title>
<itunes:image href="http://item1/image.png"/>
<itunes:duration>1:00</itunes:duration>
<enclosure url="http://item1/enclosure.mp3"/>
</item>
</channel>
</rss>
`;

// helper to sync-get properties
const getProperties = (feed, feedUrl = null, guid = null, numEps = null): any => {
const params = {};
const getProperties = (feed, feedUrl?, guid?, numEps?): any => {
const props = {};
if (feedUrl) { params[EMBED_FEED_URL_PARAM] = feedUrl; }
if (guid) { params[EMBED_EPISODE_GUID_PARAM] = guid; }
if (numEps) { params[EMBED_SHOW_PLAYLIST_PARAM] = numEps; }
feed.getProperties(params).subscribe(result => {
Object.keys(result).forEach(k => props[k] = result[k]);
});
const params = {
[EMBED_FEED_URL_PARAM]: feedUrl,
[EMBED_EPISODE_GUID_PARAM]: guid,
[EMBED_SHOW_PLAYLIST_PARAM]: numEps
};
feed.getProperties(params).subscribe(result => Object.assign(props, result));
return props;
};

it('only runs when feedUrl is set', injectHttp((feed: FeedAdapter, mocker) => {
mocker(TEST_FEED);
expect(getProperties(feed, null, null, null)).toEqual({});
expect(getProperties(feed, 'http://some.where/feed.xml', null, null)).not.toEqual({});
expect(getProperties(feed, null, '1234', null)).toEqual({});
expect(getProperties(feed, 'http://some.where/feed.xml', '1234', null)).not.toEqual({});
expect(getProperties(feed)).toEqual({});
expect(getProperties(feed, 'http://some.where/feed.xml')).not.toEqual({});
expect(getProperties(feed, undefined, '1234')).toEqual({});
expect(getProperties(feed, 'http://some.where/feed.xml', '1234')).not.toEqual({});
expect(getProperties(feed, 'http://some.where/feed.xml', '1234', 2)).not.toEqual({});
}));

Expand All @@ -78,22 +114,46 @@ describe('FeedAdapter', () => {
expect(props.episodes.length).toEqual(2);
}));

it ('parses app links', injectHttp((feed: FeedAdapter, mocker) => {
mocker(TEST_FEED);
const props = getProperties(feed, 'https://example.com/feed.xml');
expect(props.appLinks).toBeDefined();
expect(props.appLinks.apple).toEqual('https://podcasts.apple.com/us/podcast/the-adventure-zone/id947899573');
expect(props.appLinks.google)
.toEqual('https://podcasts.google.com/?feed=aHR0cDovL2ZlZWRzLjk5cGVyY2VudGludmlzaWJsZS5vcmcvOTlwZXJjZW50aW52aXNpYmxl');
expect(props.appLinks.rss).toEqual('http://atom/self/link');
}));

describe('atom self link parsing', () => {
it ('works when multiple self links are included', injectHttp((feed: FeedAdapter, mocker) => {
mocker(TEST_FEED_MULTIPLE_SELF_LINKS);
const props = getProperties(feed, 'https://example.com/feed.xml');
expect(props.appLinks.rss).toEqual('http://atom/self/link/xml');
}));

it ('falls back to the requested URL when no self links are included', injectHttp((feed: FeedAdapter, mocker) => {
mocker(TEST_FEED_NO_SELF_LINKS);
const props = getProperties(feed, 'https://example.com/feed.xml');
expect(props.appLinks.rss).toEqual('https://example.com/feed.xml');
}));
});

it('does not fallback to channel artwork at this level', injectHttp((feed: FeedAdapter, mocker) => {
mocker(TEST_FEED);
const props = getProperties(feed, 'http://some.where/feed.xml', 'guid-2', null);
const props = getProperties(feed, 'http://some.where/feed.xml', 'guid-2');
expect(props.artworkUrl).toBeUndefined();
expect(props.feedArtworkUrl).toEqual('http://channel/image.png');
}));

it('falls back to the enclosure for audioUrl', injectHttp((feed: FeedAdapter, mocker) => {
mocker(TEST_FEED);
const props = getProperties(feed, 'http://some.where/feed.xml', 'guid-2', null);
const props = getProperties(feed, 'http://some.where/feed.xml', 'guid-2');
expect(props.audioUrl).toEqual('http://item2/enclosure.mp3');
}));

it('can not find a guid', injectHttp((feed: FeedAdapter, mocker) => {
mocker(TEST_FEED);
const props = getProperties(feed, 'http://some.where/feed.xml', 'guid-not-found', null);
const props = getProperties(feed, 'http://some.where/feed.xml', 'guid-not-found');
expect(props.audioUrl).toBeUndefined();
expect(props.title).toBeUndefined();
expect(props.subtitle).toEqual('The Channel Title');
Expand All @@ -105,17 +165,17 @@ describe('FeedAdapter', () => {

it('can not find anything at all', injectHttp((feed: FeedAdapter, mocker) => {
mocker('');
expect(getProperties(feed, 'whatev', 'guid', null)).toEqual({});
expect(getProperties(feed, 'whatev', 'guid')).toEqual({});
}));

it('handles parser errors', injectHttp((feed: FeedAdapter, mocker) => {
mocker('{"some":"json"}');
expect(getProperties(feed, 'whatev', 'guid', null)).toEqual({});
expect(getProperties(feed, 'whatev', 'guid')).toEqual({});
}));

it('handles http errors', injectHttp((feed: FeedAdapter, mocker) => {
mocker('', 500);
expect(getProperties(feed, 'whatev', 'guid', null)).toEqual({});
expect(getProperties(feed, 'whatev', 'guid')).toEqual({});
}));

it('configures a proxy url', injectHttp((feed: FeedAdapter, mocker) => {
Expand Down
Loading