Skip to content

Commit 6c0a8fe

Browse files
author
Your Name
committed
Another attempt to fix tokens for Gotham + NFL. Added Twitch stream option for TNF
1 parent d0f0e51 commit 6c0a8fe

File tree

7 files changed

+191
-54
lines changed

7 files changed

+191
-54
lines changed

README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
<img src="https://i.imgur.com/FIGZdR3.png">
33
</p>
44

5-
Current version: **4.1.1**
5+
Current version: **4.1.2**
66

77
# About
88
This takes ESPN+, ESPN, FOX Sports, CBS Sports, Paramount+, MSG+, NFL, B1G+, NESN, Mountain West, FloSports, or MLB.tv programming and transforms it into a "live TV" experience with virtual linear channels. It will discover what is on, and generate a schedule of channels that will give you M3U and XMLTV files that you can import into something like [Jellyfin](https://jellyfin.org) or [Channels](https://getchannels.com).

package-lock.json

+2-2
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "eplustv",
3-
"version": "4.1.1",
3+
"version": "4.1.2",
44
"description": "",
55
"scripts": {
66
"start": "ts-node -r tsconfig-paths/register index.tsx",

services/gotham-handler.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -276,8 +276,9 @@ class GothamHandler {
276276

277277
await this.authenticateRegCode();
278278

279-
// Refresh access token
279+
// Refresh access token and entitlements
280280
await this.getAccessToken();
281+
await this.getEntitlements();
281282

282283
if (moment().add(20, 'hours').isAfter(this.expiresIn)) {
283284
console.log('Refreshing Gotham auth token');

services/nfl-handler.ts

+153-45
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import axios from 'axios';
55
import moment from 'moment';
66
import jwt_decode from 'jwt-decode';
77

8-
import {okHttpUserAgent} from './user-agent';
8+
import {okHttpUserAgent, userAgent} from './user-agent';
99
import {configPath} from './config';
1010
import {useNfl} from './networks';
1111
import {ClassTypeWithoutMethods, IEntry, IHeaders, IProvider} from './shared-interfaces';
@@ -42,6 +42,7 @@ interface INFLEvent {
4242
linear: boolean;
4343
networks: string[];
4444
broadcastAiringType?: string;
45+
hostNetwork?: string;
4546
}
4647

4748
const CLIENT_KEY = [
@@ -118,6 +119,39 @@ const CLIENT_SECRET = ['q', 'G', 'h', 'E', 'v', '1', 'R', 't', 'I', '2', 'S', 'f
118119

119120
const TV_CLIENT_SECRET = ['u', 'o', 'C', 'y', 'y', 'k', 'y', 'U', 'w', 'D', 'b', 'f', 'Q', 'Z', 'r', '2'].join('');
120121

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+
121155
const DEVICE_INFO = {
122156
capabilities: {},
123157
ctvDevice: 'AndroidTV',
@@ -160,14 +194,27 @@ const DEFAULT_CATEGORIES = ['NFL', 'NFL+', 'Football'];
160194

161195
const nflConfigPath = path.join(configPath, 'nfl_tokens.json');
162196

163-
export type TOtherAuth = 'prime' | 'tve' | 'peacock' | 'sunday_ticket';
197+
export type TOtherAuth = 'prime' | 'tve' | 'peacock' | 'sunday_ticket' | 'twitch';
164198

165199
interface INFLJwt {
166200
dmaCode: string;
167201
plans: {plan: string; status: string}[];
168202
networks?: {[key: string]: string};
169203
}
170204

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+
171218
const parseAirings = async (events: INFLEvent[]) => {
172219
const now = moment();
173220
const endDate = moment().add(2, 'days').endOf('day');
@@ -244,6 +291,7 @@ class NflHandler {
244291
public peacockUUID?: string;
245292
public youTubeUserId?: string;
246293
public youTubeUUID?: string;
294+
public twitchDeviceId?: string;
247295

248296
public initialize = async () => {
249297
const setup = (await db.providers.count({name: 'nfl'})) > 0 ? true : false;
@@ -343,9 +391,7 @@ class NflHandler {
343391
return;
344392
}
345393

346-
if (!this.expires_at || moment(this.expires_at * 1000).isBefore(moment())) {
347-
await this.extendTokens();
348-
}
394+
await this.extendTokens();
349395
};
350396

351397
public getSchedule = async (): Promise<void> => {
@@ -398,10 +444,24 @@ class NflHandler {
398444
this.checkTVEEventAccess(i) ||
399445
// Peacock
400446
(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()))
403449
) {
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+
}
405465
}
406466
} else if (
407467
i.callSign === 'NFLNRZ' &&
@@ -417,7 +477,7 @@ class NflHandler {
417477
i.contentType === 'GAME' &&
418478
i.language.find(l => l === 'en') &&
419479
i.authorizations.sunday_ticket &&
420-
this.checkSundayTicket()
480+
this.checkSundayTicketAccess()
421481
) {
422482
events.push(i);
423483
}
@@ -460,31 +520,97 @@ class NflHandler {
460520
const isGame =
461521
event.channel !== 'NFLNETWORK' && event.channel !== 'NFLDIGITAL1_OO_v3' && event.channel !== 'NFLNRZ';
462522

463-
const url = ['https://', 'api.nfl.com/', 'play/v1/asset/', id].join('');
523+
const isTwitch = event.feed === 'Twitch';
464524

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',
467584
{
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+
},
474600
},
475601
{
476602
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,
480606
},
481607
},
482608
);
483609

484-
return [data.accessUrl, {}];
610+
return data.data.streamPlaybackAccessToken;
485611
} catch (e) {
486612
console.error(e);
487-
console.log('Could not start playback');
613+
console.log('Could not get Twitch access token');
488614
}
489615
};
490616

@@ -571,7 +697,8 @@ class NflHandler {
571697
private checkTVEAccess = (): boolean => (this.mvpdIdp ? true : false);
572698
private checkPeacockAccess = (): boolean => (this.peacockUserId ? true : false);
573699
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);
575702

576703
private checkTVEEventAccess = (event: INFLEvent): boolean => {
577704
let hasChannel = false;
@@ -684,14 +811,6 @@ class NflHandler {
684811
mvpdUUID: this.mvpdUUID,
685812
mvpdUserId: this.mvpdUserId,
686813
}),
687-
...(this.amazonPrimeUserId && {
688-
amazonPrimeUUID: this.amazonPrimeUUID,
689-
amazonPrimeUserId: this.amazonPrimeUserId,
690-
}),
691-
...(this.peacockUserId && {
692-
peacockUUID: this.peacockUUID,
693-
peacockUserId: this.peacockUserId,
694-
}),
695814
...(this.youTubeUserId && {
696815
youTubeUUID: this.youTubeUUID,
697816
youTubeUserId: this.youTubeUserId,
@@ -708,20 +827,7 @@ class NflHandler {
708827
this.tv_refresh_token = data.refreshToken;
709828
this.tv_expires_at = data.expiresIn;
710829

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-
721830
await this.save();
722-
723-
await this.checkRedZoneAccess();
724-
await this.checkNetworkAccess();
725831
} catch (e) {
726832
console.error(e);
727833
console.log('Could not refresh token for NFL');
@@ -962,6 +1068,7 @@ class NflHandler {
9621068
peacockUUID,
9631069
youTubeUserId,
9641070
youTubeUUID,
1071+
twitchDeviceId,
9651072
} = tokens;
9661073

9671074
this.device_id = device_id;
@@ -983,6 +1090,7 @@ class NflHandler {
9831090
this.peacockUUID = peacockUUID;
9841091
this.youTubeUUID = youTubeUUID;
9851092
this.youTubeUserId = youTubeUserId;
1093+
this.twitchDeviceId = twitchDeviceId;
9861094
};
9871095

9881096
private loadJSON = () => {

0 commit comments

Comments
 (0)