diff --git a/README.md b/README.md index 4717274..aae13b1 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@

-Current version: **4.1.5** +Current version: **4.1.6** # About This takes ESPN+, ESPN, FOX Sports, CBS Sports, Paramount+, Gotham Sports, 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). diff --git a/package-lock.json b/package-lock.json index 40656d0..04f1592 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "eplustv", - "version": "4.1.5", + "version": "4.1.6", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "eplustv", - "version": "4.1.5", + "version": "4.1.6", "license": "MIT", "dependencies": { "@hono/node-server": "^1.13.1", diff --git a/package.json b/package.json index e3970be..707d3e5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "eplustv", - "version": "4.1.5", + "version": "4.1.6", "description": "", "scripts": { "start": "ts-node -r tsconfig-paths/register index.tsx", diff --git a/services/espn-handler.ts b/services/espn-handler.ts index baaefb2..736b81d 100644 --- a/services/espn-handler.ts +++ b/services/espn-handler.ts @@ -1,4 +1,5 @@ import fs from 'fs'; +import https from 'https'; import fsExtra from 'fs-extra'; import path from 'path'; import axios from 'axios'; @@ -37,6 +38,15 @@ global.WebSocket = ws; const espnPlusTokens = path.join(configPath, 'espn_plus_tokens.json'); const espnLinearTokens = path.join(configPath, 'espn_linear_tokens.json'); +const httpsAgent = new https.Agent({ + rejectUnauthorized: false, +}); + +// For `watch.graph.api.espn.com` URLs +const instance = axios.create({ + httpsAgent, +}); + interface IAuthResources { [key: string]: boolean; } @@ -707,7 +717,7 @@ class EspnHandler { espnPlusEnabled && (await this.getGraphQlApiKey()); try { - const {data: scenarios} = await axios.get('https://watch.graph.api.espn.com/api', { + const {data: scenarios} = await instance.get('https://watch.graph.api.espn.com/api', { params: { apiKey: this.graphQlApiKey, query: `{airing(id:"${eventId}",countryCode:"us",deviceType:SETTOP,tz:"Z") {id name description mrss:adobeRSS authTypes requiresLinearPlayback status:type startDateTime endDateTime duration source(authorization: SHIELD) { url authorizationType hasEspnId3Heartbeats hasNielsenWatermarks hasPassThroughAds commercialReplacement startSessionUrl } network { id type name adobeResource } image { url } sport { name code uid } league { name uid } program { code categoryCode isStudio } seekInSeconds simulcastAiringId airingId tracking { nielsenCrossId1 trackingId } eventId packages { name } language tier feedName brands { id name type }}}`, @@ -872,7 +882,7 @@ class EspnHandler { 'query Airings ( $countryCode: String!, $deviceType: DeviceType!, $tz: String!, $type: AiringType, $categories: [String], $networks: [String], $packages: [String], $eventId: String, $packageId: String, $start: String, $end: String, $day: String, $limit: Int ) { airings( countryCode: $countryCode, deviceType: $deviceType, tz: $tz, type: $type, categories: $categories, networks: $networks, packages: $packages, eventId: $eventId, packageId: $packageId, start: $start, end: $end, day: $day, limit: $limit ) { id airingId simulcastAiringId name type startDateTime shortDate: startDate(style: SHORT) authTypes adobeRSS duration feedName purchaseImage { url } image { url } network { id type abbreviation name shortName adobeResource isIpAuth } source { url authorizationType hasPassThroughAds hasNielsenWatermarks hasEspnId3Heartbeats commercialReplacement } packages { name } category { id name } subcategory { id name } sport { id name abbreviation code } league { id name abbreviation code } franchise { id name } program { id code categoryCode isStudio } tracking { nielsenCrossId1 nielsenCrossId2 comscoreC6 trackingId } } }'; const variables = `{"deviceType":"DESKTOP","countryCode":"US","tz":"UTC+0000","type":"LIVE","networks":${networks},"packages":${packages},"limit":500}`; - const {data: entryData} = await axios.get( + const {data: entryData} = await instance.get( encodeURI( `https://watch.graph.api.espn.com/api?apiKey=${this.graphQlApiKey}&query=${query}&variables=${variables}`, ), @@ -892,7 +902,7 @@ class EspnHandler { 'query Airings ( $countryCode: String!, $deviceType: DeviceType!, $tz: String!, $type: AiringType, $categories: [String], $networks: [String], $packages: [String], $eventId: String, $packageId: String, $start: String, $end: String, $day: String, $limit: Int ) { airings( countryCode: $countryCode, deviceType: $deviceType, tz: $tz, type: $type, categories: $categories, networks: $networks, packages: $packages, eventId: $eventId, packageId: $packageId, start: $start, end: $end, day: $day, limit: $limit ) { id airingId simulcastAiringId name type startDateTime shortDate: startDate(style: SHORT) authTypes adobeRSS duration feedName purchaseImage { url } image { url } network { id type abbreviation name shortName adobeResource isIpAuth } source { url authorizationType hasPassThroughAds hasNielsenWatermarks hasEspnId3Heartbeats commercialReplacement } packages { name } category { id name } subcategory { id name } sport { id name abbreviation code } league { id name abbreviation code } franchise { id name } program { id code categoryCode isStudio } tracking { nielsenCrossId1 nielsenCrossId2 comscoreC6 trackingId } } }'; const variables = `{"deviceType":"DESKTOP","countryCode":"US","tz":"UTC+0000","type":"UPCOMING","networks":${networks},"packages":${packages},"day":"${date}","limit":500}`; - const {data: entryData} = await axios.get( + const {data: entryData} = await instance.get( encodeURI( `https://watch.graph.api.espn.com/api?apiKey=${this.graphQlApiKey}&query=${query}&variables=${variables}`, ), diff --git a/services/nesn-handler.ts b/services/nesn-handler.ts index 1ee45cf..6f6a7a1 100644 --- a/services/nesn-handler.ts +++ b/services/nesn-handler.ts @@ -432,10 +432,10 @@ class NesnHandler { public getEventData = async (eventId: string): Promise<[string, IHeaders]> => { try { - const baseUrl = ['https://', 'dtc-stream-source-backup-prod.s3.us-east-2.amazonaws.com']; - const nesnUrl = [...baseUrl, '/nesn_stream_android_tv'].join(''); - const nesnPlusUrl = [...baseUrl, '/plus_stream_default'].join(''); - const nesn4k = [...baseUrl, '/4k_stream_default'].join(''); + const baseUrl = ['https://t2qkxvxfw6.execute-api.us-east-2.amazonaws.com/v3/stream?stream_type=']; + const nesnUrl = [...baseUrl, 'nesn_stream'].join(''); + const nesnPlusUrl = [...baseUrl, 'plus_stream'].join(''); + const nesn4k = [...baseUrl, '4k_stream'].join(''); await this.refreshTokens(); @@ -668,7 +668,7 @@ class NesnHandler { }, }); - return data.playbackToken; + return data?.subscription?.playbackToken; } catch (e) { console.error(e); console.log('Could not get playback token'); diff --git a/services/playlist-handler.ts b/services/playlist-handler.ts index 8e8be7f..8ad79dd 100644 --- a/services/playlist-handler.ts +++ b/services/playlist-handler.ts @@ -36,6 +36,7 @@ const PROXY_SEGMENTS = const reTarget = /#EXT-X-TARGETDURATION:([0-9]+)/; const reAudioTrack = /#EXT-X-MEDIA:TYPE=AUDIO.*URI="([^"]+)"/gm; +const reAudioTrackNesn = /#EXT-X-MEDIA.*TYPE=AUDIO.*URI="([^"]+)"/gm; const reMap = /#EXT-X-MAP:URI="([^"]+)"/gm; const reSubMap = /#EXT-X-MEDIA:TYPE=SUBTITLES.*URI="([^"]+)"/gm; const reVersion = /#EXT-X-VERSION:(\d+)/; @@ -108,6 +109,7 @@ export class PlaylistHandler { headers: resHeaders, } = await axios.get(manifestUrl, { headers: { + 'Accept-Encodixng': 'identity', 'User-Agent': userAgent, ...headers, }, @@ -146,7 +148,17 @@ export class PlaylistHandler { const clonedManifest = updateVersion(HLS.stringify(playlist)); let updatedManifest = clonedManifest; - if (this.network !== 'foxsports') { + if (this.network === 'nesn') { + const audioTracks = [...manifest.matchAll(reAudioTrackNesn)]; + audioTracks.forEach(track => { + if (track && track[1]) { + const fullChunklistUrl = parseReplacementUrl(track[1], realManifestUrl); + + const chunklistName = cacheLayer.getChunklistFromUrl(`${fullChunklistUrl}${urlParams}`); + updatedManifest = updatedManifest.replace(track[1], `${this.baseProxyUrl}${chunklistName}.m3u8`); + } + }); + } else if (this.network !== 'foxsports') { const audioTracks = [...manifest.matchAll(reAudioTrack)]; audioTracks.forEach(track => { if (track && track[1]) { @@ -196,6 +208,7 @@ export class PlaylistHandler { const {data: chunkList, request} = await axios.get(url, { headers: { + 'Accept-Encoding': 'identity', 'User-Agent': userAgent, ...this.headers, }, diff --git a/services/user-agent.ts b/services/user-agent.ts index 262ecff..77c7093 100644 --- a/services/user-agent.ts +++ b/services/user-agent.ts @@ -1,10 +1,10 @@ const userAgents = [ - 'Mozilla/5.0 (Macintosh; Intel Mac OS X 13_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/107.0.0.0 Safari/537.36', - 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/107.0.0.0 Safari/537.36', - 'Mozilla/5.0 (X11; Linux i686; rv:106.0) Gecko/20100101 Firefox/106.0', - 'Mozilla/5.0 (Macintosh; Intel Mac OS X 13.0; rv:106.0) Gecko/20100101 Firefox/106.0', - 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:106.0) Gecko/20100101 Firefox/106.0', - 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/107.0.0.0 Safari/537.36', + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36', + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 14.7; rv:133.0) Gecko/20100101 Firefox/133.0', + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.3', + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:133.0) Gecko/20100101 Firefox/133.0', + 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.3', + 'Mozilla/5.0 (X11; Linux i686; rv:133.0) Gecko/20100101 Firefox/133.0', ]; export const cbsSportsUserAgent = 'CBSSports/6.4.0-1723047637 (firetv)';