summaryrefslogtreecommitdiff
path: root/plugin.video.nfl.gamepass
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
parent2e27b8f706ca0b603b62ec172f7f278aaa21e3b4 (diff)
[plugin.video.nfl.gamepass] 0.11.0 (#1372)
Diffstat (limited to 'plugin.video.nfl.gamepass')
-rw-r--r--plugin.video.nfl.gamepass/README.md64
-rw-r--r--plugin.video.nfl.gamepass/addon.xml20
-rw-r--r--plugin.video.nfl.gamepass/changelog.txt10
-rw-r--r--plugin.video.nfl.gamepass/default.py379
-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
12 files changed, 639 insertions, 687 deletions
diff --git a/plugin.video.nfl.gamepass/README.md b/plugin.video.nfl.gamepass/README.md
index 861a333..c77ef93 100644
--- a/plugin.video.nfl.gamepass/README.md
+++ b/plugin.video.nfl.gamepass/README.md
@@ -1,5 +1,15 @@
+# NOTE #
+
+NFL Game Pass has split into two services: Game Pass Europe and Game Pass
+International.
+
+This addon currently only supports Game Pass Europe. However, we are very
+interested in supporting Game Pass International. If you're a Python developer
+with a Game Pass International subscription and are interested in helping out,
+check out issue #313. It'd be great to get those subscriptions working again.
+
# NFL Game Pass Kodi Plugin #
-**version 0.10.2 — Daunte Culpepper Edition**
+**version 0.11.0 — Jay Cutler Edition**
Before reading any further, please understand that this addon is unofficial and
is not endorsed or supported by the NFL in any way. Not all features are
@@ -12,37 +22,36 @@ request. Patches and (constructive) input are always welcome.
## Any Dependencies? ##
-This addon requires Kodi Krypton or later. The latest version supporting Jarvis
-was 0.9.2.
+This addon requires Kodi Krypton or later.
This addon is available from the official Kodi repository, and when installed
from there, all dependencies are installed automatically. However, if you're
installing directly from the source, make sure the following dependencies are
installed:
- * xmltodict (http://mirrors.kodi.tv/addons/jarvis/script.module.xmltodict/)
* Requests 2.x (http://mirrors.kodi.tv/addons/jarvis/script.module.requests/)
* m3u8 >= 0.2.10 (http://mirrors.kodi.tv/addons/jarvis/script.module.m3u8/)
* which needs iso8601 (http://mirrors.kodi.tv/addons/jarvis/script.module.iso8601/)
## What is NFL Game Pass? ##
-NFL Game Pass is website that allows those with subscriptions to watch NFL
-games. Archives of old games stretch back to 2009, coaches film (22 man view) is
-available, as is audio from each team's radio network. Overall, it is a sweet
-service offered by the NFL for those of us who must have our American Football
-fix.
+NFL Game Pass is service that allows those with subscriptions to watch NFL
+games. Live games, archives of old games, NFL TV shows, NFL Network, and coaches
+tape (22 man view) are available. Overall, it is a sweet service offered by the
+NFL for those of us who must have our American Football fix.
-## What is Game Pass Domestic? ##
+## What is Game Pass Europe? ##
-NFL Game Pass Domestic is the USA (and parts of Canada and UK) version of Game
-Pass, but the service is blacked out during live games. Previously it lacked
-other features, but with the mid-2015 revamping of their service, it seems the
-two services have mostly converged (though we have yet to find a side-by-side
-comparison).
+Game Pass Europe uses WPP/Bruin as its streaming provider(s), and is currently
+the only service this addon supports.
-As most/all of the current developers are located outside of the Domestic
-regions, testing, bug reports, and patches from Game Pass Domestic subscribers
-is most appreciated.
+## What is Game Pass International? ##
+
+Game Pass International uses NeuLion as its streaming partner.
+
+As all of the current developers are located outside of the "International"
+regions, testing, bug reports, and patches from Game Pass International
+subscribers is most appreciated. Please checkout issue #313 if you're interested
+in helping out.
## Why write a plugin for Kodi? ##
@@ -54,11 +63,12 @@ to watch a game, nothing else.
## What features are currently supported? ##
+With the 2017 split of Game Pass's service, this list is out of date.
+
By now, most core features are supported.
- * Archived games from 2011 to 2014 (both full and condensed)
+ * Archived games from 2016 forward
* Live games
- * Coaches Film (22 man view)
* NFL Network - Live
* A Football Life
* NFL Films Presents
@@ -72,21 +82,11 @@ By now, most core features are supported.
* NFL Total Access
Currently unsupported features:
- * Archived games prior to 2011
- * Alternate team audio
- * Coaches Show
+ * Coaches Film (22 man view)
+ * Game Pass International
## Release names ##
Want a release to be named after your player/coach of choice? Contribute to the
project in some way (code, art, debugging, beer, brazen — yet effective —
flattery, etc), and we'll gladly name a future release after them.
-
-## Roadmap ##
-
-A rough roadmap follows:
-
-* Continue work towards feature completeness
-* Stabilize Game Pass Domestic support
-* Testing
-* Code cleanup
diff --git a/plugin.video.nfl.gamepass/addon.xml b/plugin.video.nfl.gamepass/addon.xml
index 268012b..55f7f73 100644
--- a/plugin.video.nfl.gamepass/addon.xml
+++ b/plugin.video.nfl.gamepass/addon.xml
@@ -5,13 +5,12 @@
marooned on an old version. -->
<addon id="plugin.video.nfl.gamepass"
name="NFL Game Pass"
- version="0.10.2"
- provider-name="Alexqw,divingmule,BaumSchorle,eriksoderblom">
+ version="0.11.0"
+ provider-name="Alexqw,divingmule,BaumSchorle,eriksoderblom,kaileu,emilsvennesson">
<requires>
<import addon="xbmc.python" version="2.25.0"/>
<import addon="script.module.m3u8" version="0.2.10"/>
<import addon="script.module.requests" version="2.7.0"/>
- <import addon="script.module.xmltodict" version="0.9.0"/>
<import addon="xbmc.gui" version="5.12.0"/>
</requires>
<extension point="xbmc.python.script" library="default.py">
@@ -26,12 +25,15 @@
<source>https://github.com/aqw/xbmc-gamepass</source>
<forum></forum>
<disclaimer lang="en">This addon requires you to have a subscription to NFL Game Pass.[CR]Please note that these subscriptions are region restricted by the NFL.[CR]This addon is completely unofficial and is /not/ endorsed by the NFL in any way.</disclaimer>
- <news>2017.06.23 v0.10.2 -- Daunte Culpepper Edition
- + Temporarily fix addon crashing (emilsvennesson)
- + GamePass is moving to a new streaming partner. The old service will work
- + until July 31st. This fix points to the old service, and buys us time to
- + update everything for the new service.
- + Fix fanart not loading (emilsvennesson)
+ <news>2017.08.17 v0.11.0 -- Jay Cutler Edition
+ + NOTE: This addon currently does not support Game Pass International.
+ + NOTE: Game Pass Europe moved to a new streaming partner. Not all features
+ + are available yet or supported.
+ + Support for Game Pass Europe (kaileu and emilsvennesson)
+ + Updated Dutch translation (pyrocumulus)
+ + Optional support for inputstream.adaptive plugin, but (contrary to the
+ + name) no support yet for adaptive streaming (emilsvennesson)
+ + Lots of cleanup (emilsvennesson, aqw, pyrocumulus)
</news>
<assets>
<icon>resources/art/icon.png</icon>
diff --git a/plugin.video.nfl.gamepass/changelog.txt b/plugin.video.nfl.gamepass/changelog.txt
index 5385d57..55ee425 100644
--- a/plugin.video.nfl.gamepass/changelog.txt
+++ b/plugin.video.nfl.gamepass/changelog.txt
@@ -1,3 +1,13 @@
+2017.08.17 v0.11.0 -- Jay Cutler Edition
++ NOTE: This addon currently does not support Game Pass International.
++ NOTE: Game Pass Europe moved to a new streaming partner. Not all features
++ are available yet or supported.
++ Support for Game Pass Europe (kaileu and emilsvennesson)
++ Updated Dutch translation (pyrocumulus)
++ Optional support for inputstream.adaptive plugin (but, contrary to the name,
++ no support yet for adaptive streaming)
++ Lots of cleanup (emilsvennesson, aqw, pyrocumulus)
+
2017.06.23 v0.10.2 -- Daunte Culpepper Edition
+ Temporarily fix addon crashing (emilsvennesson)
+ GamePass is moving to a new streaming partner. The old service will work
diff --git a/plugin.video.nfl.gamepass/default.py b/plugin.video.nfl.gamepass/default.py
index 7950cf9..1b41231 100644
--- a/plugin.video.nfl.gamepass/default.py
+++ b/plugin.video.nfl.gamepass/default.py
@@ -2,12 +2,10 @@
"""
A Kodi addon/skin for NFL Game Pass
"""
-import calendar
-from datetime import datetime
-import os
import sys
-import time
+import json
from traceback import format_exc
+from datetime import timedelta
import xbmc
import xbmcaddon
@@ -22,10 +20,11 @@ ADDON_PATH = xbmc.translatePath(addon.getAddonInfo('path'))
ADDON_PROFILE = xbmc.translatePath(addon.getAddonInfo('profile'))
LOGGING_PREFIX = '[%s-%s]' % (addon.getAddonInfo('id'), addon.getAddonInfo('version'))
+busydialog = xbmcgui.DialogBusy()
+
if not xbmcvfs.exists(ADDON_PROFILE):
xbmcvfs.mkdir(ADDON_PROFILE)
-cookie_file = os.path.join(ADDON_PROFILE, 'cookie_file')
username = addon.getSetting('email')
password = addon.getSetting('password')
@@ -43,13 +42,21 @@ if addon.getSetting('proxy_enabled') == 'true':
if addon.getSetting('proxy_auth') == 'false':
proxy_config['auth'] = None
-gp = pigskin(proxy_config, cookie_file=cookie_file, debug=True)
+gp = pigskin(proxy_config, debug=True)
def addon_log(string):
msg = '%s: %s' % (LOGGING_PREFIX, string)
xbmc.log(msg=msg, level=xbmc.LOGDEBUG)
+def show_busy_dialog():
+ busydialog.create()
+
+def hide_busy_dialog():
+ try:
+ busydialog.close()
+ except RuntimeError,e:
+ addon_log('Error closing busy dialog: %s' % e.message)
class GamepassGUI(xbmcgui.WindowXML):
def __init__(self, *args, **kwargs):
@@ -71,6 +78,7 @@ class GamepassGUI(xbmcgui.WindowXML):
self.list_refill = False
self.focusId = 100
self.seasons_and_weeks = gp.get_seasons_and_weeks()
+ self.has_inputstream_adaptive = self.has_inputstream_adaptive()
xbmcgui.WindowXML.__init__(self, *args, **kwargs)
self.action_previous_menu = (9, 10, 92, 216, 247, 257, 275, 61467, 61448)
@@ -82,9 +90,6 @@ class GamepassGUI(xbmcgui.WindowXML):
self.games_list = self.window.getControl(230)
self.live_list = self.window.getControl(240)
- if gp.subscription == 'domestic':
- self.window.setProperty('domestic', 'true')
-
if self.list_refill:
self.season_list.reset()
self.season_list.addItems(self.season_items)
@@ -98,7 +103,7 @@ class GamepassGUI(xbmcgui.WindowXML):
self.window.setProperty('NW_clicked', 'false')
self.window.setProperty('GP_clicked', 'false')
- xbmc.executebuiltin("Dialog.Close(busydialog)")
+ hide_busy_dialog()
try:
self.setFocus(self.window.getControl(self.focusId))
@@ -127,7 +132,7 @@ class GamepassGUI(xbmcgui.WindowXML):
"""List seasons"""
self.season_items = []
# sort so that years are first (descending) followed by text
- for season in sorted(gp.nflnSeasons, key=lambda x: (x[0].isdigit(), x), reverse=True):
+ for season in sorted(gp.nfln_seasons, key=lambda x: (x[0].isdigit(), x), reverse=True):
listitem = xbmcgui.ListItem(season)
self.season_items.append(listitem)
@@ -146,128 +151,71 @@ class GamepassGUI(xbmcgui.WindowXML):
def display_weeks_games(self):
"""Show games for a given season/week"""
self.games_items = []
- games = gp.get_weeks_games(self.selected_season, self.selected_week)
+ games = gp.get_weeks_games(self.selected_season, self.selected_season_type, self.selected_week)
+ for game in games:
+ game_id = '{0}-{1}-{2}'.format(game['visitorNickName'].lower(), game['homeNickName'].lower(), str(game['gameId']))
+ game_name_shrt = '[B]%s[/B] at [B]%s[/B]' % (game['visitorNickName'], game['homeNickName'])
+ game_name_full = '[B]%s %s[/B] at [B]%s %s[/B]' % (game['visitorCityState'], game['visitorNickName'], game['homeCityState'], game['homeNickName'])
+ listitem = xbmcgui.ListItem(game_name_shrt, game_name_full)
+
+ listitem.setProperty('is_game', 'true')
+ listitem.setProperty('is_show', 'false')
+
+ if game['phase'] == 'FINAL' or game['phase'] == 'FINAL_OVERTIME':
+ # show game duration only if user wants to see it
+ if addon.getSetting('hide_game_length') == 'false':
+ game_info = '%s [CR] Duration: %s' % (game['phase'], str(timedelta(seconds=int(float(game['video']['videoDuration'])))))
+ else:
+ game_info = game['phase']
+ else:
+ if addon.getSetting('time_notation') == '0': # 12-hour clock
+ datetime_format = '%A, %b %d - %I:%M %p'
+ else: # 24-hour clock
+ datetime_format = '%A, %b %d - %H:%M'
- date_time_format = '%Y-%m-%dT%H:%M:%S.000'
- if games:
- for game in games:
- if game['homeTeam']['id'] is None: # sometimes the first item is empty
- continue
+ datetime_obj = gp.parse_datetime(game['gameDateTimeUtc'], True)
+ game_info = datetime_obj.strftime(datetime_format)
- game_info = ''
- game_id = game['id']
- game_versions = []
+ if game['videoStatus'] == 'SCHEDULED':
+ isPlayable = 'false'
+ isBlackedOut = 'false'
+ elif game['videoStatus'] == 'LIVE':
+ game_info += '[CR]» Live «'
+ video_id = str(game['video']['videoId'])
isPlayable = 'true'
isBlackedOut = 'false'
- home_team = game['homeTeam']
- away_team = game['awayTeam']
-
- # Pro-bowl doesn't have a team "name" only a team city, which is the
- # team name... wtf
- if game['homeTeam']['name'] is None:
- game_name_shrt = '[B]%s[/B] at [B]%s[/B]' % (away_team['city'], home_team['city'])
- game_name_full = game_name_shrt
- else:
- game_name_shrt = '[B]%s[/B] at [B]%s[/B]' % (away_team['name'], home_team['name'])
- game_name_full = '[B]%s %s[/B] at [B]%s %s[/B]' % (away_team['city'], away_team['name'], home_team['city'], home_team['name'])
-
- for key, value in {'Condensed': 'condensedId', 'Full': 'programId'}.items():
- if value in game:
- game_versions.append(key)
-
- if 'isLive' in game:
- game_versions.append('Live')
-
- if 'gameEndTimeGMT' in game:
- # Show game duration only if user wants to see it
- if addon.getSetting('hide_game_length') == 'false':
- try:
- start_time = datetime(*(time.strptime(game['gameTimeGMT'], date_time_format)[0:6]))
- end_time = datetime(*(time.strptime(game['gameEndTimeGMT'], date_time_format)[0:6]))
- game_info = 'Final [CR] Duration: %s' % time.strftime('%H:%M:%S', time.gmtime((end_time - start_time).seconds))
- except:
- addon_log(format_exc())
- if 'result' in game:
- game_info = 'Final'
- else:
- game_info = 'Final'
- else:
- if 'isLive' in game:
- game_info = '» Live «'
-
- try:
- if addon.getSetting('local_tz') == '0': # don't localize
- game_datetime = datetime(*(time.strptime(game['date'], date_time_format)[0:6]))
- game_info = game_datetime.strftime('%A, %b %d - %I:%M %p')
- else:
- game_gmt = time.strptime(game['gameTimeGMT'], date_time_format)
- secs = calendar.timegm(game_gmt)
- game_local = time.localtime(secs)
-
- if addon.getSetting('local_tz') == '1': # localize and use 12-hour clock
- game_info = time.strftime('%A, %b %d - %I:%M %p', game_local)
- else: # localize and use 24-hour clock
- game_info = time.strftime('%A, %b %d - %H:%M', game_local)
- except: # all else fails, just use their raw date value
- game_datetime = game['date'].split('T')
- game_info = game_datetime[0] + '[CR]' + game_datetime[1].split('.')[0] + ' ET'
-
- if 'hasProgram' not in game: # if subscription doesn't allow
- isPlayable = 'false'
- game_name_full = self.coloring(game_name_full, "disabled")
- game_name_shrt = self.coloring(game_name_shrt, "disabled")
- game_info = self.coloring(game_info, "disabled-info")
-
- try:
- if game['blocked'] == 'true':
- isPlayable = 'false'
- isBlackedOut = 'true'
- game_info = '» Blacked Out «'
- game_name_full = self.coloring(game_name_full, "disabled")
- game_name_shrt = self.coloring(game_name_shrt, "disabled")
- game_info = self.coloring(game_info, "disabled-info")
- except KeyError:
- pass
-
- listitem = xbmcgui.ListItem(game_name_shrt, game_name_full)
- listitem.setProperty('away_thumb', 'http://i.nflcdn.com/static/site/7.4/img/logos/teams-matte-144x96/%s.png' % away_team['id'])
- listitem.setProperty('home_thumb', 'http://i.nflcdn.com/static/site/7.4/img/logos/teams-matte-144x96/%s.png' % home_team['id'])
- listitem.setProperty('game_info', game_info)
- listitem.setProperty('is_game', 'true')
- listitem.setProperty('is_show', 'false')
- listitem.setProperty('isPlayable', isPlayable)
- listitem.setProperty('isBlackedOut', isBlackedOut)
- listitem.setProperty('game_id', game_id)
- listitem.setProperty('game_date', game['date'].split('T')[0])
- listitem.setProperty('game_versions', ' '.join(game_versions))
- self.games_items.append(listitem)
-
- self.games_list.addItems(self.games_items)
-
- else:
- dialog = xbmcgui.Dialog()
- dialog.ok(language(30021), language(30046))
+ listitem.setProperty('video_id', video_id)
+ listitem.setProperty('game_versions', 'Live')
+ else: # ONDEMAND
+ video_id = str(game['video']['videoId'])
+ isPlayable = 'true'
+ isBlackedOut = 'false'
+ listitem.setProperty('video_id', video_id)
+
+ listitem.setProperty('isPlayable', isPlayable)
+ listitem.setProperty('isBlackedOut', isBlackedOut)
+ listitem.setProperty('game_id', game_id)
+ listitem.setProperty('game_info', game_info)
+ listitem.setProperty('away_thumb', 'http://i.nflcdn.com/static/site/7.4/img/logos/teams-matte-144x96/%s.png' % game['visitorTeamAbbr'])
+ listitem.setProperty('home_thumb', 'http://i.nflcdn.com/static/site/7.4/img/logos/teams-matte-144x96/%s.png' % game['homeTeamAbbr'])
+ self.games_items.append(listitem)
+ self.games_list.addItems(self.games_items)
def display_seasons_weeks(self):
"""List weeks for a given season"""
- weeks = self.seasons_and_weeks[self.selected_season]
+ weeks_dict = self.seasons_and_weeks[self.selected_season]
- for week_code, week in sorted(weeks.iteritems()):
+ for week in weeks_dict:
+ if week['week_name'] == 'p':
+ title = language(30047).format(week['week_number'])
+ elif week['week_name'] == 'week':
+ title = language(30048).format(week['week_number'])
+ else:
+ title = week['week_name'].upper()
future = 'false'
- try:
- # convert EST to GMT by adding 6 hours
- week_date = week['@start'] + ' 06:00'
- # avoid super annoying bug http://forum.kodi.tv/showthread.php?tid=112916
- week_datetime = datetime(*(time.strptime(week_date, '%Y%m%d %H:%M')[0:6]))
- now_datetime = datetime.utcnow()
-
- if week_datetime > now_datetime:
- future = 'true'
- except KeyError: # some old seasons don't provide week dates
- pass
-
- listitem = xbmcgui.ListItem(week['@label'].title())
- listitem.setProperty('week_code', week_code)
+ listitem = xbmcgui.ListItem(title)
+ listitem.setProperty('week', week['week_number'])
+ listitem.setProperty('season_type', week['season_type'])
listitem.setProperty('future', future)
self.weeks_items.append(listitem)
self.weeks_list.addItems(self.weeks_items)
@@ -275,29 +223,31 @@ class GamepassGUI(xbmcgui.WindowXML):
def display_shows_episodes(self, show_name, season):
"""Show episodes for a given season/show"""
self.games_items = []
- items = gp.get_shows_episodes(show_name, season)
+ episodes = gp.get_shows_episodes(show_name, season)
- for i in items:
+ for episode in episodes:
try:
listitem = xbmcgui.ListItem('[B]%s[/B]' % show_name)
- listitem.setProperty('game_info', i['name'])
- listitem.setProperty('away_thumb', gp.image_url + i['image'])
- listitem.setProperty('url', i['publishPoint'])
- listitem.setProperty('id', i['id'])
- listitem.setProperty('type', i['type'])
+ listitem.setProperty('game_info', episode['title'])
+ listitem.setProperty('id', episode['videoId'])
listitem.setProperty('is_game', 'false')
listitem.setProperty('is_show', 'true')
listitem.setProperty('isPlayable', 'true')
self.games_items.append(listitem)
except:
addon_log('Exception adding archive directory: %s' % format_exc())
- addon_log('Directory name: %s' % i['name'])
+ addon_log('Directory name: %s' % i['title'])
self.games_list.addItems(self.games_items)
def play_url(self, url):
- xbmc.executebuiltin("Dialog.Close(busydialog)")
+ hide_busy_dialog()
self.list_refill = True
- xbmc.Player().play(url)
+ playitem = xbmcgui.ListItem(path=url)
+ if self.has_inputstream_adaptive and addon.getSetting('use_inputstream_adaptive') == 'true':
+ playitem.setProperty('inputstreamaddon', 'inputstream.adaptive')
+ playitem.setProperty('inputstream.adaptive.manifest_type', 'hls')
+ playitem.setProperty('inputstream.adaptive.stream_headers', url.split('|')[1])
+ xbmc.Player().play(item=url, listitem=playitem)
def init(self, level):
if level == 'season':
@@ -334,9 +284,9 @@ class GamepassGUI(xbmcgui.WindowXML):
"""
options = []
for bitrate in bitrates:
- options.append(bitrate + ' Kbps')
+ options.append(str(bitrate) + ' Kbps')
dialog = xbmcgui.Dialog()
- xbmc.executebuiltin("Dialog.Close(busydialog)")
+ hide_busy_dialog()
ret = dialog.select(language(30003), options)
if ret > -1:
return bitrates[ret]
@@ -346,7 +296,7 @@ class GamepassGUI(xbmcgui.WindowXML):
def select_bitrate(self, manifest_bitrates=None):
"""Returns a bitrate, while honoring the user's /preference/."""
bitrate_setting = int(addon.getSetting('preferred_bitrate'))
- bitrate_values = ['4500', '3000', '2400', '1600', '1200', '800', '400']
+ bitrate_values = ['3671533', '2394274', '1577316', '1117771', '760027', '555799', '402512']
highest = False
preferred_bitrate = None
@@ -385,7 +335,7 @@ class GamepassGUI(xbmcgui.WindowXML):
if 'Coach' in game_versions:
versions.append(language(30032))
dialog = xbmcgui.Dialog()
- xbmc.executebuiltin("Dialog.Close(busydialog)")
+ hide_busy_dialog()
preferred_version = dialog.select(language(30016), versions)
if preferred_version == 1 and 'Condensed' in game_versions:
@@ -400,6 +350,45 @@ class GamepassGUI(xbmcgui.WindowXML):
else:
return None
+ def has_inputstream_adaptive(self):
+ """Checks if InputStream Adaptive is installed and enabled."""
+ payload = {
+ 'jsonrpc': '2.0',
+ 'id': 1,
+ 'method': 'Addons.GetAddonDetails',
+ 'params': {
+ 'addonid': 'inputstream.adaptive',
+ 'properties': ['enabled']
+ }
+ }
+ response = xbmc.executeJSONRPC(json.dumps(payload))
+ data = json.loads(response)
+ if 'error' not in data and data['result']['addon']['enabled']:
+ addon_log('InputStream Adaptive is installed and enabled.')
+ return True
+ else:
+ addon_log('InputStream Adaptive is not installed and/or enabled.')
+ if addon.getSetting('use_inputstream_adaptive') == 'true':
+ addon_log('Disabling InputStream Adaptive.')
+ addon.setSetting('use_inputstream_adaptive', 'false') # reset setting
+ return False
+
+ def select_stream_url(self, streams):
+ """Determine which stream URL to use from the dict."""
+ if streams:
+ if addon.getSetting('use_inputstream_adaptive') == 'true' and self.has_inputstream_adaptive:
+ stream_url = streams['manifest_url']
+ else:
+ bitrate = self.select_bitrate(streams['bitrates'].keys())
+ if bitrate:
+ stream_url = streams['bitrates'][bitrate]
+ else: # bitrate dialog was canceled
+ return None
+ return stream_url
+ else:
+ addon_log('streams dictionary was empty.')
+ return False
+
def onFocus(self, controlId): # pylint: disable=invalid-name
# save currently focused list
if controlId in [210, 220, 230, 240]:
@@ -407,7 +396,7 @@ class GamepassGUI(xbmcgui.WindowXML):
def onClick(self, controlId): # pylint: disable=invalid-name
try:
- xbmc.executebuiltin("ActivateWindow(busydialog)")
+ show_busy_dialog()
if controlId in [110, 120, 130]:
self.games_list.reset()
self.weeks_list.reset()
@@ -427,8 +416,9 @@ class GamepassGUI(xbmcgui.WindowXML):
# display games of current week for usability purposes
cur_s_w = gp.get_current_season_and_week()
- self.selected_season = cur_s_w.keys()[0]
- self.selected_week = cur_s_w.values()[0]
+ self.selected_season = cur_s_w['season']
+ self.selected_season_type = cur_s_w['season_type']
+ self.selected_week = cur_s_w['week']
self.display_seasons()
try:
@@ -451,7 +441,7 @@ class GamepassGUI(xbmcgui.WindowXML):
self.live_list.addItems(self.live_items)
self.display_nfln_seasons()
- xbmc.executebuiltin("Dialog.Close(busydialog)")
+ hide_busy_dialog()
return
if self.main_selection == 'GamePass':
@@ -462,7 +452,8 @@ class GamepassGUI(xbmcgui.WindowXML):
self.display_seasons_weeks()
elif controlId == 220: # week is clicked
self.init('week/show')
- self.selected_week = self.weeks_list.getSelectedItem().getProperty('week_code')
+ self.selected_week = self.weeks_list.getSelectedItem().getProperty('week')
+ self.selected_season_type = self.weeks_list.getSelectedItem().getProperty('season_type')
self.display_weeks_games()
elif controlId == 230: # game is clicked
@@ -470,6 +461,7 @@ class GamepassGUI(xbmcgui.WindowXML):
if selectedGame.getProperty('isPlayable') == 'true':
self.init('game/episode')
game_id = selectedGame.getProperty('game_id')
+ video_id = selectedGame.getProperty('video_id')
game_versions = selectedGame.getProperty('game_versions')
if 'Live' in game_versions:
@@ -480,37 +472,28 @@ class GamepassGUI(xbmcgui.WindowXML):
else:
game_version = 'live'
else:
- # Check for coaches film availability
- if gp.check_for_coachestape(game_id, self.selected_season):
+ # check for coaches film availability
+ if gp.has_coaches_tape(game_id, self.selected_season):
game_versions = game_versions + ' Coach'
+ coach_id = gp.has_coaches_tape(game_id, self.selected_season)
+ # check for condensed film availability
+ if gp.has_condensed_game(game_id, self.selected_season):
+ game_versions = game_versions + ' Condensed'
+ condensed_id = gp.has_condensed_game(game_id, self.selected_season)
game_version = self.select_version(game_versions)
if game_version:
- if game_version == 'coach':
- xbmc.executebuiltin("ActivateWindow(busydialog)")
- coachesItems = []
- game_date = selectedGame.getProperty('game_date').replace('-', '/')
- self.playBackStop = False
-
- play_stream = gp.get_coaches_url(game_id, game_date, 'dummy')
- plays = gp.get_coaches_playIDs(game_id, self.selected_season)
- for playID in sorted(plays, key=int):
- cf_url = str(play_stream).replace('dummy', playID)
- item = xbmcgui.ListItem(plays[playID])
- item.setProperty('url', cf_url)
- coachesItems.append(item)
-
- self.list_refill = True
- xbmc.executebuiltin("Dialog.Close(busydialog)")
- coachGui = CoachesFilmGUI('script-gamepass-coach.xml', ADDON_PATH, plays=coachesItems)
- coachGui.doModal()
- del coachGui
+ if game_version == 'condensed':
+ stream_url = self.select_stream_url(gp.get_stream(condensed_id, 'game', username=username))
+ elif game_version == 'coach':
+ stream_url = self.select_stream_url(gp.get_stream(coach_id, 'game', username=username))
else:
- game_streams = gp.get_publishpoint_streams(game_id, 'game', game_version)
- bitrate = self.select_bitrate(game_streams.keys())
- if bitrate:
- game_url = game_streams[bitrate]
- self.play_url(game_url)
+ stream_url = self.select_stream_url(gp.get_stream(video_id, 'game', username=username))
+ if stream_url:
+ self.play_url(stream_url)
+ elif stream_url is False:
+ dialog = xbmcgui.Dialog()
+ dialog.ok(language(30043), language(30045))
elif self.main_selection == 'NFL Network':
if controlId == 210: # season is clicked
@@ -526,41 +509,31 @@ class GamepassGUI(xbmcgui.WindowXML):
elif controlId == 230: # episode is clicked
self.init('game/episode')
video_id = self.games_list.getSelectedItem().getProperty('id')
- video_streams = gp.get_publishpoint_streams(video_id, 'video')
- if video_streams:
- addon_log('Video-Streams: %s' % video_streams)
- bitrate = self.select_bitrate(video_streams.keys())
- if bitrate:
- video_url = video_streams[bitrate]
- self.play_url(video_url)
- else:
+ episode_stream_url = self.select_stream_url(gp.get_stream(video_id, 'video', username=username))
+ if episode_stream_url:
+ self.play_url(episode_stream_url)
+ elif episode_stream_url is False:
dialog = xbmcgui.Dialog()
dialog.ok(language(30043), language(30045))
elif controlId == 240: # Live content (though not games)
show_name = self.live_list.getSelectedItem().getLabel()
if show_name == 'NFL RedZone - Live':
- rz_live_streams = gp.get_publishpoint_streams('redzone')
- if rz_live_streams:
- bitrate = self.select_bitrate(rz_live_streams.keys())
- if bitrate:
- rz_live_url = rz_live_streams[bitrate]
- self.play_url(rz_live_url)
- else:
+ rz_stream_url = self.select_stream_url(gp.get_stream('redzone', username=username))
+ if rz_stream_url:
+ self.play_url(rz_stream_url)
+ elif rz_stream_url is False:
dialog = xbmcgui.Dialog()
dialog.ok(language(30043), language(30045))
elif show_name == 'NFL Network - Live':
- nw_live_streams = gp.get_publishpoint_streams('nfl_network')
- if nw_live_streams:
- bitrate = self.select_bitrate(nw_live_streams.keys())
- if bitrate:
- nw_live_url = nw_live_streams[bitrate]
- self.play_url(nw_live_url)
- else:
+ nfln_live_stream = self.select_stream_url(gp.get_stream('nfl_network', username=username))
+ if nfln_live_stream:
+ self.play_url(nfln_live_stream)
+ elif nfln_live_stream is False:
dialog = xbmcgui.Dialog()
dialog.ok(language(30043), language(30045))
- xbmc.executebuiltin("Dialog.Close(busydialog)")
+ hide_busy_dialog()
except Exception: # catch anything that might fail
- xbmc.executebuiltin("Dialog.Close(busydialog)")
+ hide_busy_dialog()
addon_log(format_exc())
dialog = xbmcgui.Dialog()
@@ -573,7 +546,7 @@ class GamepassGUI(xbmcgui.WindowXML):
class CoachesFilmGUI(xbmcgui.WindowXML):
- def __init__(self, xmlFilename, scriptPath, plays, defaultSkin="Default", defaultRes="720p"): # pylint: disable=invalid-name
+ def __init__(self, xmlFilename, scriptPath, plays, defaultSkin='Default', defaultRes='720p'): # pylint: disable=invalid-name
self.playsList = None
self.playsItems = plays
@@ -590,7 +563,7 @@ class CoachesFilmGUI(xbmcgui.WindowXML):
self.playsList.addItems(self.playsItems)
self.setFocus(self.playsList)
url = self.playsList.getListItem(0).getProperty('url')
- xbmc.executebuiltin("Dialog.Close(busydialog)")
+ hide_busy_dialog()
xbmc.executebuiltin('PlayMedia(%s,False,1)' % url)
def onClick(self, controlId): # pylint: disable=invalid-name
@@ -598,22 +571,18 @@ class CoachesFilmGUI(xbmcgui.WindowXML):
url = self.playsList.getSelectedItem().getProperty('url')
xbmc.executebuiltin('PlayMedia(%s,False,1)' % url)
-if __name__ == "__main__":
+if __name__ == '__main__':
addon_log('script starting')
- xbmc.executebuiltin("Dialog.Close(busydialog)")
+ hide_busy_dialog()
try:
gp.login(username, password)
- except gp.LoginFailure as error:
+ except gp.GamePassError as error:
dialog = xbmcgui.Dialog()
- if error.value == 'Game Pass Domestic Blackout':
- addon_log('Game Pass Domestic is in blackout.')
- dialog.ok(language(30021),
- language(30022))
+ if error.value == 'error_unauthorised':
+ dialog.ok(language(30021), language(30023))
else:
- addon_log('login failed')
- dialog.ok(language(30021),
- language(30023))
+ dialog.ok(language(30021), error.value)
sys.exit(0)
except:
addon_log(format_exc())
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">