summaryrefslogtreecommitdiff
path: root/plugin.video.nfl.gamepass/resources
diff options
context:
space:
mode:
authorAlex Waite <alex@waite.eu>2017-08-17 14:44:09 +0200
committerenen92 <enen92@users.noreply.github.com>2017-08-17 13:44:09 +0100
commit762bf5e724b660504b431e7579f998715286b53a (patch)
tree12a7ddce8f3881a158105dfc590b1da03c04b15a /plugin.video.nfl.gamepass/resources
parent2e27b8f706ca0b603b62ec172f7f278aaa21e3b4 (diff)
[plugin.video.nfl.gamepass] 0.11.0 (#1372)
Diffstat (limited to 'plugin.video.nfl.gamepass/resources')
-rw-r--r--plugin.video.nfl.gamepass/resources/language/Dutch/strings.po56
-rw-r--r--plugin.video.nfl.gamepass/resources/language/English/strings.po18
-rw-r--r--plugin.video.nfl.gamepass/resources/language/German/strings.po28
-rw-r--r--plugin.video.nfl.gamepass/resources/language/Japanese/strings.po32
-rw-r--r--plugin.video.nfl.gamepass/resources/language/Russian/strings.po28
-rw-r--r--plugin.video.nfl.gamepass/resources/language/Ukrainian/strings.po28
-rw-r--r--plugin.video.nfl.gamepass/resources/lib/pigskin.py658
-rw-r--r--plugin.video.nfl.gamepass/resources/settings.xml5
8 files changed, 412 insertions, 441 deletions
diff --git a/plugin.video.nfl.gamepass/resources/language/Dutch/strings.po b/plugin.video.nfl.gamepass/resources/language/Dutch/strings.po
index 9f61d78..7164f27 100644
--- a/plugin.video.nfl.gamepass/resources/language/Dutch/strings.po
+++ b/plugin.video.nfl.gamepass/resources/language/Dutch/strings.po
@@ -78,8 +78,8 @@ msgid "Choose a game version"
msgstr "Kies een wedstrijd versie"
msgctxt "#30020"
-msgid "Date/time in local time"
-msgstr "Datum/tijd in lokale tijd"
+msgid "Time Notation"
+msgstr "Tijd Weergave"
msgctxt "#30021"
msgid "Error"
@@ -87,15 +87,15 @@ msgstr "Fout"
msgctxt "#30022"
msgid "Due to broadcast restrictions, NFL Game Pass Domestic is currently unavailable. Please try again later."
-msgstr "Vanwege uitzend beperkingen, NFL Game Pass Domestic is momenteel niet beschikbaar. Probeer het later opnieuw."
+msgstr "Vanwege uitzendbeperkingen is NFL Game Pass Domestic momenteel niet beschikbaar. Probeer het later opnieuw."
msgctxt "#30023"
msgid "Logging into NFL Game Pass failed. Make sure that your account information is correct and your subscription is valid."
-msgstr "Inloggen in NFL Game Pass mislukt. Zorg ervoor dat uw account informatie correct zijn en uw abonnement geldig is."
+msgstr "Inloggen bij NFL Game Pass is mislukt. Zorg ervoor dat uw accountgegevens correct zijn en uw abonnement geldig is."
msgctxt "#30024"
msgid "Unexpected error ='(. Please enable debuging for both the addon and Kodi, and submit a bug report."
-msgstr ""
+msgstr "Onverwachte fout ='(. Schakel debug logging in voor zowel de addon als Kodi en verstuur een bug report."
msgctxt "#30025"
msgid "Hide game length"
@@ -106,12 +106,12 @@ msgid "No"
msgstr "Nee"
msgctxt "#30027"
-msgid "Yes, with 12-hour clock (AM/PM)"
-msgstr "Ja, met 12-uurs klok (AM/PM)"
+msgid "12-hour clock (AM/PM)"
+msgstr "12-uurs klok (AM/PM)"
msgctxt "#30028"
-msgid "Yes, with 24-hour clock"
-msgstr "Ja, met 24-uurs klok"
+msgid "24-hour clock"
+msgstr "24-uurs klok"
msgctxt "#30029"
msgid "General"
@@ -127,40 +127,60 @@ msgstr "Coaches Film"
msgctxt "#30033"
msgid "Proxy Settings"
-msgstr ""
+msgstr "Proxy Instellingen"
msgctxt "#30034"
msgid "Use an HTTP proxy to access Game Pass"
-msgstr ""
+msgstr "Gebruik een HTTP proxy om Game Pass te benaderen"
msgctxt "#30035"
msgid "Protocol"
-msgstr ""
+msgstr "Protocol"
msgctxt "#30036"
msgid "Server"
-msgstr ""
+msgstr "Server"
msgctxt "#30037"
msgid "Port"
-msgstr ""
+msgstr "Poort"
msgctxt "#30038"
msgid "Username"
-msgstr ""
+msgstr "Gebruikersnaam"
msgctxt "#30039"
msgid "Password"
-msgstr ""
+msgstr "Wachtwoord"
msgctxt "#30042"
msgid "Enable 'Basic' Authentication"
-msgstr ""
+msgstr "Gebruik 'Eenvoudige' Authenticatie"
msgctxt "#30043"
msgid "There was a problem playing that stream"
-msgstr ""
+msgstr "Er was een probleem bij het afspelen van de stream"
msgctxt "#30044"
msgid "Some shows are known to not work. Please file a bug if the show is available and works in the official app."
msgstr ""
+
+msgctxt "#30045"
+msgid "No valid stream URL was found."
+msgstr "Geen geldige stream URL gevonden."
+
+msgctxt "#30046"
+msgid "There is currently no data available for this week."
+msgstr "Er zijn op dit moment geen gegevens beschikbaar voor deze week."
+
+msgctxt "#30047"
+msgid "PRESEASON WEEK {0}"
+msgstr "VOORSEIZOEN WEEK {0}"
+
+msgctxt "#30048"
+msgid "WEEK {0}"
+msgstr "WEEK {0}"
+
+msgctxt "#30049"
+msgid "Use InputStream Adaptive"
+msgstr "Gebruik Inputstream Adaptive"
diff --git a/plugin.video.nfl.gamepass/resources/language/English/strings.po b/plugin.video.nfl.gamepass/resources/language/English/strings.po
index 2303fe4..1f0afc6 100644
--- a/plugin.video.nfl.gamepass/resources/language/English/strings.po
+++ b/plugin.video.nfl.gamepass/resources/language/English/strings.po
@@ -78,7 +78,7 @@ msgid "Choose a game version"
msgstr ""
msgctxt "#30020"
-msgid "Localize Game Date/Time"
+msgid "Time Notation"
msgstr ""
msgctxt "#30021"
@@ -106,11 +106,11 @@ msgid "No"
msgstr ""
msgctxt "#30027"
-msgid "Yes, with 12-hour clock (AM/PM)"
+msgid "12-hour clock (AM/PM)"
msgstr ""
msgctxt "#30028"
-msgid "Yes, with 24-hour clock"
+msgid "24-hour clock"
msgstr ""
msgctxt "#30029"
@@ -172,3 +172,15 @@ msgstr ""
msgctxt "#30046"
msgid "There is currently no data available for this week."
msgstr ""
+
+msgctxt "#30047"
+msgid "PRESEASON WEEK {0}"
+msgstr ""
+
+msgctxt "#30048"
+msgid "WEEK {0}"
+msgstr ""
+
+msgctxt "#30049"
+msgid "Use InputStream Adaptive"
+msgstr ""
diff --git a/plugin.video.nfl.gamepass/resources/language/German/strings.po b/plugin.video.nfl.gamepass/resources/language/German/strings.po
index 19df51e..e630cc0 100644
--- a/plugin.video.nfl.gamepass/resources/language/German/strings.po
+++ b/plugin.video.nfl.gamepass/resources/language/German/strings.po
@@ -78,8 +78,8 @@ msgid "Choose a game version"
msgstr "Wähle eine Spiel Version"
msgctxt "#30020"
-msgid "Date/time in local time"
-msgstr "Uhrzeit in lokaler Zeitzone"
+msgid "Time Notation"
+msgstr ""
msgctxt "#30021"
msgid "Error"
@@ -106,11 +106,11 @@ msgid "No"
msgstr ""
msgctxt "#30027"
-msgid "Yes, with 12-hour clock (AM/PM)"
+msgid "12-hour clock (AM/PM)"
msgstr ""
msgctxt "#30028"
-msgid "Yes, with 24-hour clock"
+msgid "24-hour clock"
msgstr ""
msgctxt "#30029"
@@ -164,3 +164,23 @@ msgstr ""
msgctxt "#30044"
msgid "Some shows are known to not work. Please file a bug if the show is available and works in the official app."
msgstr ""
+
+msgctxt "#30045"
+msgid "No valid stream URL was found."
+msgstr ""
+
+msgctxt "#30046"
+msgid "There is currently no data available for this week."
+msgstr ""
+
+msgctxt "#30047"
+msgid "PRESEASON WEEK {0}"
+msgstr ""
+
+msgctxt "#30048"
+msgid "WEEK {0}"
+msgstr ""
+
+msgctxt "#30049"
+msgid "Use InputStream Adaptive"
+msgstr ""
diff --git a/plugin.video.nfl.gamepass/resources/language/Japanese/strings.po b/plugin.video.nfl.gamepass/resources/language/Japanese/strings.po
index fb818fb..77b15d7 100644
--- a/plugin.video.nfl.gamepass/resources/language/Japanese/strings.po
+++ b/plugin.video.nfl.gamepass/resources/language/Japanese/strings.po
@@ -79,8 +79,8 @@ msgid "Choose a game version"
msgstr "再生バージョン選択"
msgctxt "#30020"
-msgid "Localize Game Date/Time"
-msgstr "現地時間"
+msgid "Time Notation"
+msgstr ""
msgctxt "#30021"
msgid "Error"
@@ -107,12 +107,12 @@ msgid "No"
msgstr "いいえ"
msgctxt "#30027"
-msgid "Yes, with 12-hour clock (AM/PM)"
-msgstr "はい、12時間制 (AM/PM)"
+msgid "12-hour clock (AM/PM)"
+msgstr "12時間制 (AM/PM)"
msgctxt "#30028"
-msgid "Yes, with 24-hour clock"
-msgstr "はい、24時間制"
+msgid "24-hour clock"
+msgstr "24時間制"
msgctxt "#30029"
msgid "General"
@@ -165,3 +165,23 @@ msgstr "再生に問題が発生しました"
msgctxt "#30044"
msgid "Some shows are known to not work. Please file a bug if the show is available and works in the official app."
msgstr "再生できない試合があります。公式アプリで再生できる場合、バグをレポートしてください。"
+
+msgctxt "#30045"
+msgid "No valid stream URL was found."
+msgstr ""
+
+msgctxt "#30046"
+msgid "There is currently no data available for this week."
+msgstr ""
+
+msgctxt "#30047"
+msgid "PRESEASON WEEK {0}"
+msgstr ""
+
+msgctxt "#30048"
+msgid "WEEK {0}"
+msgstr ""
+
+msgctxt "#30049"
+msgid "Use InputStream Adaptive"
+msgstr ""
diff --git a/plugin.video.nfl.gamepass/resources/language/Russian/strings.po b/plugin.video.nfl.gamepass/resources/language/Russian/strings.po
index c5c9bfc..7f5f6b1 100644
--- a/plugin.video.nfl.gamepass/resources/language/Russian/strings.po
+++ b/plugin.video.nfl.gamepass/resources/language/Russian/strings.po
@@ -78,8 +78,8 @@ msgid "Choose a game version"
msgstr "Выбирать вручную"
msgctxt "#30020"
-msgid "Localize Game Date/Time"
-msgstr "Локализировать время игр"
+msgid "Time Notation"
+msgstr ""
msgctxt "#30021"
msgid "Error"
@@ -106,12 +106,12 @@ msgid "No"
msgstr "Нет"
msgctxt "#30027"
-msgid "Yes, with 12-hour clock (AM/PM)"
-msgstr "Да, 12ч (AM/PM)"
+msgid "12-hour clock (AM/PM)"
+msgstr "12ч (AM/PM)"
msgctxt "#30028"
-msgid "Yes, with 24-hour clock"
-msgstr "Да, 24ч"
+msgid "24-hour clock"
+msgstr "24ч"
msgctxt "#30029"
msgid "General"
@@ -172,3 +172,19 @@ msgstr "Некоторые шоу могут не работать. Пожалу
msgctxt "#30045"
msgid "No valid stream URL was found."
msgstr "Не возможно найти URL для потока."
+
+msgctxt "#30046"
+msgid "There is currently no data available for this week."
+msgstr ""
+
+msgctxt "#30047"
+msgid "PRESEASON WEEK {0}"
+msgstr ""
+
+msgctxt "#30048"
+msgid "WEEK {0}"
+msgstr ""
+
+msgctxt "#30049"
+msgid "Use InputStream Adaptive"
+msgstr ""
diff --git a/plugin.video.nfl.gamepass/resources/language/Ukrainian/strings.po b/plugin.video.nfl.gamepass/resources/language/Ukrainian/strings.po
index 383af0b..5de8e67 100644
--- a/plugin.video.nfl.gamepass/resources/language/Ukrainian/strings.po
+++ b/plugin.video.nfl.gamepass/resources/language/Ukrainian/strings.po
@@ -78,8 +78,8 @@ msgid "Choose a game version"
msgstr "Вибирати версію гри"
msgctxt "#30020"
-msgid "Localize Game Date/Time"
-msgstr "Локалізувати час гри"
+msgid "Time Notation"
+msgstr ""
msgctxt "#30021"
msgid "Error"
@@ -106,12 +106,12 @@ msgid "No"
msgstr "Ні"
msgctxt "#30027"
-msgid "Yes, with 12-hour clock (AM/PM)"
-msgstr "Так, 12г (AM/PM)"
+msgid "12-hour clock (AM/PM)"
+msgstr "12г (AM/PM)"
msgctxt "#30028"
-msgid "Yes, with 24-hour clock"
-msgstr "Так, 24г"
+msgid "24-hour clock"
+msgstr "24г"
msgctxt "#30029"
msgid "General"
@@ -172,3 +172,19 @@ msgstr "Деякі шоу можуть не працювати. Будь лас
msgctxt "#30045"
msgid "No valid stream URL was found."
msgstr "Неможливо знайти URL для потоку."
+
+msgctxt "#30046"
+msgid "There is currently no data available for this week."
+msgstr ""
+
+msgctxt "#30047"
+msgid "PRESEASON WEEK {0}"
+msgstr ""
+
+msgctxt "#30048"
+msgid "WEEK {0}"
+msgstr ""
+
+msgctxt "#30049"
+msgid "Use InputStream Adaptive"
+msgstr ""
diff --git a/plugin.video.nfl.gamepass/resources/lib/pigskin.py b/plugin.video.nfl.gamepass/resources/lib/pigskin.py
index 96e24de..0432993 100644
--- a/plugin.video.nfl.gamepass/resources/lib/pigskin.py
+++ b/plugin.video.nfl.gamepass/resources/lib/pigskin.py
@@ -2,36 +2,32 @@
A Kodi-agnostic library for NFL Game Pass
"""
import codecs
-import cookielib
-import hashlib
-import random
-import m3u8
-import re
+import uuid
import sys
+import json
+import calendar
+import time
import urllib
-from traceback import format_exc
-from uuid import getnode as get_mac
-from urlparse import urlsplit
+import xml.etree.ElementTree as ET
+from datetime import datetime, timedelta
import requests
-import xmltodict
-
+import m3u8
class pigskin(object):
- def __init__(self, proxy_config, cookie_file, debug=False):
+ def __init__(self, proxy_config, debug=False):
self.debug = debug
- self.subscription = ''
- self.base_url = 'https://gp.nfl.com/nflgp'
- self.servlets_url = 'https://gp.nfl.com/nflgp/servlets'
- self.simpleconsole_url = self.servlets_url + '/simpleconsole'
- self.boxscore_url = ''
- self.image_url = ''
- self.locEDLBaseUrl = ''
- self.non_seasonal_shows = {}
- self.seasonal_shows = {}
- self.nflnSeasons = []
-
+ self.base_url = 'https://www.nflgamepass.com'
+ self.user_agent = 'Firefox'
self.http_session = requests.Session()
+ self.access_token = None
+ self.refresh_token = None
+ self.config = self.make_request(self.base_url + '/api/en/content/v1/web/config', 'get')
+ self.client_id = self.config['modules']['API']['CLIENT_ID']
+ self.nfln_shows = {}
+ self.nfln_seasons = []
+ self.parse_shows()
+
if proxy_config is not None:
proxy_url = self.build_proxy_url(proxy_config)
if proxy_url != '':
@@ -39,44 +35,11 @@ class pigskin(object):
'http': proxy_url,
'https': proxy_url,
}
- self.cookie_jar = cookielib.LWPCookieJar(cookie_file)
- try:
- self.cookie_jar.load(ignore_discard=True, ignore_expires=True)
- except IOError:
- pass
- self.http_session.cookies = self.cookie_jar
-
- # get needed URLs from simpleconsole
- # no auth needed, so we can get this info without invoking a login
- url = self.simpleconsole_url
- post_data = {'isFlex': 'true'}
- sc_data = self.make_request(url=url, method='post', payload=post_data)
- try:
- url_dict = xmltodict.parse(sc_data)
- self.boxscore_url = url_dict['result']['pbpFeedPrefix']
- self.image_url = url_dict['result']['config']['locProgramImage']
- self.locEDLBaseUrl = url_dict['result']['config']['locEDL'].replace('/edl/nflgp/', '')
-
- self.log('boxscore url: %s' % self.boxscore_url)
- self.log('image url: %s' % self.image_url)
- self.log('locEDLBaseUrl: %s' % self.locEDLBaseUrl)
- except xmltodict.expat.ExpatError:
- self.log('Failed to parse contents of the "simpleconsole".')
- self.log('pigskin __init__-ing failed. Time to debug!')
- return None
-
- # get subscription type
- if '<isGPDomestic>' in sc_data:
- self.subscription = 'domestic'
- self.log('NFL Game Pass Domestic detected.')
- else:
- self.subscription = 'international'
- self.log('NFL Game Pass International detected.')
self.log('Debugging enabled.')
self.log('Python Version: %s' % sys.version)
- class LoginFailure(Exception):
+ class GamePassError(Exception):
def __init__(self, value):
self.value = value
@@ -95,6 +58,43 @@ class pigskin(object):
except:
pass
+ def make_request(self, url, method, params=None, payload=None, headers=None):
+ """Make an HTTP request. Return the response."""
+ self.log('Request URL: %s' % url)
+ self.log('Method: %s' % method)
+ if params:
+ self.log('Params: %s' % params)
+ if payload:
+ self.log('Payload: %s' % payload)
+ if headers:
+ self.log('Headers: %s' % headers)
+
+ if method == 'get':
+ req = self.http_session.get(url, params=params, headers=headers)
+ elif method == 'put':
+ req = self.http_session.put(url, params=params, data=payload, headers=headers)
+ else: # post
+ req = self.http_session.post(url, params=params, data=payload, headers=headers)
+ self.log('Response code: %s' % req.status_code)
+ self.log('Response: %s' % req.content)
+
+ return self.parse_response(req)
+
+ def parse_response(self, req):
+ """Try to load JSON data into dict and raise potential errors."""
+ try:
+ response = json.loads(req.content)
+ except ValueError: # if response is not json
+ response = req.content
+
+ if isinstance(response, dict):
+ for key in response.keys():
+ if key.lower() == 'message':
+ if response[key]: # raise all messages as GamePassError if message is not empty
+ raise self.GamePassError(response[key])
+
+ return response
+
def build_proxy_url(self, config):
proxy_url = ''
@@ -129,393 +129,259 @@ class pigskin(object):
return proxy_url
- def check_for_coachestape(self, game_id, season):
- """Return whether coaches tape is available for a given game."""
- url = self.boxscore_url + '/' + season + '/' + game_id + '.xml'
- boxscore = self.make_request(url=url, method='get')
-
- try:
- boxscore_dict = xmltodict.parse(boxscore, encoding='cp1252')
- except xmltodict.expat.ExpatError:
- try:
- boxscore_dict = xmltodict.parse(boxscore)
- except xmltodict.expat.ExpatError:
- return False
-
- try:
- if boxscore_dict['dataset']['@coach'] == 'true':
- return True
- else:
- return False
- except KeyError:
- return False
-
- def check_for_subscription(self):
- """Return whether a subscription and user name are detected. Determines
- whether a login was successful."""
- url = self.simpleconsole_url
- post_data = {'isFlex': 'true'}
- sc_data = self.make_request(url=url, method='post', payload=post_data)
-
- if '</userName>' not in sc_data:
- self.log('No user name detected in Game Pass response.')
- return False
- elif '</subscription>' not in sc_data:
- self.log('No subscription detected in Game Pass response.')
- return False
- else:
- self.log('Subscription and user name detected in Game Pass response.')
- return True
-
- def gen_plid(self):
- """Return a "unique" MD5 hash. Getting the video path requires a plid,
- which looks like MD5 and always changes. Reusing a plid does not work,
- so our guess is that it's a id for each instance of the player.
+ def login(self, username, password):
+ """Blindly authenticate to Game Pass. Use has_subscription() to
+ determine success.
"""
- rand = random.getrandbits(10)
- mac_address = str(get_mac())
- md5 = hashlib.md5(str(rand) + mac_address)
- return md5.hexdigest()
-
- def get_coaches_playIDs(self, game_id, season):
- """Return a dict of play IDs with associated play descriptions."""
- playIDs = {}
- url = self.boxscore_url + '/' + season + '/' + game_id + '.xml'
- boxscore = self.make_request(url=url, method='get')
-
- try:
- boxscore_dict = xmltodict.parse(boxscore, encoding='cp1252')
- except xmltodict.expat.ExpatError:
- try:
- boxscore_dict = xmltodict.parse(boxscore)
- except xmltodict.expat.ExpatError:
- return False
-
- for row in boxscore_dict['dataset']['table']['row']:
- playIDs[row['@PlayID']] = row['@PlayDescription']
-
- return playIDs
-
- def get_coaches_url(self, game_id, game_date, event_id):
- """Return the URL for a coaches-film play."""
- self.get_current_season_and_week() # set cookies
- url = self.servlets_url + '/publishpoint'
-
- post_data = {'id': game_id, 'type': 'game', 'nt': '1', 'gt': 'coach',
- 'event': event_id, 'bitrate': '1600', 'gdate': game_date}
- headers = {'User-Agent': 'iPad'}
- coach_data = self.make_request(url=url, method='post', payload=post_data, headers=headers)
- coach_dict = xmltodict.parse(coach_data)['result']
-
- return coach_dict['path']
-
- def get_current_season_and_week(self):
- """Return the current season and week_code (e.g. 210) in a dict."""
- url = self.simpleconsole_url
- post_data = {'isFlex': 'true'}
- sc_data = self.make_request(url=url, method='post', payload=post_data)
-
- sc_dict = xmltodict.parse(sc_data)['result']
- current_s_w = {sc_dict['currentSeason']: sc_dict['currentWeek']}
- return current_s_w
-
- def parse_shows(self, sc_dict):
- """Parse return from /simpleconsole request to build shows list dynamically"""
- try:
- # All (nearly) NFL Network Shows
- show_dict = {}
- for show in sc_dict['nflnShows']['show']:
- name = show['name']
- season_dict = {}
-
- for season in show['seasons']['season']:
- if isinstance(season, dict):
- season_id = season['@catId']
- season_name = season['#text']
- else:
- season_id = show['seasons']['season']['@catId']
- season_name = show['seasons']['season']['#text']
-
- # Trim season name to just the year if year is present
- # Common season names: '2014', 'Season 2014', and 'Archives'
- try:
- season_name = re.findall(r"\d{4}(?!\d)", season_name)[0]
- except IndexError:
- pass
-
- season_dict[season_name] = season_id
-
- if season_name not in self.nflnSeasons:
- self.nflnSeasons.append(season_name)
-
- show_dict[name] = season_dict
-
- # RedZone is "special" and is returned separately in the XML
- rz_dict = {}
- for season in sc_dict['redZoneCats']['cat']:
- rz_dict[season['@season']] = season['@id']
-
- if season['@season'] not in self.nflnSeasons:
- self.nflnSeasons.append(season['@season'])
-
- show_dict['RedZone Archives'] = rz_dict
-
- self.seasonal_shows.update(show_dict)
- except KeyError:
- self.log('Parsing shows failed')
- raise
-
- def get_publishpoint_streams(self, video_id, stream_type=None, game_type=None):
- """Return the URL for a stream."""
- streams = {}
- self.get_current_season_and_week() # set cookies
- url = self.servlets_url + '/publishpoint'
-
- if video_id == 'nfl_network':
- post_data = {'id': '1', 'type': 'channel', 'nt': '1'}
- elif video_id == 'redzone':
- post_data = {'id': '2', 'type': 'channel', 'nt': '1'}
- elif stream_type == 'game':
- post_data = {'id': video_id, 'type': stream_type, 'nt': '1', 'gt': game_type}
- else:
- post_data = {'id': video_id, 'type': stream_type, 'nt': '1'}
-
- headers = {'User-Agent': 'iPad'}
- m3u8_data = self.make_request(url=url, method='post', payload=post_data, headers=headers)
- m3u8_dict = xmltodict.parse(m3u8_data)['result']
- self.log('NFL Dict %s' % m3u8_dict)
-
- m3u8_url = m3u8_dict['path'].replace('_ipad', '')
- m3u8_param = m3u8_url.split('?', 1)[-1]
- # I /hate/ lying with User-Agent.
- # Huge points for making this work without lying.
- m3u8_header = {'Cookie': 'nlqptid=' + m3u8_param,
- 'User-Agent': 'Safari/537.36 Mozilla/5.0 AppleWebKit/537.36 Chrome/31.0.1650.57',
- 'Accept-encoding': 'identity, gzip, deflate',
- 'Connection': 'keep-alive'}
-
- try:
- m3u8_manifest = self.make_request(url=m3u8_url, method='get')
- except:
- m3u8_manifest = False
-
- if m3u8_manifest:
- m3u8_obj = m3u8.loads(m3u8_manifest)
- if m3u8_obj.is_variant: # if this m3u8 contains links to other m3u8s
- for playlist in m3u8_obj.playlists:
- bitrate = int(playlist.stream_info.bandwidth) / 1000
- streams[str(bitrate)] = m3u8_url[:m3u8_url.rfind('/') + 1] + playlist.uri + '?' + m3u8_url.split('?')[1] + '|' + urllib.urlencode(m3u8_header)
- else:
- streams['sole available'] = m3u8_url
-
- return streams
-
- def get_shows(self, season):
- """Return a list of all shows for a season."""
- seasons_shows = self.non_seasonal_shows.keys()
- for show_name, show_codes in self.seasonal_shows.items():
- if season in show_codes:
- seasons_shows.append(show_name)
+ url = self.config['modules']['API']['LOGIN']
+ post_data = {
+ 'username': username,
+ 'password': password,
+ 'client_id': self.client_id,
+ 'grant_type': 'password'
+ }
+ data = self.make_request(url, 'post', payload=post_data)
+ self.access_token = data['access_token']
+ self.refresh_token = data['refresh_token']
+ self.check_for_subscription()
- return sorted(seasons_shows)
+ return True
- def get_shows_episodes(self, show_name, season=None):
- """Return a list of episodes for a show. Return empty list if none are
- found or if an error occurs.
- """
- url = self.servlets_url + '/browse'
- try:
- cid = self.seasonal_shows[show_name][season]
- except KeyError:
- try:
- cid = self.non_seasonal_shows[show_name]
- except KeyError:
- return []
+ def check_for_subscription(self):
+ """Returns True if a subscription is detected. Raises error_unauthorised on failure."""
+ url = self.config['modules']['API']['USER_PROFILE']
+ headers = {'Authorization': 'Bearer {0}'.format(self.access_token)}
+ self.make_request(url, 'get', headers=headers)
- if show_name == 'NFL RedZone Archives':
- ps = 17
- else:
- ps = 50
+ return True
+ def refresh_tokens(self):
+ """Refreshes authorization tokens."""
+ url = self.config['modules']['API']['LOGIN']
post_data = {
- 'isFlex': 'true',
- 'cid': cid,
- 'pm': 0,
- 'ps': ps,
- 'pn': 1
+ 'refresh_token': self.refresh_token,
+ 'client_id': self.client_id,
+ 'grant_type': 'refresh_token'
}
+ data = self.make_request(url, 'post', payload=post_data)
+ self.access_token = data['access_token']
+ self.refresh_token = data['refresh_token']
- archive_data = self.make_request(url=url, method='post', payload=post_data)
- archive_dict = xmltodict.parse(archive_data)['result']
-
- try:
- items = archive_dict['programs']['program']
- # if only one episode is returned, we explicitly put it into a list
- if isinstance(items, dict):
- items = [items]
- return items
- except TypeError:
- return []
+ return True
def get_seasons_and_weeks(self):
"""Return a multidimensional array of all seasons and weeks."""
seasons_and_weeks = {}
try:
- url = self.locEDLBaseUrl + '/mobile/weeks_v2.xml'
- s_w_data = self.make_request(url=url, method='get')
- s_w_data_dict = xmltodict.parse(s_w_data)
+ url = self.config['modules']['ROUTES_DATA_PROVIDERS']['games']
+ seasons = self.make_request(url, 'get')
except:
self.log('Acquiring season and week data failed.')
raise
try:
- for season in s_w_data_dict['seasons']['season']:
- year = season['@season']
- season_dict = {}
-
- for week in season['week']:
- if week['@section'] == "pre": # preseason
- week_code = '1' + week['@value'].zfill(2)
- season_dict[week_code] = week
- else: # regular season and post season
- week_code = '2' + week['@value'].zfill(2)
- season_dict[week_code] = week
-
- seasons_and_weeks[year] = season_dict
+ for season in seasons['modules']['mainMenu']['seasonStructureList']:
+ weeks = []
+ year = str(season['season'])
+ for season_type in season['seasonTypes']:
+ for week in season_type['weeks']:
+ week_dict = {
+ 'week_number': str(week['number']),
+ 'week_name': week['weekNameAbbr'],
+ 'season_type': season_type['seasonType']
+ }
+ weeks.append(week_dict)
+
+ seasons_and_weeks[year] = weeks
except KeyError:
self.log('Parsing season and week data failed.')
raise
return seasons_and_weeks
- def get_weeks_games(self, season, week_code):
- """Return a list of games for a week."""
- url = self.servlets_url + '/games'
- post_data = {
- 'isFlex': 'true',
- 'season': season,
- 'week': week_code
+ def get_current_season_and_week(self):
+ """Return the current season, season type, and week in a dict."""
+ try:
+ url = self.config['modules']['ROUTES_DATA_PROVIDERS']['games']
+ seasons = self.make_request(url, 'get')
+ except:
+ self.log('Acquiring season and week data failed.')
+ raise
+
+ current_s_w = {
+ 'season': seasons['modules']['meta']['currentContext']['currentSeason'],
+ 'season_type': seasons['modules']['meta']['currentContext']['currentSeasonType'],
+ 'week': str(seasons['modules']['meta']['currentContext']['currentWeek'])
}
- game_data = self.make_request(url=url, method='post', payload=post_data)
- game_data_dict = xmltodict.parse(game_data)['result']
- if game_data_dict['games']:
- games = game_data_dict['games']['game']
- # if only one game is returned, we explicitly put it into a list
- if isinstance(games, dict):
- games = [games]
+ return current_s_w
- return games
+ def get_weeks_games(self, season, season_type, week):
+ try:
+ url = self.config['modules']['ROUTES_DATA_PROVIDERS']['games_detail'].replace(':seasonType', season_type).replace(':season', season).replace(':week', week)
+ games_data = self.make_request(url, 'get')
+ # collect the games from all keys in 'modules'
+ games = [g for x in games_data['modules'].keys() for g in games_data['modules'][x]['content']]
+ except:
+ self.log('Acquiring games data failed.')
+ raise
+
+ return sorted(games, key=lambda x: x['gameDateTimeUtc'])
+
+ def has_coaches_tape(self, game_id, season):
+ """Return whether coaches tape is available for a given game."""
+ url = self.config['modules']['ROUTES_DATA_PROVIDERS']['game_page'].replace(':season', season).replace(':gameslug', game_id)
+ response = self.make_request(url, 'get')
+ coaches_tape = response['modules']['singlegame']['content'][0]['coachfilmVideo']
+ if coaches_tape:
+ self.log('Coaches Tape found.')
+ return coaches_tape['videoId']
else:
- return None
+ self.log('No Coaches Tape found for this game.')
+ return False
- def login(self, username=None, password=None):
- """Complete login process for Game Pass. Errors (auth issues, blackout,
- etc) are raised as LoginFailure.
- """
- if self.check_for_subscription():
- self.log('Already logged into Game Pass %s' % self.subscription)
+
+ def has_condensed_game(self, game_id, season):
+ """Return whether condensed game version is available."""
+ url = self.config['modules']['ROUTES_DATA_PROVIDERS']['game_page'].replace(':season', season).replace(':gameslug', game_id)
+ response = self.make_request(url, 'get')
+ condensed = response['modules']['singlegame']['content'][0]['condensedVideo']
+ if condensed:
+ self.log('Condensed game found.')
+ return condensed['videoId']
else:
- if username and password:
- self.log('Not (yet) logged into %s' % self.subscription)
- self.login_to_account(username, password)
- if not self.check_for_subscription():
- raise self.LoginFailure('%s login failed' % self.subscription)
- elif self.subscription == 'domestic' and self.service_blackout():
- raise self.LoginFailure('Game Pass Domestic Blackout')
+ self.log('No condensed version was found for this game.')
+ return False
+
+ def get_stream(self, video_id, game_type=None, username=None):
+ """Return the URL for a stream."""
+ self.refresh_tokens()
+
+ if video_id == 'nfl_network':
+ diva_config_url = self.config['modules']['DIVA']['HTML5']['SETTINGS']['Live24x7']
+ url = self.config['modules']['ROUTES_DATA_PROVIDERS']['network']
+ response = self.make_request(url, 'get')
+ video_id = response['modules']['networkLiveVideo']['content'][0]['videoId']
+ else:
+ if game_type == 'live':
+ diva_config_url = self.config['modules']['DIVA']['HTML5']['SETTINGS']['LiveNoData']
else:
- self.log('No username and password supplied.')
- raise self.LoginFailure('No username and password supplied.')
+ diva_config_url = self.config['modules']['DIVA']['HTML5']['SETTINGS']['VodNoData']
+
+ diva_config_data = self.make_request(diva_config_url.replace('device', 'html5'), 'get')
+ diva_config_root = ET.fromstring(diva_config_data)
+ for i in diva_config_root.iter('parameter'):
+ if i.attrib['name'] == 'processingUrlCallPath':
+ processing_url = i.attrib['value']
+ elif i.attrib['name'] == 'videoDataPath':
+ stream_request_url = i.attrib['value'].replace('{V.ID}', video_id)
+ akamai_xml_data = self.make_request(stream_request_url, 'get')
+ akamai_xml_root = ET.fromstring(akamai_xml_data)
+ for i in akamai_xml_root.iter('videoSource'):
+ if i.attrib['format'] == 'ChromeCast':
+ for text in i.itertext():
+ if 'http' in text:
+ m3u8_url = text
+ break
- def login_to_account(self, username, password):
- """Blindly authenticate to Game Pass. Use check_for_subscription() to
- determine success.
- """
- url = self.base_url + '/secure/nfllogin'
post_data = {
- 'username': username,
- 'password': password
+ 'Type': '1',
+ 'User': '',
+ 'VideoId': video_id,
+ 'VideoSource': m3u8_url,
+ 'VideoKind': 'Video',
+ 'AssetState': '3',
+ 'PlayerType': 'HTML5',
+ 'other': '{0}|{1}|web|{1}|undefined|{2}' .format(str(uuid.uuid4()), self.access_token, self.user_agent, username)
}
- self.make_request(url=url, method='post', payload=post_data)
- def make_request(self, url, method, payload=None, headers=None):
- """Make an http request. Return the response."""
- self.log('Request URL: %s' % url)
- self.log('Headers: %s' % headers)
+ response = self.make_request(processing_url, 'post', payload=json.dumps(post_data))
- try:
- if method == 'get':
- req = self.http_session.get(url, params=payload, headers=headers, allow_redirects=False)
- else: # post
- req = self.http_session.post(url, data=payload, headers=headers, allow_redirects=False)
- req.raise_for_status()
- self.log('Response code: %s' % req.status_code)
- self.log('Response: %s' % req.content)
- self.cookie_jar.save(ignore_discard=True, ignore_expires=False)
- return req.content
- except requests.exceptions.HTTPError as error:
- self.log('An HTTP error occurred: %s' % error)
- raise
- except requests.exceptions.ProxyError:
- self.log('Error connecting to proxy server')
- raise
- except requests.exceptions.ConnectionError as error:
- self.log('Connection Error: - %s' % error.message)
- raise
- except requests.exceptions.RequestException as error:
- self.log('Error: - %s' % error.value)
- raise
+ return self.parse_m3u8_manifest(response['ContentUrl'])
- def parse_manifest(self, manifest):
- """Return a dict of the supplied XML manifest. Builds and adds
- "full_url" for convenience.
- """
+ def parse_m3u8_manifest(self, manifest_url):
+ """Return the manifest URL along with its bitrate."""
streams = {}
- manifest_dict = xmltodict.parse(manifest)
-
- for stream in manifest_dict['channel']['streamDatas']['streamData']:
- try:
- url_path = stream['@url']
- bitrate = url_path[(url_path.rindex('_') + 1):url_path.rindex('.')]
- try:
- stream['full_url'] = 'http://%s%s.m3u8' % (stream['httpservers']['httpserver']['@name'], url_path)
- except TypeError: # if multiple servers are returned, use the first in the list
- stream['full_url'] = 'http://%s%s.m3u8' % (stream['httpservers']['httpserver'][0]['@name'], url_path)
-
- streams[bitrate] = stream
- except KeyError:
- self.log(format_exc())
+ m3u8_header = {
+ 'Connection': 'keep-alive',
+ 'User-Agent': self.user_agent
+ }
+ streams['manifest_url'] = manifest_url + '|' + urllib.urlencode(m3u8_header)
+ streams['bitrates'] = {}
+ m3u8_manifest = self.make_request(manifest_url, 'get')
+ m3u8_obj = m3u8.loads(m3u8_manifest)
+ for playlist in m3u8_obj.playlists:
+ bitrate = int(playlist.stream_info.bandwidth) / 1000
+ streams['bitrates'][bitrate] = manifest_url[:manifest_url.rfind('/manifest') + 1] + playlist.uri + '?' + manifest_url.split('?')[1] + '|' + urllib.urlencode(m3u8_header)
return streams
def redzone_on_air(self):
"""Return whether RedZone Live is currently broadcasting."""
- url = self.simpleconsole_url
- post_data = {'isFlex': 'true'}
- sc_data = self.make_request(url=url, method='post', payload=post_data)
+ url = self.config['modules']['ROUTES_DATA_PROVIDERS']['redzone']
+ response = self.make_request(url, 'get')
+ if not response['modules']['redZoneLive']['content']:
+ return False
+ else:
+ return True
- sc_dict = xmltodict.parse(sc_data)['result']
+ def parse_shows(self):
+ """Dynamically parse the NFL Network shows into a dict."""
+ show_dict = {}
+ url = self.config['modules']['API']['NETWORK_PROGRAMS']
+ response = self.make_request(url, 'get')
+
+ for show in response['modules']['programs']:
+ season_dict = {}
+ for season in show['seasons']:
+ season_name = season['value']
+ season_id = season['slug']
+ season_dict[season_name] = season_id
+ if season_name not in self.nfln_seasons:
+ self.nfln_seasons.append(season_name)
+ show_dict[show['title']] = season_dict
+ self.nfln_shows.update(show_dict)
- # Dynamically parse NFL-Network shows
- self.parse_shows(sc_dict)
+ def get_shows(self, season):
+ """Return a list of all shows for a season."""
+ seasons_shows = []
- # Check if RedZone is Live
- if sc_dict['rzPhase'] in ('pre', 'in'):
- self.log('RedZone is on air.')
- return True
- else:
- self.log('RedZone is not on air.')
- return False
+ for show_name, show_codes in self.nfln_shows.items():
+ if season in show_codes:
+ seasons_shows.append(show_name)
- def service_blackout(self):
- """Return whether Game Pass is blacked out."""
- url = self.base_url + '/secure/schedule'
- blackout_message = ('Due to broadcast restrictions, NFL Game Pass is currently unavailable.'
- ' Please check back later.')
- service_data = self.make_request(url=url, method='get')
+ return sorted(seasons_shows)
- if blackout_message in service_data:
- return True
+ def get_shows_episodes(self, show_name, season=None):
+ """Return a list of episodes for a show. Return empty list if none are
+ found or if an error occurs."""
+ url = self.config['modules']['API']['NETWORK_PROGRAMS']
+ programs = self.make_request(url, 'get')['modules']['programs']
+ for show in programs:
+ if show_name == show['title']:
+ selected_show = show
+ break
+ season_slug = [x['slug'] for x in selected_show['seasons'] if season == x['value']][0]
+ request_url = self.config['modules']['API']['NETWORK_EPISODES']
+ episodes_url = request_url.replace(':seasonSlug', season_slug).replace(':tvShowSlug', selected_show['slug'])
+
+ return self.make_request(episodes_url, 'get')['modules']['archive']['content']
+
+ def parse_datetime(self, date_string, localize=False):
+ """Parse NFL Game Pass date string to datetime object."""
+ date_time_format = '%Y-%m-%dT%H:%M:%S.%fZ'
+ datetime_obj = datetime(*(time.strptime(date_string, date_time_format)[0:6]))
+ if localize:
+ return self.utc_to_local(datetime_obj)
else:
- return False
+ return datetime_obj
+
+ @staticmethod
+ def utc_to_local(utc_dt):
+ """Convert UTC time to local time."""
+ # get integer timestamp to avoid precision lost
+ timestamp = calendar.timegm(utc_dt.timetuple())
+ local_dt = datetime.fromtimestamp(timestamp)
+ assert utc_dt.resolution >= timedelta(microseconds=1)
+ return local_dt.replace(microsecond=utc_dt.microsecond)
diff --git a/plugin.video.nfl.gamepass/resources/settings.xml b/plugin.video.nfl.gamepass/resources/settings.xml
index ee5512f..0b0a6a3 100644
--- a/plugin.video.nfl.gamepass/resources/settings.xml
+++ b/plugin.video.nfl.gamepass/resources/settings.xml
@@ -4,9 +4,10 @@
<setting id="password" type="text" label="30002" default="" option="hidden" visible="!eq(-1,)" enable="!eq(-1,)"/>
</category>
<category label="30030">
- <setting id="preferred_bitrate" type="select" label="30003" lvalues="30004|30005|30006|30007|30008|30009|30010|30011|30012" default="8"/>
+ <setting id="use_inputstream_adaptive" type="bool" label="30049" default="false"/>
+ <setting id="preferred_bitrate" type="select" label="30003" lvalues="30004|30005|30006|30007|30008|30009|30010|30011|30012" visible="eq(-1,false)" default="8"/>
<setting id="preferred_game_version" type="select" label="30013" lvalues="30014|30015|30012" default="0"/>
- <setting id="local_tz" type="enum" label="30020" lvalues="30026|30027|30028" default="0"/>
+ <setting id="time_notation" type="enum" label="30020" lvalues="30027|30028" default="1"/>
<setting id="hide_game_length" type="bool" label="30025" default="false"/>
</category>
<category label="30033">