1
1
import BaseVideo from "./baseVideo" ;
2
+ import $ from "jquery" ;
2
3
3
4
// https://github.com/mikesteele/dual-captions/blob/b0ab92e4670100a27b76b2796995ad1be89f1672/site_integrations/netflix/index.js
4
5
// https://stackoverflow.com/questions/42105028/netflix-video-player-in-chrome-how-to-seek
5
6
6
7
export default class Netflix extends BaseVideo {
7
8
static sitePattern = / ^ ( h t t p s : \/ \/ ) ( w w w \. n e t f l i x \. c o m ) / ;
8
- static captionRequestPattern = / ^ ( h t t p s : \/ \/ ) .* ( n f l x v i d e o \. n e t ) / ;
9
+ static captionRequestPattern = / ^ ( h t t p s : \/ \/ ) .* ( n f l x v i d e o \. n e t ) \/ \? o = 1 / ;
9
10
static baseUrl = "https://www.netflix.com" ;
10
11
11
12
static playerSelector = "" ;
12
13
static playerApiSelector = "" ;
13
14
static captionContainerSelector = "" ;
14
15
static captionWindowSelector = "" ;
15
16
static captionBoxSelector = "" ;
16
- }
17
+ static sub = { } ;
18
+
19
+ static #injectScriptConstructor = ( ( ) => {
20
+ this . listenMessageFrameFromInject ( ) ;
21
+ } ) ( ) ;
22
+ static async listenPlayer ( ) { }
23
+ static async handleUrlChange ( url = window . location . href ) {
24
+ this . pausedByExtension = false ;
25
+ this . callMethodFromInject ( "activateCaption" , url ) ;
26
+ }
27
+ static async activateCaption ( url ) {
28
+ // skip if user caption off, is shorts skip
29
+ if ( ! this . isVideoUrl ( url ) ) {
30
+ return ;
31
+ }
32
+ await this . waitPlayerReady ( ) ; //wait player load
33
+ // get video lang
34
+ var lang = await this . guessVideoLang ( ) ;
35
+ this . killInterceptDebounce ( ) ; // end caption intercept
36
+ await this . interceptCaption ( ) ; // start caption intercept
37
+ await this . waitUntilForever ( ( ) => this . getVideoId ( ) ) ;
38
+
39
+ if ( this . checkPlayerCaptionOff ( ) ) {
40
+ console . log ( "caption is off" ) ;
41
+ } else {
42
+ console . log ( this . getPlayer ( ) . getTextTrack ( ) )
43
+ var videoId = this . getVideoId ( ) ;
44
+ this . requestTrack ( lang , videoId ) ; //turn on caption on specified lang
45
+ }
46
+ }
47
+ static async isVideoUrl ( url ) {
48
+ return url . includes ( `${ this . baseUrl } /watch` ) ;
49
+ }
50
+ static async waitPlayerReady ( ) {
51
+ await this . waitUntilForever ( ( ) => this . getPlayer ( ) ) ;
52
+ var player = this . getPlayer ( ) ;
53
+ await this . waitUntilForever ( ( ) => player . getAudioTrack ( ) ) ;
54
+ }
55
+ static setPlayerCaption ( lang ) {
56
+ this . getPlayer ( ) . setTimedTextTrack ( lang ) ;
57
+ }
58
+ static setPlayerCaptionOff ( ) {
59
+ var offTrack = this . getPlayer ( )
60
+ . getTimedTextTrackList ( )
61
+ . find ( ( track ) => track . trackId . includes ( "NONE" ) ) ;
62
+ this . getPlayer ( ) . setTimedTextTrack ( offTrack ) ;
63
+ }
64
+ static checkPlayerCaptionOff ( ) {
65
+ const textTrackList = this . getPlayer ( ) . getTimedTextTrackList ( ) ;
66
+ var currentTrack = this . getPlayer ( ) . getTextTrack ( ) ;
67
+ return ! currentTrack || currentTrack . trackId === textTrackList [ 0 ] ?. trackId ;
68
+ }
69
+
70
+ static getPlayer ( ) {
71
+ var videoPlayer =
72
+ window ?. netflix ?. appContext ?. state ?. playerApp ?. getAPI ( ) . videoPlayer ;
73
+ var playerSessionId = videoPlayer
74
+ ?. getAllPlayerSessionIds ( )
75
+ . find ( ( s ) => s . includes ( "watch" ) ) ;
76
+ var player = videoPlayer ?. getVideoPlayerBySessionId ( playerSessionId ) ;
77
+ return player ;
78
+ }
79
+ static getVideoId ( ) {
80
+ return String ( this . getPlayer ( ) . getMovieId ( ) ) ;
81
+ }
82
+ static async guessVideoLang ( ) {
83
+ return this . getPlayer ( ) ?. getAudioTrack ( ) ?. bcp47 ;
84
+ }
85
+ static async guessSubtitleLang ( url , subtitle ) {
86
+ return this . getPlayer ( ) ?. getTimedTextTrack ( ) ?. bcp47 ;
87
+ }
88
+
89
+ static async interceptCaption ( ) {
90
+ if ( this . interceptorLoaded ) {
91
+ return ;
92
+ }
93
+ this . interceptorLoaded = true ;
94
+ this . interceptor . apply ( ) ;
95
+ this . interceptor . on ( "request" , async ( { request, requestId } ) => {
96
+ try {
97
+ if ( this . captionRequestPattern . test ( request . url ) ) {
98
+ //get source lang sub
99
+ var response = await this . requestSubtitle ( request . url ) ;
100
+ var targetLang = this . setting [ "translateTarget" ] ;
101
+ var videoId = this . getVideoId ( ) ;
102
+ var xml = await response . text ( ) ;
103
+ var sub1 = this . parseSubtitle ( xml , videoId ) ;
104
+ var responseSub = sub1 ;
105
+
106
+ if ( sub1 . lang != targetLang ) {
107
+ var sub2 = await this . requestSubtitleWithReset ( targetLang ) ;
108
+ var mergedSub = this . mergeSubtitles ( sub1 , sub2 ) ;
109
+ responseSub = mergedSub ;
110
+ }
111
+ var xmlRes = this . encodeMergedSubtitles ( responseSub ) ;
112
+ request . respondWith ( new Response ( xmlRes ) ) ;
113
+ }
114
+ } catch ( error ) {
115
+ console . log ( error ) ;
116
+ }
117
+ } ) ;
118
+ }
119
+
120
+ static async requestSubtitle ( subUrl , lang , tlang , videoId ) {
121
+ var res = await fetch ( subUrl ) ;
122
+ return res ;
123
+ }
124
+ static async requestSubtitleWithReset ( lang , videoId ) {
125
+ var player = this . getPlayer ( ) ;
126
+ var prevSub = player . getTimedTextTrack ( ) ;
127
+ var sub = await this . requestTrack ( lang , videoId ) ;
128
+ player . setTextTrack ( prevSub ) ;
129
+ return sub ;
130
+ }
131
+ static async requestTrack ( lang , videoId ) {
132
+ var videoId = videoId || this . getVideoId ( ) ;
133
+ if ( this . sub ?. [ videoId ] ?. [ lang ] ) {
134
+ return this . sub ?. [ videoId ] ?. [ lang ] ;
135
+ }
136
+ var player = this . getPlayer ( ) ;
137
+ var subList = player . getTimedTextTrackList ( ) ;
138
+ const selectedTimedTextTrack = subList
139
+ . sort ( ( a , b ) => ( a . trackType === "ASSISTIVE" ? - 1 : 1 ) )
140
+ . filter ( ( textTrack ) => textTrack . isImageBased === false )
141
+ . find ( ( textTrack ) => textTrack . bcp47 === lang ) ;
142
+
143
+ if ( ! selectedTimedTextTrack ) {
144
+ return null ;
145
+ }
146
+
147
+ player . setTextTrack ( selectedTimedTextTrack ) ;
148
+ await this . waitUntilForever ( ( ) => {
149
+ return this . sub ?. [ videoId ] ?. [ lang ] ;
150
+ } ) ;
151
+ return this . sub ?. [ videoId ] ?. [ lang ] ;
152
+ }
153
+ static extractVideoId ( url ) {
154
+ const match = url . match ( / \/ w a t c h \/ ( \d + ) / ) ;
155
+ return match ? match [ 1 ] : null ;
156
+ }
157
+
158
+ static parseSubtitle ( sub , videoId ) {
159
+ const concatSubtitles = [ ] ;
160
+ const parser = new DOMParser ( ) ;
161
+ const xmlDoc = parser . parseFromString ( sub , "text/xml" ) ;
162
+ const subtitles = Array . from ( xmlDoc . getElementsByTagName ( "p" ) ) . map ( ( p ) =>
163
+ p . cloneNode ( true )
164
+ ) ;
165
+ var lang = xmlDoc . documentElement . getAttribute ( "xml:lang" ) ;
166
+ // remove div tag
167
+ const div = xmlDoc . querySelector ( "div" ) ;
168
+ div ?. parentNode ?. removeChild ( div ) ;
169
+
170
+ // Extract all regions and update their IDs to start with the language code
171
+ const layout = xmlDoc . getElementsByTagName ( "layout" ) [ 0 ] ;
172
+ if ( layout ) {
173
+ var regions = Array . from ( layout . getElementsByTagName ( "region" ) ) ;
174
+ regions . forEach ( ( region , index ) => {
175
+ const newId = `region${ index } _${ lang } ` ;
176
+ region . setAttribute ( "xml:id" , newId ) ;
177
+ } ) ;
178
+ }
179
+
180
+ // parse subtitles
181
+ for ( let i = 0 ; i < subtitles . length ; i ++ ) {
182
+ const subtitle = subtitles [ i ] ;
183
+ const start = parseInt ( subtitle . getAttribute ( "begin" ) . replace ( "t" , "" ) ) ;
184
+ const end = parseInt ( subtitle . getAttribute ( "end" ) . replace ( "t" , "" ) ) ;
185
+ const text = subtitle . textContent ;
186
+ const region = subtitle . getAttribute ( "region" ) + "_" + lang ;
187
+ var prev = concatSubtitles ?. [ concatSubtitles . length - 1 ] ;
188
+ if ( prev && prev . start === start && prev . end === end ) {
189
+ prev . text += " " + text ;
190
+ } else {
191
+ concatSubtitles . push ( { start, end, text, region } ) ;
192
+ }
193
+ }
194
+
195
+ var parsedSubMeta = {
196
+ xmlDoc,
197
+ lang,
198
+ subtitles : concatSubtitles ,
199
+ regions,
200
+ } ;
201
+ if ( ! this . sub [ videoId ] ) {
202
+ this . sub [ videoId ] = { } ;
203
+ }
204
+ this . sub [ videoId ] [ lang ] = parsedSubMeta ;
205
+ return parsedSubMeta ;
206
+ }
207
+
208
+ static mergeSubtitles ( sub1Meta , sub2Meta ) {
209
+ var mergedSub = [ ] ;
210
+ var sub1 = sub1Meta . subtitles ;
211
+ var sub2 = sub2Meta . subtitles ;
212
+ var mergedSubMeta = { ...sub1Meta } ;
213
+
214
+ const layout = mergedSubMeta ?. xmlDoc ?. getElementsByTagName ( "layout" ) ?. [ 0 ] ;
215
+
216
+ sub2Meta ?. regions ?. forEach ( ( region ) => {
217
+ layout ?. appendChild ( region ) ;
218
+ } ) ;
219
+
220
+ // fix mismatch length between sub1 sub2
221
+ for ( let [ i , sub1Line ] of sub1 . entries ( ) ) {
222
+ var line1 = sub1Line ;
223
+ var line2 = "" ;
224
+ // get most overlapped sub
225
+ sub2 . forEach ( ( line ) => {
226
+ line . overlap = Math . max (
227
+ sub1Line . end - line . start ,
228
+ line . end - sub1Line . start
229
+ ) ;
230
+ } ) ;
231
+ sub2 . sort ( ( a , b ) => a . overlap - b . overlap ) ;
232
+
233
+ // if sub2 has no overlap, use sub1
234
+ mergedSub . push ( line1 ) ;
235
+ if ( sub2 . length && 0 < sub2 [ 0 ] . overlap ) {
236
+ line2 = sub2 [ 0 ] ;
237
+ let line1Copy = { ...line1 } ;
238
+ line1Copy . text = line2 . text ;
239
+ mergedSub . push ( line1Copy ) ;
240
+ }
241
+ }
242
+
243
+ mergedSubMeta . subtitles = mergedSub ;
244
+ return mergedSubMeta ;
245
+ }
246
+
247
+ static encodeMergedSubtitles ( subMeta ) {
248
+ var xmlDoc = subMeta . xmlDoc ;
249
+ var subtitles = subMeta . subtitles ;
250
+ const body = xmlDoc . getElementsByTagName ( "body" ) [ 0 ] ;
251
+ let div = body . getElementsByTagName ( "div" ) [ 0 ] ;
252
+ if ( ! div ) {
253
+ div = xmlDoc . createElement ( "div" ) ;
254
+ body . appendChild ( div ) ;
255
+ }
256
+
257
+ subtitles . forEach ( ( sub ) => {
258
+ const p = xmlDoc . createElement ( "p" ) ;
259
+ p . setAttribute ( "xml:id" , `subtitle${ subtitles . indexOf ( sub ) + 1 } ` ) ;
260
+ p . setAttribute ( "begin" , `${ sub . start } t` ) ;
261
+ p . setAttribute ( "end" , `${ sub . end } t` ) ;
262
+ p . setAttribute ( "region" , sub . region ) ;
263
+
264
+ const span = xmlDoc . createElement ( "span" ) ;
265
+ span . setAttribute ( "style" , "style0" ) ;
266
+ span . textContent = sub . text ;
267
+
268
+ p . appendChild ( span ) ;
269
+ div . appendChild ( p ) ;
270
+ } ) ;
271
+
272
+ const serializer = new XMLSerializer ( ) ;
273
+ return serializer . serializeToString ( xmlDoc ) ;
274
+ }
275
+
276
+ static encodeSubtitle ( subtitles ) {
277
+ return subtitles ;
278
+ }
279
+ }
0 commit comments