@playpilot/tpi 6.10.7 → 6.11.1
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/README.md +21 -4
- package/dist/link-injections.js +9 -9
- package/package.json +1 -1
- package/src/lib/api/externalPages.ts +1 -1
- package/src/lib/data/translations.ts +25 -0
- package/src/lib/types/config.d.ts +5 -0
- package/src/lib/types/playlink.d.ts +1 -1
- package/src/lib/types/script.d.ts +2 -2
- package/src/main.ts +3 -3
- package/src/routes/components/Debugger.svelte +3 -20
- package/src/routes/components/Explore/ExploreCallToAction.svelte +0 -3
- package/src/routes/components/Playlinks/Playlink.svelte +1 -0
- package/src/routes/components/Playlinks/Playlinks.svelte +62 -20
- package/src/tests/routes/components/Playlinks/Playlinks.test.js +52 -1
package/package.json
CHANGED
|
@@ -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.
|
|
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.
|
|
@@ -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
|
-
//
|
|
24
|
-
|
|
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
|
-
|
|
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('
|
|
98
|
-
console.log(this.
|
|
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
|
|
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
|
-
|
|
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;
|
|
@@ -1,12 +1,9 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import IconArrow from '../Icons/IconArrow.svelte'
|
|
3
|
-
import ImageTitlesList from '$lib/images/titles-list.webp'
|
|
4
3
|
import { openModal } from '$lib/modal'
|
|
5
4
|
</script>
|
|
6
5
|
|
|
7
6
|
<button class="call-to-action" onclick={() => openModal({ type: 'explore' })}>
|
|
8
|
-
<img src={ImageTitlesList} alt="" width="70" height="57" />
|
|
9
|
-
|
|
10
7
|
<div>
|
|
11
8
|
<strong>Looking for something else?</strong>
|
|
12
9
|
<div>Use our streamingguide</div>
|
|
@@ -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
|
-
|
|
48
|
-
{#
|
|
49
|
-
<
|
|
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
|
-
|
|
52
|
-
{#
|
|
53
|
-
<Playlink playlink
|
|
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
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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',
|
|
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
|
})
|