@@ -5,7 +5,7 @@ import axios from 'axios';
5
5
import moment from 'moment' ;
6
6
import jwt_decode from 'jwt-decode' ;
7
7
8
- import { okHttpUserAgent } from './user-agent' ;
8
+ import { okHttpUserAgent , userAgent } from './user-agent' ;
9
9
import { configPath } from './config' ;
10
10
import { useNfl } from './networks' ;
11
11
import { ClassTypeWithoutMethods , IEntry , IHeaders , IProvider } from './shared-interfaces' ;
@@ -42,6 +42,7 @@ interface INFLEvent {
42
42
linear : boolean ;
43
43
networks : string [ ] ;
44
44
broadcastAiringType ?: string ;
45
+ hostNetwork ?: string ;
45
46
}
46
47
47
48
const CLIENT_KEY = [
@@ -118,6 +119,39 @@ const CLIENT_SECRET = ['q', 'G', 'h', 'E', 'v', '1', 'R', 't', 'I', '2', 'S', 'f
118
119
119
120
const TV_CLIENT_SECRET = [ 'u' , 'o' , 'C' , 'y' , 'y' , 'k' , 'y' , 'U' , 'w' , 'D' , 'b' , 'f' , 'Q' , 'Z' , 'r' , '2' ] . join ( '' ) ;
120
121
122
+ const TWITCH_CLIENT_ID = [
123
+ 'k' ,
124
+ 'i' ,
125
+ 'm' ,
126
+ 'n' ,
127
+ 'e' ,
128
+ '7' ,
129
+ '8' ,
130
+ 'k' ,
131
+ 'x' ,
132
+ '3' ,
133
+ 'n' ,
134
+ 'c' ,
135
+ 'x' ,
136
+ '6' ,
137
+ 'b' ,
138
+ 'r' ,
139
+ 'g' ,
140
+ 'o' ,
141
+ '4' ,
142
+ 'm' ,
143
+ 'v' ,
144
+ '6' ,
145
+ 'w' ,
146
+ 'k' ,
147
+ 'i' ,
148
+ '5' ,
149
+ 'h' ,
150
+ '1' ,
151
+ 'k' ,
152
+ 'o' ,
153
+ ] . join ( '' ) ;
154
+
121
155
const DEVICE_INFO = {
122
156
capabilities : { } ,
123
157
ctvDevice : 'AndroidTV' ,
@@ -160,14 +194,27 @@ const DEFAULT_CATEGORIES = ['NFL', 'NFL+', 'Football'];
160
194
161
195
const nflConfigPath = path . join ( configPath , 'nfl_tokens.json' ) ;
162
196
163
- export type TOtherAuth = 'prime' | 'tve' | 'peacock' | 'sunday_ticket' ;
197
+ export type TOtherAuth = 'prime' | 'tve' | 'peacock' | 'sunday_ticket' | 'twitch' ;
164
198
165
199
interface INFLJwt {
166
200
dmaCode : string ;
167
201
plans : { plan : string ; status : string } [ ] ;
168
202
networks ?: { [ key : string ] : string } ;
169
203
}
170
204
205
+ interface ITwitchAccessTokenRes {
206
+ data : ITwitchAccessToken ;
207
+ }
208
+
209
+ interface ITwitchAccessToken {
210
+ streamPlaybackAccessToken : ITwitchStreamToken ;
211
+ }
212
+
213
+ interface ITwitchStreamToken {
214
+ value : string ;
215
+ signature : string ;
216
+ }
217
+
171
218
const parseAirings = async ( events : INFLEvent [ ] ) => {
172
219
const now = moment ( ) ;
173
220
const endDate = moment ( ) . add ( 2 , 'days' ) . endOf ( 'day' ) ;
@@ -244,6 +291,7 @@ class NflHandler {
244
291
public peacockUUID ?: string ;
245
292
public youTubeUserId ?: string ;
246
293
public youTubeUUID ?: string ;
294
+ public twitchDeviceId ?: string ;
247
295
248
296
public initialize = async ( ) => {
249
297
const setup = ( await db . providers . count ( { name : 'nfl' } ) ) > 0 ? true : false ;
@@ -343,9 +391,7 @@ class NflHandler {
343
391
return ;
344
392
}
345
393
346
- if ( ! this . expires_at || moment ( this . expires_at * 1000 ) . isBefore ( moment ( ) ) ) {
347
- await this . extendTokens ( ) ;
348
- }
394
+ await this . extendTokens ( ) ;
349
395
} ;
350
396
351
397
public getSchedule = async ( ) : Promise < void > => {
@@ -398,10 +444,24 @@ class NflHandler {
398
444
this . checkTVEEventAccess ( i ) ||
399
445
// Peacock
400
446
( i . authorizations . peacock && this . checkPeacockAccess ( ) ) ||
401
- // Prime
402
- ( i . authorizations . amazon_prime && this . checkPrimeAccess ( ) )
447
+ // Prime || Twitch.tv
448
+ ( i . authorizations . amazon_prime && ( this . checkPrimeAccess ( ) || this . checkTwitchAccess ( ) ) )
403
449
) {
404
- events . push ( i ) ;
450
+ if ( i . authorizations . amazon_prime ) {
451
+ if ( this . checkTwitchAccess ( ) ) {
452
+ events . push ( {
453
+ ...i ,
454
+ externalId : `${ i . externalId } -twitch` ,
455
+ networks : [ 'Twitch' ] ,
456
+ } ) ;
457
+ }
458
+
459
+ if ( this . checkPrimeAccess ( ) ) {
460
+ events . push ( i ) ;
461
+ }
462
+ } else {
463
+ events . push ( i ) ;
464
+ }
405
465
}
406
466
} else if (
407
467
i . callSign === 'NFLNRZ' &&
@@ -417,7 +477,7 @@ class NflHandler {
417
477
i . contentType === 'GAME' &&
418
478
i . language . find ( l => l === 'en' ) &&
419
479
i . authorizations . sunday_ticket &&
420
- this . checkSundayTicket ( )
480
+ this . checkSundayTicketAccess ( )
421
481
) {
422
482
events . push ( i ) ;
423
483
}
@@ -460,31 +520,97 @@ class NflHandler {
460
520
const isGame =
461
521
event . channel !== 'NFLNETWORK' && event . channel !== 'NFLDIGITAL1_OO_v3' && event . channel !== 'NFLNRZ' ;
462
522
463
- const url = [ 'https://' , 'api.nfl.com/' , 'play/v1/asset/' , id ] . join ( '' ) ;
523
+ const isTwitch = event . feed === 'Twitch' ;
464
524
465
- const { data} = await axios . post (
466
- url ,
525
+ if ( ! isTwitch ) {
526
+ const url = [ 'https://' , 'api.nfl.com/' , 'play/v1/asset/' , id ] . join ( '' ) ;
527
+
528
+ const { data} = await axios . post (
529
+ url ,
530
+ {
531
+ ...( this . checkTVEAccess ( ) && {
532
+ idp : this . mvpdIdp ,
533
+ mvpdUUID : this . mvpdUUID ,
534
+ mvpdUserId : this . mvpdUserId ,
535
+ networks : event . feed || 'NFLN' ,
536
+ } ) ,
537
+ } ,
538
+ {
539
+ headers : {
540
+ 'Content-Type' : 'application/json' ,
541
+ 'User-Agent' : okHttpUserAgent ,
542
+ authorization : `Bearer ${ isGame ? this . access_token : this . tv_access_token } ` ,
543
+ } ,
544
+ } ,
545
+ ) ;
546
+
547
+ return [ data . accessUrl , { } ] ;
548
+ } else {
549
+ try {
550
+ const channel = event . name . indexOf ( 'Vision' ) > - 1 ? 'primevision' : 'primevideo' ;
551
+
552
+ const accessToken = await this . getTwitchAccessToken ( channel ) ;
553
+
554
+ const url = [
555
+ 'https://usher.ttvnw.net' ,
556
+ '/api/channel/hls/' ,
557
+ `${ channel } .m3u8` ,
558
+ '?client_id=' ,
559
+ TWITCH_CLIENT_ID ,
560
+ '&token=' ,
561
+ accessToken . value ,
562
+ '&sig=' ,
563
+ accessToken . signature ,
564
+ '&allow_source=true' ,
565
+ '&allow_audio_only=false' ,
566
+ ] . join ( '' ) ;
567
+
568
+ return [ url , { } ] ;
569
+ } catch ( e ) {
570
+ console . error ( e ) ;
571
+ console . log ( 'Could not start playback from Twitch' ) ;
572
+ }
573
+ }
574
+ } catch ( e ) {
575
+ console . error ( e ) ;
576
+ console . log ( 'Could not start playback' ) ;
577
+ }
578
+ } ;
579
+
580
+ private getTwitchAccessToken = async ( channel : string ) : Promise < ITwitchStreamToken > => {
581
+ try {
582
+ const { data} = await axios . post < ITwitchAccessTokenRes > (
583
+ 'https://gql.twitch.tv/gql' ,
467
584
{
468
- ...( this . checkTVEAccess ( ) && {
469
- idp : this . mvpdIdp ,
470
- mvpdUUID : this . mvpdUUID ,
471
- mvpdUserId : this . mvpdUserId ,
472
- networks : event . feed || 'NFLN' ,
473
- } ) ,
585
+ extensions : {
586
+ persistedQuery : {
587
+ sha256Hash : 'ed230aa1e33e07eebb8928504583da78a5173989fadfb1ac94be06a04f3cdbe9' ,
588
+ version : 1 ,
589
+ } ,
590
+ } ,
591
+ operationName : 'PlaybackAccessToken' ,
592
+ variables : {
593
+ isLive : true ,
594
+ isVod : false ,
595
+ login : channel ,
596
+ platform : 'web' ,
597
+ playerType : 'site' ,
598
+ vodID : '' ,
599
+ } ,
474
600
} ,
475
601
{
476
602
headers : {
477
- 'Content-Type ' : 'application/json' ,
478
- 'User-Agent' : okHttpUserAgent ,
479
- authorization : `Bearer ${ isGame ? this . access_token : this . tv_access_token } ` ,
603
+ 'Client-id ' : TWITCH_CLIENT_ID ,
604
+ 'User-Agent' : userAgent ,
605
+ 'X-Device-Id' : this . twitchDeviceId ,
480
606
} ,
481
607
} ,
482
608
) ;
483
609
484
- return [ data . accessUrl , { } ] ;
610
+ return data . data . streamPlaybackAccessToken ;
485
611
} catch ( e ) {
486
612
console . error ( e ) ;
487
- console . log ( 'Could not start playback ' ) ;
613
+ console . log ( 'Could not get Twitch access token ' ) ;
488
614
}
489
615
} ;
490
616
@@ -571,7 +697,8 @@ class NflHandler {
571
697
private checkTVEAccess = ( ) : boolean => ( this . mvpdIdp ? true : false ) ;
572
698
private checkPeacockAccess = ( ) : boolean => ( this . peacockUserId ? true : false ) ;
573
699
private checkPrimeAccess = ( ) : boolean => ( this . amazonPrimeUserId ? true : false ) ;
574
- private checkSundayTicket = ( ) : boolean => ( this . youTubeUserId ? true : false ) ;
700
+ private checkSundayTicketAccess = ( ) : boolean => ( this . youTubeUserId ? true : false ) ;
701
+ private checkTwitchAccess = ( ) : boolean => ( this . twitchDeviceId ? true : false ) ;
575
702
576
703
private checkTVEEventAccess = ( event : INFLEvent ) : boolean => {
577
704
let hasChannel = false ;
@@ -684,14 +811,6 @@ class NflHandler {
684
811
mvpdUUID : this . mvpdUUID ,
685
812
mvpdUserId : this . mvpdUserId ,
686
813
} ) ,
687
- ...( this . amazonPrimeUserId && {
688
- amazonPrimeUUID : this . amazonPrimeUUID ,
689
- amazonPrimeUserId : this . amazonPrimeUserId ,
690
- } ) ,
691
- ...( this . peacockUserId && {
692
- peacockUUID : this . peacockUUID ,
693
- peacockUserId : this . peacockUserId ,
694
- } ) ,
695
814
...( this . youTubeUserId && {
696
815
youTubeUUID : this . youTubeUUID ,
697
816
youTubeUserId : this . youTubeUserId ,
@@ -708,20 +827,7 @@ class NflHandler {
708
827
this . tv_refresh_token = data . refreshToken ;
709
828
this . tv_expires_at = data . expiresIn ;
710
829
711
- if ( data . additionalInfo ) {
712
- data . additionalInfo . forEach ( ai => {
713
- if ( ai . data ) {
714
- if ( ai . data . idp === 'amazon' ) {
715
- this . amazonPrimeUUID = ai . data . newUUID ;
716
- }
717
- }
718
- } ) ;
719
- }
720
-
721
830
await this . save ( ) ;
722
-
723
- await this . checkRedZoneAccess ( ) ;
724
- await this . checkNetworkAccess ( ) ;
725
831
} catch ( e ) {
726
832
console . error ( e ) ;
727
833
console . log ( 'Could not refresh token for NFL' ) ;
@@ -962,6 +1068,7 @@ class NflHandler {
962
1068
peacockUUID,
963
1069
youTubeUserId,
964
1070
youTubeUUID,
1071
+ twitchDeviceId,
965
1072
} = tokens ;
966
1073
967
1074
this . device_id = device_id ;
@@ -983,6 +1090,7 @@ class NflHandler {
983
1090
this . peacockUUID = peacockUUID ;
984
1091
this . youTubeUUID = youTubeUUID ;
985
1092
this . youTubeUserId = youTubeUserId ;
1093
+ this . twitchDeviceId = twitchDeviceId ;
986
1094
} ;
987
1095
988
1096
private loadJSON = ( ) => {
0 commit comments