@playpilot/tpi 6.10.7 → 6.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@playpilot/tpi",
3
- "version": "6.10.7",
3
+ "version": "6.11.0",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "dev": "vite dev",
@@ -50,7 +50,7 @@ export async function fetchLinkInjections(
50
50
  }
51
51
 
52
52
  // This is used when debugging (using window.PlayPilotLinkInjections.debug())
53
- window.PlayPilotLinkInjections.successful_fetches?.push(response)
53
+ window.PlayPilotLinkInjections.last_successful_fetch = response
54
54
 
55
55
  return response
56
56
  }
@@ -101,6 +101,31 @@ export const translations = {
101
101
  [Language.Swedish]: 'Titta nu',
102
102
  [Language.Danish]: 'Se nu',
103
103
  },
104
+ 'Category: AVOD': {
105
+ [Language.English]: 'Stream for free',
106
+ [Language.Swedish]: 'Streama gratis',
107
+ [Language.Danish]: 'Stream gratis',
108
+ },
109
+ 'Category: SVOD': {
110
+ [Language.English]: 'Streaming services',
111
+ [Language.Swedish]: 'Streamingtjänster',
112
+ [Language.Danish]: 'Streamingtjenester',
113
+ },
114
+ 'Category: RENT': {
115
+ [Language.English]: 'Rent it',
116
+ [Language.Swedish]: 'Hyr den',
117
+ [Language.Danish]: 'Lej den',
118
+ },
119
+ 'Category: BUY': {
120
+ [Language.English]: 'Buy it',
121
+ [Language.Swedish]: 'Köp den',
122
+ [Language.Danish]: 'Køb den',
123
+ },
124
+ 'Category: Other': {
125
+ [Language.English]: 'Other',
126
+ [Language.Swedish]: 'Annat',
127
+ [Language.Danish]: 'Andet',
128
+ },
104
129
  'Share': {
105
130
  [Language.English]: 'Share',
106
131
  [Language.Swedish]: 'Dela',
@@ -59,6 +59,11 @@ export type ConfigResponse = {
59
59
  */
60
60
  disable_scroll_reveal_posters?: boolean
61
61
 
62
+ /**
63
+ * Playlinks are merged by default, when this option is enabled playlinks will instead be shown categorized.
64
+ */
65
+ categorize_playlinks?: boolean
66
+
62
67
  /**
63
68
  * The following options are all relevant for in text disclaimers, which renders as a disclaimer text within the article,
64
69
  * rather than only inside of title cards.
@@ -14,4 +14,4 @@ export type PlaylinkData = {
14
14
  }
15
15
  }
16
16
 
17
- export type PlaylinkCategory = 'SVOD' | 'BUY' | 'RENT' | 'TVOD'
17
+ export type PlaylinkCategory = 'SVOD' | 'BUY' | 'RENT' | 'TVOD' | 'AVOD'
@@ -20,8 +20,8 @@ export type ScriptConfig = {
20
20
  after_article_insert_position?: InsertPosition | ''
21
21
  // The language of the page will be inferred from the html `lang` attribute. Can be set manually using this option in the config object.
22
22
  language?: string | null
23
- // Lists all fetches to external pages
24
- successful_fetches?: LinkInjectionResponse[]
23
+ // Set to the last success external-pages fetch.
24
+ last_successful_fetch?: LinkInjectionResponse | null
25
25
  // Lists all tracked events through the `track()` function.
26
26
  tracked_events?: { event: string, payload: Record<string, any> }[]
27
27
  // Queued tracking events that were fired before consent was given. These might be fired later if the user consents.
package/src/main.ts CHANGED
@@ -15,7 +15,7 @@ window.PlayPilotLinkInjections = {
15
15
  region: null,
16
16
  organization_sid: null,
17
17
  domain_sid: null,
18
- successful_fetches: [],
18
+ last_successful_fetch: null,
19
19
  tracked_events: [],
20
20
  queued_tracking_events: [],
21
21
  split_test_identifiers: {},
@@ -94,8 +94,8 @@ window.PlayPilotLinkInjections = {
94
94
  console.log('Valid elements', elements)
95
95
  console.groupEnd()
96
96
 
97
- console.groupCollapsed('Successful fetches')
98
- console.log(this.successful_fetches)
97
+ console.groupCollapsed('Last fetch')
98
+ console.log(this.last_successful_fetch)
99
99
  console.groupEnd()
100
100
 
101
101
  console.groupCollapsed('Meta')
@@ -1,6 +1,5 @@
1
1
  <script lang="ts">
2
2
  import { SplitTest } from '$lib/enums/SplitTest'
3
- import { getPageMetaData } from '$lib/meta'
4
3
  import { getFullUrlPath } from '$lib/url'
5
4
  import { onDestroy } from 'svelte'
6
5
 
@@ -13,7 +12,6 @@
13
12
  const secrets = ['tpidebug', 'debugtpi']
14
13
  const lastInputs: string[] = []
15
14
  const isUsingBetaScript = !!document.querySelector('script[src*="scripts.playpilot.com/link-injection@next"]')
16
- const metadata = getPageMetaData()
17
15
 
18
16
  let data = $state(dataToReadable())
19
17
  let shown = $state(false)
@@ -26,7 +24,7 @@
26
24
  function dataToReadable(): Record<string, undefined | Record<string, any>[]> {
27
25
  const data = window.PlayPilotLinkInjections
28
26
 
29
- const successfulInjections = data.evaluated_link_injections?.filter(injection => !injection.failed) || []
27
+ const succesfulInjections = data.evaluated_link_injections?.filter(injection => !injection.failed) || []
30
28
  const failedInjections = data.evaluated_link_injections?.filter(injection => injection.failed) || []
31
29
 
32
30
  const visiblePixels = Array.from(document.querySelectorAll<HTMLImageElement>('[data-playpilot-pixel]'))
@@ -38,8 +36,7 @@
38
36
  { label: 'HTML selector', data: data.selector },
39
37
  ],
40
38
  'API Config': Object.entries(window.PlayPilotLinkInjections.config || {}).map(([label, data]) => ({ label, data })),
41
- 'External pages responses': window.PlayPilotLinkInjections.successful_fetches?.map((item, index) => ({ label: index + 1, data: item })),
42
- [`Successful injections (${successfulInjections.length})`]: successfulInjections.map(injection => ({ label: injection.title, data: injection.sentence })),
39
+ [`Succesful injections (${succesfulInjections.length})`]: succesfulInjections.map(injection => ({ label: injection.title, data: injection.sentence })),
43
40
  [`Failed injections (${failedInjections.length})`]: failedInjections.map(injection => ({ label: injection.title, data: `Reason: ${injection.failed_message} | Sentence: ${injection.sentence}` })),
44
41
  [`Fetched ads (${data.ads?.length || 0})`]: data.ads?.map(ad => ({ label: ad.campaign_name, data: ad })),
45
42
  [`Tracking events (${data.tracked_events?.length || 0})`]: data.tracked_events?.map(event => ({ label: event.event, data: event.payload })),
@@ -124,14 +121,6 @@
124
121
 
125
122
  <hr />
126
123
 
127
- <div class="meta">
128
- {metadata.content_heading}<br>
129
- modified_time: {metadata.content_modified_time}<br>
130
- published_time: {metadata.content_published_time}
131
- </div>
132
-
133
- <hr />
134
-
135
124
  {#if !!(window.PlayPilotLinkInjections.config?.exclude_urls_pattern && getFullUrlPath().match(window.PlayPilotLinkInjections?.config?.exclude_urls_pattern))}
136
125
  <span class="error">The current URL is excluded via the config</span>
137
126
  <hr />
@@ -188,7 +177,6 @@
188
177
  }
189
178
 
190
179
  hr {
191
- margin: margin(0.5) 0;
192
180
  border-color: theme(primary);
193
181
  }
194
182
 
@@ -218,16 +206,11 @@
218
206
  border: 1px solid theme(primary);
219
207
  background: black;
220
208
  overflow: auto;
209
+ color: white;
221
210
  font-family: "Consolas", monospace;
222
- font-size: theme(font-size-base);
223
- line-height: 1.5em;
224
211
  color: theme(primary);
225
212
  }
226
213
 
227
- .meta {
228
- font-size: theme(font-size-small);
229
- }
230
-
231
214
  .item {
232
215
  white-space: nowrap;
233
216
  overflow-x: auto;
@@ -23,6 +23,7 @@
23
23
  const usePixel = $derived(isPixelAllowed() && !highlighted)
24
24
 
25
25
  const categoryStrings = {
26
+ AVOD: t('Stream'),
26
27
  SVOD: t('Stream'),
27
28
  BUY: t('Buy'),
28
29
  RENT: t('Rent'),
@@ -3,13 +3,17 @@
3
3
  import { t } from '$lib/localization'
4
4
  import { mergePlaylinks } from '$lib/playlink'
5
5
  import { track } from '$lib/tracking'
6
- import type { PlaylinkData } from '$lib/types/playlink'
6
+ import type { PlaylinkCategory, PlaylinkData } from '$lib/types/playlink'
7
7
  import type { TitleData } from '$lib/types/title'
8
8
  import { heading } from '$lib/actions/heading'
9
9
  import { campaignToPlaylink, getFirstAdOfType } from '$lib/api/ads'
10
10
  import { getContext } from 'svelte'
11
11
  import Playlink from './Playlink.svelte'
12
12
 
13
+ type Category = PlaylinkCategory | '' | 'Other'
14
+ type CategorizedPlaylinks = Partial<Record<Category, PlaylinkData[]>>
15
+ type SortedPlaylinks = [Category, PlaylinkData[]][]
16
+
13
17
  interface Props {
14
18
  playlinks: PlaylinkData[]
15
19
  title: TitleData
@@ -19,10 +23,13 @@
19
23
 
20
24
  const isModal = getContext('scope') === 'modal'
21
25
  const displayAd = getFirstAdOfType('card')
26
+ const categorize = !!window.PlayPilotLinkInjections?.config?.categorize_playlinks
22
27
 
23
28
  let outerWidth = $state(0)
24
29
 
25
30
  const mergedPlaylinks = $derived(mergePlaylinks(playlinks))
31
+ const categorizedPlaylinks = $derived(categorizePlaylinks(playlinks))
32
+ const shownPlaylinks: SortedPlaylinks = $derived(categorize ? sortCategories(categorizedPlaylinks) : [['' as Category, mergedPlaylinks]])
26
33
 
27
34
  // Grid turns into a list when the playlinks container is small enough
28
35
  // It is also a list by default if a display ad is present, as that would
@@ -32,6 +39,37 @@
32
39
  function onclick(playlink: string): void {
33
40
  track(isModal ? TrackingEvent.TitleModalPlaylinkClick : TrackingEvent.TitlePopoverPlaylinkClick, title, { playlink })
34
41
  }
42
+
43
+ function categorizePlaylinks(playlinks: PlaylinkData[]): CategorizedPlaylinks {
44
+ const categories: CategorizedPlaylinks = {}
45
+
46
+ for (const playlink of playlinks) {
47
+ const { category } = playlink.extra_info
48
+
49
+ if (!(category in categories)) categories[category] = []
50
+ categories[category]!.push(playlink)
51
+ }
52
+
53
+ return categories
54
+ }
55
+
56
+ function sortCategories(categorizedPlaylinks: CategorizedPlaylinks): SortedPlaylinks {
57
+ const order: Partial<PlaylinkCategory>[] = ['AVOD', 'SVOD', 'RENT', 'BUY']
58
+ const sortedCategories: SortedPlaylinks = []
59
+
60
+ for (const category of order) {
61
+ if (categorizedPlaylinks[category]) sortedCategories.push([category, categorizedPlaylinks[category]])
62
+ }
63
+
64
+ const restPlaylinks: PlaylinkData[] = []
65
+ for (const playlinks of Object.values(categorizedPlaylinks)) {
66
+ for (const playlink of playlinks) {
67
+ if (!order.includes(playlink.extra_info.category)) restPlaylinks.push(playlink)
68
+ }
69
+ }
70
+
71
+ return [...sortedCategories, ['Other', restPlaylinks]]
72
+ }
35
73
  </script>
36
74
 
37
75
 
@@ -42,24 +80,28 @@
42
80
  {window?.PlayPilotLinkInjections?.config?.playlinks_disclaimer_text || t('Commission Disclaimer')}
43
81
  <a href="https://playpilot.com/" target="_blank" rel="sponsored">PlayPilot.com</a>
44
82
  </div>
83
+ {:else}
84
+ <div class="empty" data-testid="playlinks-empty">
85
+ {t('Title Unavailable')}
86
+ </div>
45
87
  {/if}
46
88
 
47
- <div class="playlinks" class:list bind:clientWidth={outerWidth}>
48
- {#each mergedPlaylinks as playlink, index}
49
- <Playlink {playlink} onclick={() => onclick(playlink.name)} />
89
+ {#each shownPlaylinks as [category, playlinks]}
90
+ {#if category && playlinks.length}
91
+ <div class="heading category" use:heading={4}>{t(`Category: ${category}`)}</div>
92
+ {/if}
50
93
 
51
- <!-- A fake highlighted playlink as part of the display ad, to be shown after the first playlink -->
52
- {#if displayAd && (index === 0)}
53
- <Playlink playlink={campaignToPlaylink(displayAd)} onclick={() => track(TrackingEvent.DisplayedAdPlaylickClick, title, { campaign_name: displayAd.campaign_name })} hideCategory disclaimer={displayAd.disclaimer || ''} />
54
- {/if}
55
- {/each}
94
+ <div class="playlinks" data-testid="category-{category}" class:list bind:clientWidth={outerWidth}>
95
+ {#each playlinks as playlink, index}
96
+ <Playlink {playlink} onclick={() => onclick(playlink.name)} />
56
97
 
57
- {#if !mergedPlaylinks.length}
58
- <div class="empty" data-testid="playlinks-empty">
59
- {t('Title Unavailable')}
60
- </div>
61
- {/if}
62
- </div>
98
+ <!-- A fake highlighted playlink as part of the display ad, to be shown after the first playlink -->
99
+ {#if displayAd && (index === 0)}
100
+ <Playlink playlink={campaignToPlaylink(displayAd)} onclick={() => track(TrackingEvent.DisplayedAdPlaylickClick, title, { campaign_name: displayAd.campaign_name })} hideCategory disclaimer={displayAd.disclaimer || ''} />
101
+ {/if}
102
+ {/each}
103
+ </div>
104
+ {/each}
63
105
 
64
106
  <style lang="scss">
65
107
  .heading {
@@ -71,6 +113,11 @@
71
113
  line-height: normal;
72
114
  }
73
115
 
116
+ .category {
117
+ margin: margin(0.5) 0 margin(0.25);
118
+ font-size: theme(playlinks-category-font-size, font-size-small);
119
+ }
120
+
74
121
  .playlinks {
75
122
  box-sizing: border-box;
76
123
  display: grid;
@@ -105,7 +152,6 @@
105
152
  }
106
153
 
107
154
  .empty {
108
- grid-column: span 2;
109
155
  padding: margin(0.75);
110
156
  margin-top: margin(0.5);
111
157
  background: theme(playlink-background, lighter);
@@ -113,9 +159,5 @@
113
159
  border-radius: theme(playlink-border-radius, border-radius);
114
160
  white-space: initial;
115
161
  line-height: 1.35;
116
-
117
- .list & {
118
- grid-column: 1;
119
- }
120
162
  }
121
163
  </style>
@@ -112,7 +112,7 @@ describe('Playlinks.svelte', () => {
112
112
  expect(getAllByText('Some playlink')).toHaveLength(1)
113
113
  })
114
114
 
115
- it('Should render as list when only one playlink is present', async () => {
115
+ it('Should render as list when only one playlink is present', () => {
116
116
  const playlinks = [
117
117
  { name: 'Some playlink', logo_url: 'logo', extra_info: { category: 'SVOD' } },
118
118
  ]
@@ -122,4 +122,55 @@ describe('Playlinks.svelte', () => {
122
122
  // @ts-ignore
123
123
  expect(container.querySelector('.playlinks').classList).toContain('list')
124
124
  })
125
+
126
+ it('Should categorize and sort playlinks if config option is given', () => {
127
+ // @ts-ignore
128
+ window.PlayPilotLinkInjections = {
129
+ config: { categorize_playlinks: true },
130
+ }
131
+
132
+ const playlinks = [
133
+ { name: 'Some svod playlink', logo_url: 'logo', extra_info: { category: 'SVOD' } },
134
+ { name: 'Some rent playlink', extra_info: { category: 'RENT' } },
135
+ { name: 'Some other rent playlink', logo_url: '', extra_info: { category: 'RENT' } },
136
+ { name: 'Some free playlink', logo_url: null, extra_info: { category: 'AVOD' } },
137
+ ]
138
+
139
+ // @ts-ignore
140
+ const { container, getByText } = render(Playlinks, { playlinks, title })
141
+
142
+ const elements = /** @type {HTMLElement[]} */ (Array.from(container.querySelectorAll('[data-playlink]')))
143
+
144
+ expect(elements[0].innerText).toContain(playlinks[3].name)
145
+ expect(elements[1].innerText).toContain(playlinks[0].name)
146
+ expect(elements[2].innerText).toContain(playlinks[1].name)
147
+ expect(elements[3].innerText).toContain(playlinks[2].name)
148
+
149
+ expect(getByText('Streaming services')).toBeTruthy()
150
+ expect(getByText('Rent it')).toBeTruthy()
151
+ expect(getByText('Stream for free')).toBeTruthy()
152
+ })
153
+
154
+ it('Should list uncategorized playlinks under "other" category', () => {
155
+ // @ts-ignore
156
+ window.PlayPilotLinkInjections = {
157
+ config: { categorize_playlinks: true },
158
+ }
159
+
160
+ const playlinks = [
161
+ { name: 'Some udef playlink', logo_url: 'logo', extra_info: { category: 'UDEF' } },
162
+ { name: 'Some empty playlink', extra_info: { category: '' } },
163
+ { name: 'Some svod playlink', logo_url: '', extra_info: { category: 'SVOD' } },
164
+ { name: 'Some tvod playlink', logo_url: null, extra_info: { category: 'TVOD' } },
165
+ ]
166
+
167
+ // @ts-ignore
168
+ const { getByTestId, getByText } = render(Playlinks, { playlinks, title })
169
+
170
+ expect(getByText('Other')).toBeTruthy()
171
+ expect(getByTestId('category-Other').querySelectorAll('[data-playlink]')).toHaveLength(3)
172
+
173
+ expect(getByText('Streaming services'))
174
+ expect(getByTestId('category-SVOD').querySelectorAll('[data-playlink]')).toHaveLength(1)
175
+ })
125
176
  })