From 872e38e40a4baeb47971f2c017b9308810b98720 Mon Sep 17 00:00:00 2001
From: Your Name
Date: Fri, 4 Oct 2024 17:26:08 -0600
Subject: [PATCH] Hopefully fixed issue with NFL+ login breaking. Added NFL
Channel as a linear option. Added 1080p streaming for NFL+ linear feeds.
Quiet fix for Paramount+
---
README.md | 3 +-
package-lock.json | 4 +-
package.json | 2 +-
services/channels.ts | 8 +
services/networks.ts | 12 ++
services/nfl-handler.ts | 303 +++++++++++++++++++++++++++++++---
services/paramount-handler.ts | 2 +-
7 files changed, 310 insertions(+), 24 deletions(-)
diff --git a/README.md b/README.md
index 73f58db..2102794 100644
--- a/README.md
+++ b/README.md
@@ -2,7 +2,7 @@
-Current version: **3.2.4**
+Current version: **3.2.5**
# About
This takes ESPN/ESPN+, FOX 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).
@@ -78,6 +78,7 @@ Use if you would like to login with NFL+
|---|---|---|---|
| NFLPLUS | Set if you would like NFL+ events | False | False |
| NFLNETWORK* | Set if you would like the NFL Network channel (only available with `LINEAR_CHANNELS`) | False | False |
+| NFLCHANNEL* | Set if you would like the NFL Channel (only available with `LINEAR_CHANNELS`) | False | False |
#### NESN
Use if you would like to login with NESN. Will get NESN and NESN+ programming
diff --git a/package-lock.json b/package-lock.json
index 4b115b4..0bef411 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "eplustv",
- "version": "3.2.4",
+ "version": "3.2.5",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "eplustv",
- "version": "3.2.4",
+ "version": "3.2.5",
"license": "MIT",
"dependencies": {
"axios": "^1.2.2",
diff --git a/package.json b/package.json
index 54a886b..ea81319 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "eplustv",
- "version": "3.2.4",
+ "version": "3.2.5",
"description": "",
"scripts": {
"start": "ts-node index.ts",
diff --git a/services/channels.ts b/services/channels.ts
index e212390..8b5065a 100644
--- a/services/channels.ts
+++ b/services/channels.ts
@@ -160,6 +160,14 @@ export const CHANNEL_MAP = {
stationId: '65025',
tvgName: 'NFLNRZD',
},
+ 32: {
+ canUse: useNfl.channel,
+ id: 'NFLDIGITAL1_OO_v3',
+ logo: 'https://tmsimg.fancybits.co/assets/s121705_ll_h15_aa.png?w=360&h=270',
+ name: 'NFL Channel',
+ stationId: '121705',
+ tvgName: 'NFLDC1',
+ },
40: {
canUse: useMLBtv,
id: 'MLBTVBI',
diff --git a/services/networks.ts b/services/networks.ts
index 791987d..684382f 100644
--- a/services/networks.ts
+++ b/services/networks.ts
@@ -36,11 +36,20 @@ export const useParamount = {
export const useMsgPlus = process.env.MSGPLUS?.toLowerCase() === 'true' ? true : false;
export const useNfl = {
+ _channel: process.env.NFLCHANNEL?.toLowerCase() === 'true' ? true : false,
_network: process.env.NFLNETWORK?.toLowerCase() === 'true' ? true : false,
+ // _paramount: process.env.NFL_PARAMOUNT?.toLowerCase() === 'true' ? true : false,
_redZone: true,
+ // _tve: process.env.NFL_TVE?.toLowerCase() === 'true' ? true : false,
+ get channel(): boolean {
+ return this._channel && this.plus;
+ },
get network(): boolean {
return this._network && this.plus;
},
+ // get paramount(): boolean {
+ // return this._paramount && this.plus;
+ // },
plus: process.env.NFLPLUS?.toLowerCase() === 'true' ? true : false,
get redZone(): boolean {
return this._redZone && this.plus;
@@ -48,6 +57,9 @@ export const useNfl = {
set redZone(value: boolean) {
this._redZone = value;
},
+ // get tve(): boolean {
+ // return this._tve && this.plus;
+ // },
};
export const useMountainWest = process.env.MTNWEST?.toLowerCase() === 'true' ? true : false;
diff --git a/services/nfl-handler.ts b/services/nfl-handler.ts
index 7118f8b..e4b3594 100644
--- a/services/nfl-handler.ts
+++ b/services/nfl-handler.ts
@@ -19,6 +19,10 @@ interface INFLRes {
};
}
+interface INFLChannelRes {
+ items: INFLEvent[];
+}
+
interface INFLEvent {
title: string;
startTime: string;
@@ -69,8 +73,45 @@ const CLIENT_KEY = [
'7',
].join('');
+const TV_CLIENT_KEY = [
+ 'A',
+ '3',
+ 'b',
+ '7',
+ '4',
+ 'w',
+ 'O',
+ 'i',
+ 'S',
+ 'D',
+ 'M',
+ 'r',
+ 'h',
+ 'J',
+ 'K',
+ 'e',
+ 'X',
+ 'A',
+ 'E',
+ 'I',
+ 'q',
+ 'g',
+ 'R',
+ 'I',
+ 'C',
+ 'B',
+ 'i',
+ 'B',
+ 'N',
+ 'o',
+ '7',
+ 'o',
+].join('');
+
const CLIENT_SECRET = ['q', 'G', 'h', 'E', 'v', '1', 'R', 't', 'I', '2', 'S', 'f', 'R', 'Q', 'O', 'e'].join('');
+const TV_CLIENT_SECRET = ['u', 'o', 'C', 'y', 'y', 'k', 'y', 'U', 'w', 'D', 'b', 'f', 'Q', 'Z', 'r', '2'].join('');
+
const DEVICE_INFO = {
capabilities: {},
ctvDevice: 'AndroidTV',
@@ -90,8 +131,29 @@ const DEVICE_INFO = {
versionName: '59.0.29.1346644',
};
+const TV_DEVICE_INFO = {
+ capabilities: {},
+ ctvDevice: 'AndroidTV',
+ diskCapacity: 6964142080,
+ displayHeight: 2160,
+ displayWidth: 3840,
+ idfv: 'unknown',
+ isLocationEnabled: true,
+ manufacturer: 'onn',
+ memory: 2063581184,
+ model: 'onn. 4K Streaming Box',
+ networkType: 'wifi',
+ osName: 'Android',
+ osVersion: '12',
+ vendor: 'onn',
+ version: 'goldfish',
+ versionName: '18.0.65.101385778',
+};
+
const DEFAULT_CATEGORIES = ['NFL', 'NFL+', 'Football'];
+type TOtherAuth = 'paramount' | 'tve';
+
const parseAirings = async (events: INFLEvent[]) => {
const now = moment();
const endDate = moment().add(2, 'days').endOf('day');
@@ -103,7 +165,9 @@ const parseAirings = async (events: INFLEvent[]) => {
const start = moment(event.startTime);
const end = moment(start).add(event.duration, 'seconds');
- const isLinear = useLinear && (event.callSign === 'NFLNETWORK' || event.callSign === 'NFLNRZ');
+ const isLinear =
+ useLinear &&
+ (event.callSign === 'NFLNETWORK' || event.callSign === 'NFLNRZ' || event.callSign === 'NFLDIGITAL1_OO_v3');
if (!isLinear) {
end.add(1, 'hour');
@@ -137,7 +201,7 @@ const parseAirings = async (events: INFLEvent[]) => {
...(isLinear && {
channel: event.callSign,
linear: true,
- replay: event.callSign === 'NFLNETWORK',
+ replay: event.callSign === 'NFLNETWORK' || event.callSign === 'NFLDIGITAL1_OO_v3',
}),
});
}
@@ -151,6 +215,16 @@ class NflHandler {
public device_id?: string;
public device_info?: string;
public uid?: string;
+ public tv_access_token?: string;
+ public tv_refresh_token?: string;
+ public tv_expires_at?: number;
+
+ // Supplemental Auth
+ // public paramountPlusUserId?: string;
+ // public paramountPlusUUID?: string;
+ // public mvpdIdp?: string;
+ // public mvpdUserId?: string;
+ // public mvpdUUID?: string;
public initialize = async () => {
if (!useNfl.plus) {
@@ -168,6 +242,14 @@ class NflHandler {
if (!this.expires_at || !this.access_token) {
await this.startProviderAuthFlow();
}
+
+ // if (useNfl.paramount && (!this.paramountPlusUserId || !this.paramountPlusUUID)) {
+ // await this.startProviderAuthFlow('paramount');
+ // }
+
+ // if (useNfl.tve && (!this.mvpdIdp || !this.mvpdUserId || !this.mvpdUUID)) {
+ // await this.startProviderAuthFlow('tve');
+ // }
};
public refreshTokens = async () => {
@@ -176,7 +258,7 @@ class NflHandler {
}
if (!this.expires_at || moment(this.expires_at).isBefore(moment().add(30, 'minutes'))) {
- await this.extendToken();
+ await this.extendTokens();
}
};
@@ -199,6 +281,7 @@ class NflHandler {
const events: INFLEvent[] = [];
try {
+ const now = moment().subtract(12, 'hours');
const endSchedule = moment().add(2, 'days').endOf('day');
const url = ['https://', 'api.nfl.com', '/experience/v1/livestreams'].join('');
@@ -229,6 +312,26 @@ class NflHandler {
}
});
+ if (useNfl.channel) {
+ const url = [
+ 'https://',
+ 'api.nfl.com',
+ '/live/v1/nflchannel',
+ '?starttime=',
+ now.toISOString(),
+ '&endtime=',
+ endSchedule.toISOString(),
+ ].join('');
+
+ const {data: nflChannelData} = await axios.get(url, {
+ headers: {
+ Authorization: `Bearer ${this.access_token}`,
+ },
+ });
+
+ nflChannelData.items.forEach(i => events.push(i));
+ }
+
await parseAirings(events);
} catch (e) {
console.error(e);
@@ -238,7 +341,12 @@ class NflHandler {
public getEventData = async (id: string): Promise<[string, IHeaders]> => {
try {
- await this.extendToken();
+ await this.extendTokens();
+
+ const event = await db.entries.findOne({id});
+
+ const isGame =
+ event.channel !== 'NFLNETWORK' && event.channel !== 'NFLDIGITAL1_OO_v3' && event.channel !== 'NFLNRZ';
const url = ['https://', 'api.nfl.com/', 'play/v1/asset/', id].join('');
@@ -249,7 +357,7 @@ class NflHandler {
headers: {
'Content-Type': 'application/json',
'User-Agent': okHttpUserAgent,
- authorization: `Bearer ${this.access_token}`,
+ authorization: `Bearer ${isGame ? this.access_token : this.tv_access_token}`,
},
},
);
@@ -274,6 +382,11 @@ class NflHandler {
return useNfl.redZone;
};
+ private extendTokens = async (): Promise => {
+ await this.extendToken();
+ await this.extendTvToken();
+ };
+
private extendToken = async (uidSignature?: string, signatureTimestamp?: string): Promise => {
try {
const url = ['https://', 'api.nfl.com', '/identity/v3/token/refresh'].join('');
@@ -292,6 +405,15 @@ class NflHandler {
signatureTimestamp,
uidSignature,
}),
+ // ...(this.mvpdIdp && {
+ // mvpdIdp: this.mvpdIdp,
+ // mvpdUUID: this.mvpdUUID,
+ // mvpdUserId: this.mvpdUserId,
+ // }),
+ // ...(this.paramountPlusUserId && {
+ // paramountPlusUUID: this.paramountPlusUUID,
+ // paramountPlusUserId: this.paramountPlusUserId,
+ // }),
},
{
headers: {
@@ -312,6 +434,58 @@ class NflHandler {
}
};
+ private extendTvToken = async (uidSignature?: string, signatureTimestamp?: string): Promise => {
+ try {
+ const url = ['https://', 'api.nfl.com', '/identity/v3/token/refresh'].join('');
+
+ const {data} = await axios.post(
+ url,
+ {
+ clientKey: TV_CLIENT_KEY,
+ clientSecret: TV_CLIENT_SECRET,
+ deviceId: this.device_id,
+ deviceInfo: Buffer.from(JSON.stringify(TV_DEVICE_INFO), 'utf-8').toString('base64'),
+ networkType: 'wifi',
+ refreshToken: this.refresh_token,
+ uid: this.uid,
+ ...(uidSignature && {
+ signatureTimestamp,
+ uidSignature,
+ }),
+ // ...(this.mvpdIdp && {
+ // mvpdIdp: this.mvpdIdp,
+ // mvpdUUID: this.mvpdUUID,
+ // mvpdUserId: this.mvpdUserId,
+ // }),
+ // ...(this.paramountPlusUserId && {
+ // paramountPlusUUID: this.paramountPlusUUID,
+ // paramountPlusUserId: this.paramountPlusUserId,
+ // }),
+ },
+ {
+ headers: {
+ 'User-Agent': okHttpUserAgent,
+ },
+ },
+ );
+
+ this.tv_access_token = data.accessToken;
+ this.tv_refresh_token = data.refreshToken;
+ this.tv_expires_at = data.expiresIn;
+ this.save();
+
+ this.checkRedZoneAccess();
+ } catch (e) {
+ console.error(e);
+ console.log('Could not refresh token for NFL+');
+ }
+ };
+
+ private getTokens = async (): Promise => {
+ await this.getToken();
+ await this.getTvToken();
+ };
+
private getToken = async (): Promise => {
try {
const url = ['https://', 'api.nfl.com', '/identity/v3/token'].join('');
@@ -345,9 +519,42 @@ class NflHandler {
}
};
- private startProviderAuthFlow = async (): Promise => {
+ private getTvToken = async (): Promise => {
try {
- await this.getToken();
+ const url = ['https://', 'api.nfl.com', '/identity/v3/token'].join('');
+
+ const {data} = await axios.post(
+ url,
+ {
+ clientKey: TV_CLIENT_KEY,
+ clientSecret: TV_CLIENT_SECRET,
+ deviceId: this.device_id,
+ deviceInfo: Buffer.from(JSON.stringify(TV_DEVICE_INFO), 'utf-8').toString('base64'),
+ networkType: 'wifi',
+ },
+ {
+ headers: {
+ 'User-Agent': okHttpUserAgent,
+ },
+ },
+ );
+
+ this.tv_access_token = data.accessToken;
+ this.tv_refresh_token = data.refreshToken;
+
+ if (this.uid) {
+ this.tv_expires_at = data.expiresIn;
+ this.save();
+ }
+ } catch (e) {
+ console.error(e);
+ console.log('Could not get TV token for NFL+');
+ }
+ };
+
+ private startProviderAuthFlow = async (otherAuth?: TOtherAuth): Promise => {
+ try {
+ await this.getTokens();
const url = ['https://', 'api.nfl.com', '/utilities/v1/regcode'].join('');
const {data} = await axios.get(url, {
@@ -366,12 +573,22 @@ class NflHandler {
putUrl,
{
ctvDevice: 'AndroidTV',
- deviceId: '15942c3b-e487-4a95-bb9a-361ca08fd385',
+ deviceId: this.device_id,
expiresIn: 600,
- nflAccount: true,
- nflToken: true,
platform: 'ctv',
regCode: code,
+ ...(!otherAuth && {
+ nflAccount: true,
+ nflToken: true,
+ }),
+ // ...(otherAuth === 'paramount' && {
+ // idp: 'PARAMOUNT_PLUS',
+ // nflAccount: false,
+ // }),
+ // ...(otherAuth === 'tve' && {
+ // idp: 'TV_PROVIDER',
+ // nflAccount: false,
+ // }),
},
{
headers: {
@@ -383,7 +600,7 @@ class NflHandler {
);
console.log('=== NFL+ Auth ===');
- console.log('Please open a browser window and go to: https://id.nfl.com/account/activate?platform=androidtv');
+ console.log('Please open a browser window and go to: https://id.nfl.com/account/activate');
console.log('Enter code: ', code);
console.log('App will continue when login has completed...');
@@ -395,7 +612,7 @@ class NflHandler {
const authenticate = async () => {
if (numOfReqs < maxNumOfReqs) {
- const res = await this.authenticateRegCode(code);
+ const res = await this.authenticateRegCode(code, otherAuth);
numOfReqs += 1;
if (res) {
@@ -420,7 +637,7 @@ class NflHandler {
}
};
- private authenticateRegCode = async (code: string): Promise => {
+ private authenticateRegCode = async (code: string, otherAuth?: TOtherAuth): Promise => {
try {
const url = ['https://', 'api.nfl.com', '/keystore/v1/mvpd/', code].join('');
@@ -431,13 +648,39 @@ class NflHandler {
},
});
- if (!data || !data.uidSignature) {
+ if (!data) {
return false;
}
- this.uid = data.uid;
+ if (otherAuth) {
+ if (!data.userId) {
+ return false;
+ }
+
+ if (otherAuth === 'tve') {
+ // this.mvpdIdp = data.idp;
+ // this.mvpdUserId = data.userId;
+ // this.mvpdUUID = data.uuid;
+ // this.save();
+ } else if (otherAuth === 'paramount') {
+ // this.paramountPlusUserId = data.userId;
+ // this.paramountPlusUUID = data.uuid;
+ // this.save();
+ }
+
+ await this.extendToken();
+ await this.extendTvToken();
+ } else {
+ if (!data.uidSignature) {
+ return false;
+ }
+
+ this.uid = data.uid;
+
+ await this.extendToken(data.uidSignature, data.signatureTimestamp);
+ await this.extendTvToken(data.uidSignature, data.signatureTimestamp);
+ }
- await this.extendToken(data.uidSignature, data.signatureTimestamp);
return true;
} catch (e) {
return false;
@@ -450,15 +693,37 @@ class NflHandler {
private load = () => {
if (fs.existsSync(path.join(configPath, 'nfl_tokens.json'))) {
- const {device_id, access_token, expires_at, refresh_token, uid} = fsExtra.readJSONSync(
- path.join(configPath, 'nfl_tokens.json'),
- );
+ const {
+ device_id,
+ access_token,
+ expires_at,
+ refresh_token,
+ uid,
+ tv_access_token,
+ tv_expires_at,
+ tv_refresh_token,
+ // paramountPlusUserId,
+ // paramountPlusUUID,
+ // mvpdIdp,
+ // mvpdUserId,
+ // mvpdUUID,
+ } = fsExtra.readJSONSync(path.join(configPath, 'nfl_tokens.json'));
this.device_id = device_id;
this.access_token = access_token;
this.expires_at = expires_at;
this.refresh_token = refresh_token;
+ this.tv_access_token = tv_access_token;
+ this.tv_expires_at = tv_expires_at;
+ this.tv_refresh_token = tv_refresh_token;
this.uid = uid;
+
+ // Supplemental Auth
+ // this.paramountPlusUserId = paramountPlusUserId;
+ // this.paramountPlusUUID = paramountPlusUUID;
+ // this.mvpdIdp = mvpdIdp;
+ // this.mvpdUserId = mvpdUserId;
+ // this.mvpdUUID = mvpdUUID;
}
};
}
diff --git a/services/paramount-handler.ts b/services/paramount-handler.ts
index b92d409..50171c4 100644
--- a/services/paramount-handler.ts
+++ b/services/paramount-handler.ts
@@ -255,7 +255,7 @@ class ParamountHandler {
},
);
- data.listings.forEach(e => events.push(e));
+ data.listings?.forEach(e => events.push(e));
const channels = await this.getLiveChannels();