diff --git a/src/app/app.routing.ts b/src/app/app.routing.ts index 84a1b17..dd779da 100644 --- a/src/app/app.routing.ts +++ b/src/app/app.routing.ts @@ -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 }, @@ -16,7 +18,9 @@ export const routingComponents: any[] = [ DemoComponent, EmbedComponent, EpisodePickerComponent, - ShareModalComponent + ShareModalComponent, + AppLinksComponent, + AppIconComponent ]; export const routingProviders: any[] = []; diff --git a/src/app/embed/adapters/adapter.properties.ts b/src/app/embed/adapters/adapter.properties.ts index 7937e45..111428e 100644 --- a/src/app/embed/adapters/adapter.properties.ts +++ b/src/app/embed/adapters/adapter.properties.ts @@ -1,4 +1,6 @@ import { Observable } from 'rxjs/Observable'; +import { AppLinks } from './applinks'; +export { AppLinks, toAppLinks } from './applinks'; export const PropNames = [ 'audioUrl', @@ -9,7 +11,8 @@ export const PropNames = [ 'subscribeTarget', 'artworkUrl', 'feedArtworkUrl', - 'episodes' + 'episodes', + 'appLinks' ]; export interface AdapterProperties { @@ -22,6 +25,7 @@ export interface AdapterProperties { artworkUrl?: string; feedArtworkUrl?: string; episodes?: Array; + appLinks?: AppLinks; index?: number; } diff --git a/src/app/embed/adapters/applinks.ts b/src/app/embed/adapters/applinks.ts new file mode 100644 index 0000000..e6a4036 --- /dev/null +++ b/src/app/embed/adapters/applinks.ts @@ -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; +} diff --git a/src/app/embed/adapters/draper.adapter.spec.ts b/src/app/embed/adapters/draper.adapter.spec.ts index a813551..1305f18 100644 --- a/src/app/embed/adapters/draper.adapter.spec.ts +++ b/src/app/embed/adapters/draper.adapter.spec.ts @@ -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', () => { @@ -21,6 +22,7 @@ describe('DraperAdapter', () => { foo + agreatslug @@ -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({}); })); @@ -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'); @@ -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'); diff --git a/src/app/embed/adapters/draper.adapter.ts b/src/app/embed/adapters/draper.adapter.ts index 76acff9..cfdd4ff 100644 --- a/src/app/embed/adapters/draper.adapter.ts +++ b/src/app/embed/adapters/draper.adapter.ts @@ -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'; @@ -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; } @@ -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 { diff --git a/src/app/embed/adapters/feed.adapter.spec.ts b/src/app/embed/adapters/feed.adapter.spec.ts index 00b1892..a307414 100644 --- a/src/app/embed/adapters/feed.adapter.spec.ts +++ b/src/app/embed/adapters/feed.adapter.spec.ts @@ -19,6 +19,9 @@ describe('FeedAdapter', () => { + + guid-1 Title #1 @@ -43,25 +46,58 @@ describe('FeedAdapter', () => { `; + const TEST_FEED_MULTIPLE_SELF_LINKS = ` + + + The Channel Title + + + + + guid-1 + Title #1 + + 1:00 + + + + + `; + + const TEST_FEED_NO_SELF_LINKS = ` + + + The Channel Title + + + guid-1 + Title #1 + + 1:00 + + + + + `; + // 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({}); })); @@ -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'); @@ -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) => { diff --git a/src/app/embed/adapters/feed.adapter.ts b/src/app/embed/adapters/feed.adapter.ts index 886a986..c736308 100644 --- a/src/app/embed/adapters/feed.adapter.ts +++ b/src/app/embed/adapters/feed.adapter.ts @@ -6,7 +6,7 @@ import { EMBED_EPISODE_GUID_PARAM, EMBED_SHOW_PLAYLIST_PARAM } from './../embed.constants'; -import { AdapterProperties, DataAdapter } from './adapter.properties'; +import { AdapterProperties, DataAdapter, AppLinks, toAppLinks } from './adapter.properties'; import { sha1 } from './sha1'; const GUID_PREFIX = 's1!'; @@ -40,7 +40,7 @@ export class FeedAdapter implements DataAdapter { processFeed(feedUrl: string, episodeGuid?: string, numEpisodes?: number | string): Observable { return this.fetchFeed(feedUrl).map(body => { - const props = this.parseFeed(body, episodeGuid, numEpisodes); + const props = this.parseFeed(body, episodeGuid, numEpisodes, feedUrl); Object.keys(props).filter(k => props[k] === undefined).forEach(key => delete props[key]); return props; }).catch(err => { @@ -62,10 +62,15 @@ export class FeedAdapter implements DataAdapter { }); } - parseFeed(xml: string, episodeGuid?: string, numEpisodes?: number | string): AdapterProperties { + parseFeed(xml: string, episodeGuid?: string, numEpisodes?: number | string, requestedUrl?: string): AdapterProperties { const parser = new DOMParser(); const doc = parser.parseFromString(xml, 'application/xml') as XMLDocument; - let props = this.processDoc(doc); + + if (!doc.getElementsByTagName('rss').length) { + return {}; + } + + let props = this.processDoc(doc, {}, requestedUrl); if (numEpisodes) { const episodes = this.parseFeedEpisodes(doc, numEpisodes); @@ -113,11 +118,11 @@ export class FeedAdapter implements DataAdapter { }); } - processDoc(doc: XMLDocument, props: AdapterProperties = {}): AdapterProperties { + processDoc(doc: XMLDocument, props: AdapterProperties = {}, requestedUrl?: string): AdapterProperties { props.subtitle = this.getTagText(doc, 'title'); props.subscribeUrl = this.getTagAttributeNS(doc, ATOM_NAMESPACE, 'link', 'href'); // TODO: what if this isn't the first link? props.feedArtworkUrl = this.getTagAttributeNS(doc, ITUNES_NAMESPACE, 'image', 'href'); - + props.appLinks = this.getAppLinks(doc, requestedUrl); return props; } @@ -166,6 +171,20 @@ export class FeedAdapter implements DataAdapter { } } + protected getAppLinks(doc: XMLDocument, requestedUrl?: string): AppLinks { + const linkTags = Array.from(doc.getElementsByTagNameNS(ATOM_NAMESPACE, 'link')); + const selfLinks = linkTags.filter(el => el.getAttribute('rel') === 'self'); + const rss = this.getRssLink(selfLinks) || requestedUrl; + const urls = linkTags.filter(el => el.getAttribute('rel') === 'me').map(el => el.getAttribute('href')); + return {rss, ...toAppLinks(urls)}; + } + + protected getRssLink(links: Element[]): string | undefined { + if (!links || !links.length) { + return; + } + return (links.find(link => link.getAttribute('type') === 'application/rss+xml') || links[0]).getAttribute('href'); + } protected getTagTextNS(el: Element | XMLDocument, namespace: string, tag: string): string { const found = el.getElementsByTagNameNS(namespace, tag); diff --git a/src/app/embed/adapters/qsd.adapter.ts b/src/app/embed/adapters/qsd.adapter.ts index ff92036..14ef079 100644 --- a/src/app/embed/adapters/qsd.adapter.ts +++ b/src/app/embed/adapters/qsd.adapter.ts @@ -2,8 +2,8 @@ import { Injectable } from '@angular/core'; import { Observable } from 'rxjs/Observable'; import { EMBED_AUDIO_URL_PARAM, EMBED_TITLE_PARAM, EMBED_SUBTITLE_PARAM, EMBED_SUBSCRIBE_URL_PARAM, EMBED_SUBSCRIBE_TARGET, EMBED_IMAGE_URL_PARAM, - EMBED_EP_IMAGE_URL_PARAM } from '../embed.constants'; -import { AdapterProperties, DataAdapter } from './adapter.properties'; + EMBED_EP_IMAGE_URL_PARAM, EMBED_APP_LINKS_PARAM } from '../embed.constants'; +import { AdapterProperties, DataAdapter, toAppLinks } from './adapter.properties'; @Injectable() export class QSDAdapter implements DataAdapter { @@ -22,7 +22,8 @@ export class QSDAdapter implements DataAdapter { subscribeUrl: params[EMBED_SUBSCRIBE_URL_PARAM], subscribeTarget: params[EMBED_SUBSCRIBE_TARGET], feedArtworkUrl: params[EMBED_IMAGE_URL_PARAM], - artworkUrl: params[EMBED_EP_IMAGE_URL_PARAM] + artworkUrl: params[EMBED_EP_IMAGE_URL_PARAM], + appLinks: params[EMBED_APP_LINKS_PARAM] ? toAppLinks(params[EMBED_APP_LINKS_PARAM].split(',')) : undefined }; } diff --git a/src/app/embed/app-links/app-icon.component.ts b/src/app/embed/app-links/app-icon.component.ts new file mode 100644 index 0000000..19aa09f --- /dev/null +++ b/src/app/embed/app-links/app-icon.component.ts @@ -0,0 +1,41 @@ +import { Component, Input } from '@angular/core'; +import { AppLink } from './app-links.component'; + +@Component({ + selector: 'app-icon', + styles: [` + :host { + border-radius: 8px; + overflow: hidden; + display: inline-block; + width: 48px; + height: 48px; + margin-right: 15px; + position: relative; + box-shadow: 0 0 5px -3px rgba(0, 0, 0, 0.3); + } + + :host::after { + position: absolute; + top: 0; bottom: 0; left: 0; right: 0; + border-radius: 8px; + display: block; + content: ''; + box-shadow: inset 0 0 1px rgba(255, 255, 255, 0.3); + } + + img { + width: 100%; + } + `], + template: ` + + ` +}) +export class AppIconComponent { + @Input() appName: string; + + get imageUrl() { + return `/assets/images/app-icons/${this.appName}.png`; + } +} diff --git a/src/app/embed/app-links/app-links.component.css b/src/app/embed/app-links/app-links.component.css new file mode 100644 index 0000000..e69de29 diff --git a/src/app/embed/app-links/app-links.component.ts b/src/app/embed/app-links/app-links.component.ts new file mode 100644 index 0000000..cf20734 --- /dev/null +++ b/src/app/embed/app-links/app-links.component.ts @@ -0,0 +1,59 @@ +import { Component, Input, OnChanges, SimpleChanges } from '@angular/core'; +import { AppLinks } from '../adapters/applinks'; + +@Component({ + selector: 'app-links', + styleUrls: ['app-links.component.css'], + template: ` + + ` +}) +export class AppLinksComponent implements OnChanges { + @Input() appLinks: AppLinks; + sortedAppLinks: AppLink[]; + + ngOnChanges(changes: SimpleChanges): void { + if (changes.appLinks) { + this.sortedAppLinks = getSortedAppLinks(changes.appLinks.currentValue); + } + } + +} + + +export interface AppLink { + appName: keyof AppLinks; + url: string; +} + +const APP_PRIORITY_ORDER: (keyof AppLinks)[] = [ + 'apple', + 'spotify', + 'radiopublic', + 'stitcher', + 'overcast', + 'pocketcasts', + 'google', + 'iheartradio', + 'googleplay', + 'playerfm', + 'tunein', + 'soundcloud', + 'castbox', + 'breaker', + 'castro', + 'podbean', + 'anchor' +]; + +function getSortedAppLinks(appLinks?: AppLinks): AppLink[] { + const result = []; + if (appLinks) { + for (const appName of APP_PRIORITY_ORDER) { + if (appLinks[appName]) { + result.push({ appName, url: appLinks[appName] }); + } + } + } + return result; +} diff --git a/src/app/embed/embed.component.ts b/src/app/embed/embed.component.ts index e4be65e..5468fca 100644 --- a/src/app/embed/embed.component.ts +++ b/src/app/embed/embed.component.ts @@ -4,7 +4,7 @@ import { MergeAdapter } from './adapters/merge.adapter'; import { QSDAdapter } from './adapters/qsd.adapter'; import { DraperAdapter } from './adapters/draper.adapter'; import { FeedAdapter } from './adapters/feed.adapter'; -import { AdapterProperties } from './adapters/adapter.properties'; +import { AdapterProperties, AppLinks } from './adapters/adapter.properties'; import { PlayerComponent } from '../shared/player/player.component'; import { EMBED_SHOW_PLAYLIST_PARAM } from './embed.constants'; import { VisibilityService } from '../shared/visibility'; @@ -35,49 +35,62 @@ const IOS = 'iOSApp'; const BEACON = 'beacon'; const EVENT = 'event'; -const RequestAnimationFrame = window['requestAnimationFrame'] - || window['mozRequestAnimationFrame'] - || window['webkitRequestAnimationFrame'] - || window['msRequestAnimationFrame'] - || (cb => setTimeout(cb, 0)); +const RequestAnimationFrame = + window['requestAnimationFrame'] || + window['mozRequestAnimationFrame'] || + window['webkitRequestAnimationFrame'] || + window['msRequestAnimationFrame'] || + (cb => setTimeout(cb, 0)); @Component({ selector: 'play-embed', styleUrls: ['embed.component.css'], providers: [MergeAdapter, QSDAdapter, DraperAdapter, FeedAdapter], template: ` - - + +
-

Never miss an episode from {{this.subtitle}} - and other great podcasts when you download the free RadioPublic app.

+

+ Never miss an episode from {{ this.subtitle }} and + other great podcasts when you download the free RadioPublic app. +

-

You can also download the audio file if you're on a computer.

+

+ You can also + download the audio file + if you're on a computer. +

` }) - export class EmbedComponent implements OnInit { - showShareModal = false; hasInteracted = false; downloadRequested = false; @@ -96,6 +109,8 @@ export class EmbedComponent implements OnInit { modalDisplayReason?: string; + appLinks?: AppLinks; + // playlist showPlaylist: boolean; episodes: AdapterProperties[]; @@ -105,12 +120,17 @@ export class EmbedComponent implements OnInit { @ViewChild(PlayerComponent) private player: PlayerComponent; - constructor(private route: ActivatedRoute, private adapter: MergeAdapter, private visibility: VisibilityService) {} + constructor( + private route: ActivatedRoute, + private adapter: MergeAdapter, + private visibility: VisibilityService + ) {} ngOnInit() { this.route.queryParams.forEach(params => { this.pymId = params[PYM_CHILD_ID_PARAM]; - this.showPlaylist = typeof params[EMBED_SHOW_PLAYLIST_PARAM] !== 'undefined'; + this.showPlaylist = + typeof params[EMBED_SHOW_PLAYLIST_PARAM] !== 'undefined'; this.setEmbedHeight(); this.loadOnInFrame(params); }); @@ -167,23 +187,30 @@ export class EmbedComponent implements OnInit { } playStoreLink() { - return `https://play.radiopublic.com/${encodeURIComponent(this.subscribeUrl)}?getApp=1&platform=android`; + return `https://play.radiopublic.com/${encodeURIComponent( + this.subscribeUrl + )}?getApp=1&platform=android`; } appStoreLink() { - return `https://play.radiopublic.com/${encodeURIComponent(this.subscribeUrl)}?getApp=1&platform=ios`; + return `https://play.radiopublic.com/${encodeURIComponent( + this.subscribeUrl + )}?getApp=1&platform=ios`; } private assignEpisodePropertiesToPlayer(properties: AdapterProperties) { - this.audioUrl = ( properties.audioUrl || this.audioUrl ); - this.duration = ( properties.duration || this.duration || 0 ); - this.title = ( properties.title || this.title ); - this.subtitle = ( properties.subtitle || this.subtitle ); - this.subscribeUrl = ( properties.subscribeUrl || this.subscribeUrl ); - this.subscribeTarget = ( properties.subscribeTarget || this.subscribeTarget || '_blank'); - this.artworkUrl = ( properties.artworkUrl || this.artworkUrl ); - this.feedArtworkUrl = ( properties.feedArtworkUrl || this.feedArtworkUrl ); + this.audioUrl = properties.audioUrl || this.audioUrl; + this.duration = properties.duration || this.duration || 0; + this.title = properties.title || this.title; + this.subtitle = properties.subtitle || this.subtitle; + this.subscribeUrl = properties.subscribeUrl || this.subscribeUrl; + this.subscribeTarget = + properties.subscribeTarget || this.subscribeTarget || '_blank'; + this.artworkUrl = properties.artworkUrl || this.artworkUrl; + this.feedArtworkUrl = properties.feedArtworkUrl || this.feedArtworkUrl; this.episodes = properties.episodes || []; + console.log(properties); + this.appLinks = properties.appLinks; // fallback to feed image this.artworkUrl = this.artworkUrl || this.feedArtworkUrl; @@ -208,15 +235,19 @@ export class EmbedComponent implements OnInit { @HostListener('window:resize', []) setEmbedHeight() { if (window.parent && window.parent.postMessage) { - window.parent.postMessage(JSON.stringify({ - src: window.location.toString(), - context: 'iframe.resize', - height: 185 - }), '*'); + window.parent.postMessage( + JSON.stringify({ + src: window.location.toString(), + context: 'iframe.resize', + height: 185 + }), + '*' + ); if (this.pymId) { - window.parent.postMessage([ - 'pym', this.pymId, 'height', 185 - ].join(PYM_MESSAGE_DELIMITER), '*'); + window.parent.postMessage( + ['pym', this.pymId, 'height', 185].join(PYM_MESSAGE_DELIMITER), + '*' + ); } } } @@ -224,8 +255,11 @@ export class EmbedComponent implements OnInit { private logEvent(category: string, action: string, label: string) { if (window['ga']) { window['ga']('send', { - transport: BEACON, hitType: EVENT, - eventCategory: category, eventAction: action, eventLabel: label + transport: BEACON, + hitType: EVENT, + eventCategory: category, + eventAction: action, + eventLabel: label }); } } @@ -237,5 +271,4 @@ export class EmbedComponent implements OnInit { }); }); } - } diff --git a/src/app/embed/embed.constants.ts b/src/app/embed/embed.constants.ts index 87b31e8..c8fdfaf 100644 --- a/src/app/embed/embed.constants.ts +++ b/src/app/embed/embed.constants.ts @@ -15,3 +15,4 @@ export const EMBED_SUBSCRIBE_URL_PARAM = 'us'; export const EMBED_SUBSCRIBE_TARGET = 'gs'; export const EMBED_CTA_TARGET = 'gc'; export const EMBED_SHOW_PLAYLIST_PARAM = 'sp'; +export const EMBED_APP_LINKS_PARAM = 'la'; diff --git a/src/assets/images/app-icons/anchor.png b/src/assets/images/app-icons/anchor.png new file mode 100755 index 0000000..f6ce99f Binary files /dev/null and b/src/assets/images/app-icons/anchor.png differ diff --git a/src/assets/images/app-icons/apple.png b/src/assets/images/app-icons/apple.png new file mode 100755 index 0000000..8be4242 Binary files /dev/null and b/src/assets/images/app-icons/apple.png differ diff --git a/src/assets/images/app-icons/breaker.png b/src/assets/images/app-icons/breaker.png new file mode 100755 index 0000000..2b344dd Binary files /dev/null and b/src/assets/images/app-icons/breaker.png differ diff --git a/src/assets/images/app-icons/castbox.png b/src/assets/images/app-icons/castbox.png new file mode 100755 index 0000000..dd9de08 Binary files /dev/null and b/src/assets/images/app-icons/castbox.png differ diff --git a/src/assets/images/app-icons/castro.png b/src/assets/images/app-icons/castro.png new file mode 100755 index 0000000..5e4fbef Binary files /dev/null and b/src/assets/images/app-icons/castro.png differ diff --git a/src/assets/images/app-icons/google.png b/src/assets/images/app-icons/google.png new file mode 100755 index 0000000..3537705 Binary files /dev/null and b/src/assets/images/app-icons/google.png differ diff --git a/src/assets/images/app-icons/googleplay.png b/src/assets/images/app-icons/googleplay.png new file mode 100755 index 0000000..d8b26fe Binary files /dev/null and b/src/assets/images/app-icons/googleplay.png differ diff --git a/src/assets/images/app-icons/iheartradio.png b/src/assets/images/app-icons/iheartradio.png new file mode 100755 index 0000000..33ad267 Binary files /dev/null and b/src/assets/images/app-icons/iheartradio.png differ diff --git a/src/assets/images/app-icons/overcast.png b/src/assets/images/app-icons/overcast.png new file mode 100755 index 0000000..72227ee Binary files /dev/null and b/src/assets/images/app-icons/overcast.png differ diff --git a/src/assets/images/app-icons/playerfm.png b/src/assets/images/app-icons/playerfm.png new file mode 100755 index 0000000..53a44b9 Binary files /dev/null and b/src/assets/images/app-icons/playerfm.png differ diff --git a/src/assets/images/app-icons/pocketcasts.png b/src/assets/images/app-icons/pocketcasts.png new file mode 100755 index 0000000..0cdeded Binary files /dev/null and b/src/assets/images/app-icons/pocketcasts.png differ diff --git a/src/assets/images/app-icons/podbean.png b/src/assets/images/app-icons/podbean.png new file mode 100755 index 0000000..52df4db Binary files /dev/null and b/src/assets/images/app-icons/podbean.png differ diff --git a/src/assets/images/app-icons/radiopublic.png b/src/assets/images/app-icons/radiopublic.png new file mode 100644 index 0000000..d07a1bd Binary files /dev/null and b/src/assets/images/app-icons/radiopublic.png differ diff --git a/src/assets/images/app-icons/soundcloud.png b/src/assets/images/app-icons/soundcloud.png new file mode 100755 index 0000000..e93dec1 Binary files /dev/null and b/src/assets/images/app-icons/soundcloud.png differ diff --git a/src/assets/images/app-icons/spotify.png b/src/assets/images/app-icons/spotify.png new file mode 100755 index 0000000..bb82846 Binary files /dev/null and b/src/assets/images/app-icons/spotify.png differ diff --git a/src/assets/images/app-icons/stitcher.png b/src/assets/images/app-icons/stitcher.png new file mode 100755 index 0000000..b8bf1ef Binary files /dev/null and b/src/assets/images/app-icons/stitcher.png differ diff --git a/src/assets/images/app-icons/tunein.png b/src/assets/images/app-icons/tunein.png new file mode 100755 index 0000000..498c147 Binary files /dev/null and b/src/assets/images/app-icons/tunein.png differ