Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion addon.xml
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<addon id="plugin.program.steam.library" name="Steam Library" version="0.6.3" provider-name="aanderse">
<addon id="plugin.program.steam.library" name="Steam Library" version="0.7.0" provider-name="aanderse">
<requires>
<import addon="xbmc.python" version="2.19.0" />
<import addon="script.module.requests" version="2.18.4" />
<import addon="script.module.requests-cache" version="0.4.13" />
<import addon="script.module.routing" version="0.2.0"/>
</requires>
<extension point="xbmc.python.pluginsource" library="addon.py">
Expand Down
101 changes: 101 additions & 0 deletions resources/arts.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import xbmc
import xbmcaddon

import os
from datetime import timedelta
import requests
import requests_cache

from util import log

__addon__ = xbmcaddon.Addon()
artFallbackEnabled = __addon__.getSetting("enable-art-fallback") == 'true' # Kodi stores boolean settings as strings
monthsBeforeArtsExpiration = int(__addon__.getSetting("arts-expire-after-months")) # Default is 2 months

# define the cache file to reside in the ..\Kodi\userdata\addon_data\(your addon)
addonUserDataFolder = xbmc.translatePath(__addon__.getAddonInfo('profile'))
ART_AVAILABILITY_CACHE_FILE = xbmc.translatePath(os.path.join(addonUserDataFolder, 'requests_cache_arts'))

cached_requests = requests_cache.core.CachedSession(ART_AVAILABILITY_CACHE_FILE, backend='sqlite',
expire_after= timedelta(weeks=4*monthsBeforeArtsExpiration),
allowable_methods=('HEAD',), allowable_codes=(200, 404),
old_data_on_error=True,
fast_save=True)
# Existing Steam art types urls, to format to format with appid / img_icon_path
STEAM_ARTS_TYPES = { # img_icon_path is provided by steam API to get the icon. https://developer.valvesoftware.com/wiki/Steam_Web_API#GetOwnedGames_.28v0001.29
'poster': 'http://cdn.akamai.steamstatic.com/steam/apps/{appid}/library_600x900.jpg', # Can return 404
'hero': 'http://cdn.akamai.steamstatic.com/steam/apps/{appid}/library_hero.jpg', # Can return 404
'header': 'http://cdn.akamai.steamstatic.com/steam/apps/{appid}/header.jpg',
'generated_bg': 'http://cdn.akamai.steamstatic.com/steam/apps/{appid}/page_bg_generated_v6b.jpg', # Auto generated background with a shade of blue.
'icon': 'https://steamcdn-a.akamaihd.net/steamcommunity/public/images/apps/{appid}/{img_icon_path}.jpg',
'clearlogo': 'http://cdn.akamai.steamstatic.com/steam/apps/{appid}/logo.png' # Can return 404
}

# Dictionary containing for each art type, a url for the art (to format with appid / img_icon_path afterwards), and a fallback art type.
# Having no fallback also means that the art url won't be tested
ARTS_ASSIGNMENTS = {
'poster': {'url': STEAM_ARTS_TYPES['poster'], 'fallback': 'landscape'},
'banner': {'url': STEAM_ARTS_TYPES['hero'], 'fallback': 'landscape'},
'fanart': {'url': STEAM_ARTS_TYPES['hero'], 'fallback': 'fanart1'},
'fanart1': {'url': STEAM_ARTS_TYPES['header'], 'fallback': None},
'fanart2': {'url': STEAM_ARTS_TYPES['generated_bg'], 'fallback': None}, # Multiple fanart https://kodi.wiki/view/Artwork_types#fanart.23
'landscape': {'url': STEAM_ARTS_TYPES['header'], 'fallback': None},
'thumb': {'url': STEAM_ARTS_TYPES['header'], 'fallback': None},
'icon': {'url': STEAM_ARTS_TYPES['icon'], 'fallback': None},
'clearlogo': {'url': STEAM_ARTS_TYPES['clearlogo'], 'fallback': None}
}


def is_art_url_available(url, timeout=2):
"""
Sends a HEAD request to check if an online resource is available. Uses a cache mechanism to speed things up or serve offline if a connection is unavailable.

:param url: url to check availability
:param timeout: timeout of the request in seconds. Default is 2
:return: boolean False if the status code is between 400&600 , True otherwise
"""
result = False
try:
response = cached_requests.head(url, timeout=timeout)
if not 400 <= response.status_code < 600: # We consider valid any status codes below 400 or above 600
result = True
except IOError:
result = False
return result


def resolve_art_url(art_type, appid, img_icon_path='', art_fallback_enabled=artFallbackEnabled):
"""
Resolve the art url of a specified game/app, for a given art type defined in the :const:`ARTS_DATA` dictionary.
Handles fallback to another art type if needed (ie the requested one is unavailable and fallback is enabled).

:param art_type: a valid art type, defined in :const:`ARTS_DATA`
:param appid: appid of the game/app we want to get the art for.
:param img_icon_path: A path provided by steam to get the icon art url. https://developer.valvesoftware.com/wiki/Steam_Web_API#GetOwnedGames_.28v0001.29
:param art_fallback_enabled: Whether to fall back to another art type if an art is unavailable. Defaults to the user addon settings, which default to true
:return: resolved art URL. Can be the URL of another available art if .
"""
valid_art_url = None
requested_art = ARTS_ASSIGNMENTS.get(art_type, None)

while valid_art_url is None and requested_art is not None: # If the current media type is defined and we did not find a valid url yet
art_url = requested_art.get('url').format(appid=appid, img_icon_path=img_icon_path) # We replace "{appid}" and "{img_icon_path}" in the url
fallback_art_type = requested_art.get("fallback", None)
if (not art_fallback_enabled) or (fallback_art_type is None) or is_art_url_available(art_url):
# If art fallback is disabled, or if there is no fallback defined, we directly assume the art url as valid.
# Otherwise, if art fallback is enabled and there is a fallback defined, we check if is_art_url_available before proceeding
valid_art_url = art_url
else: # If art fallback is enabled and art is not available, we set the current art data to the defined fallback, before retrying.
requested_art = ARTS_ASSIGNMENTS.get(fallback_art_type, None) # Art data will be None if the fallback_art_type does not exist in the art_urls dict

if valid_art_url is None: # If the previous loop could not find a valid media url among the defined art types
log("Issue resolving media {0} for app id {1}".format(art_type, appid))

return valid_art_url


def delete_cache():
"""
Deletes the cache containing the data about which art types are available or not
"""
os.remove(ART_AVAILABILITY_CACHE_FILE + ".sqlite")
29 changes: 25 additions & 4 deletions resources/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
import routing
import sys
import xbmcplugin

import arts
import registry
import steam
from util import *
Expand Down Expand Up @@ -114,13 +116,23 @@ def run(appid):
steam.run(__addon__.getSetting('steam-exe'), __addon__.getSetting('steam-args'), appid)


@plugin.route('/delete_cache')
def delete_cache():
arts.delete_cache()


def create_directory_items(app_entries):
"""
Creates a list item for each game/app entry provided

:param app_entries: array of game entries, with each entry containing at least keys : appid, name, img_icon_url, img_logo_url, playtime_forever
:returns: an array of list items of the game entries, formatted like so : [(url,listItem,bool),..]
"""

# We set the folder content to "movies" as the program/game contents are locked out of many useful views, such as posters, headers, and more.
xbmcplugin.setContent(plugin.handle, "movies")
# TODO setContent to games when more skins support this content type.

directory_items = []
for app_entry in app_entries:
appid = str(app_entry['appid'])
Expand All @@ -130,7 +142,8 @@ def create_directory_items(app_entries):
item = xbmcgui.ListItem(name)

item.addContextMenuItems([('Play', 'RunPlugin(' + run_url + ')'),
('Install', 'RunPlugin(' + plugin.url_for(install, appid=appid) + ')')])
('Install', 'RunPlugin(' + plugin.url_for(install, appid=appid) + ')')],
replaceItems=True) # Since we set the content type to "movies", default movie context elements may appear. We replace them.

art_dictionary = create_arts_dictionary(app_entry)
item.setArt(art_dictionary)
Expand All @@ -143,11 +156,19 @@ def create_directory_items(app_entries):
def create_arts_dictionary(app_entry):
"""
Creates a dictionary of arts keys and their associated links, for a given app entry.
:param app_entry: dictionary of app informations, containing at least the keys : appid, img_icon_url, img_logo_url
:param app_entry: dictionary of app information, containing at least the keys : appid, img_icon_url, img_logo_url
:return: dictionary of arts for the app.
"""
art_dictionary = {'thumb': 'http://cdn.akamai.steamstatic.com/steam/apps/' + str(app_entry['appid']) + '/header.jpg',
'fanart': 'http://cdn.akamai.steamstatic.com/steam/apps/' + str(app_entry['appid']) + '/page_bg_generated_v6b.jpg'}

appid = str(app_entry['appid'])
img_icon_url = app_entry['img_icon_url']
art_dictionary = {}

# Multiple fanart https://kodi.wiki/view/Artwork_types#fanart.23
SUPPORTED_ART_TYPES = ['poster', 'landscape', 'banner', 'clearlogo', 'thumb', 'fanart', 'fanart1', 'fanart2', 'icon']

for art_type in SUPPORTED_ART_TYPES:
art_dictionary[art_type] = arts.resolve_art_url(art_type, appid, img_icon_url)
return art_dictionary


Expand Down
3 changes: 3 additions & 0 deletions resources/settings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,7 @@
<setting id="steam-path" type="folder" label="Your Steam folder (contains registry.vdf)"/>
<setting id="steam-args" type="text" label="Arguments to pass to your Steam executable"/>
<setting id="version" type="text" label="Internal version number, do not modify" visible="false"/>
<setting id="enable-art-fallback" type="bool" default="true" label="Fallback to another art type if a game has missing art. First launch may be longer for large libraries."/>
<setting id="arts-expire-after-months" type="number" default="2" label="Number of months before expiration of the arts availability cache"/>
<setting id="delete-cache" type="action" label="Clean available games and arts cache" action="RunPlugin(plugin://plugin.program.steam.library/delete_cache)"/>
</settings>
8 changes: 5 additions & 3 deletions resources/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,17 @@ def log(msg, level=xbmc.LOGDEBUG):
xbmc.log('[%s] %s' % (__addon__.getAddonInfo('id'), msg), level=level)


def show_error(e, message):
def show_error(e, message, display_notification=True):
""" Displays an error message to the user and log the cause.

:type e: Exception
:param e: An exception object to add to the error log
:param message: An error message to display to the user
:param display_notification: boolean indication whether or not an error notification is shown in Kodi
"""
notify = xbmcgui.Dialog()
notify.notification('Error', message, xbmcgui.NOTIFICATION_ERROR)
if display_notification:
notify = xbmcgui.Dialog()
notify.notification('Error', message, xbmcgui.NOTIFICATION_ERROR)
log(str(e), xbmc.LOGERROR)


Expand Down