Skip to content

Commit b3af194

Browse files
authored
Merge pull request #20 from AmineI/AmineI-More-Arts
More arts types
2 parents 252e4e8 + 02cbcb3 commit b3af194

5 files changed

Lines changed: 136 additions & 8 deletions

File tree

addon.xml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
2-
<addon id="plugin.program.steam.library" name="Steam Library" version="0.6.3" provider-name="aanderse">
2+
<addon id="plugin.program.steam.library" name="Steam Library" version="0.7.0" provider-name="aanderse">
33
<requires>
44
<import addon="xbmc.python" version="2.19.0" />
55
<import addon="script.module.requests" version="2.18.4" />
6+
<import addon="script.module.requests-cache" version="0.4.13" />
67
<import addon="script.module.routing" version="0.2.0"/>
78
</requires>
89
<extension point="xbmc.python.pluginsource" library="addon.py">

resources/arts.py

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
import xbmc
2+
import xbmcaddon
3+
4+
import os
5+
from datetime import timedelta
6+
import requests
7+
import requests_cache
8+
9+
from util import log
10+
11+
__addon__ = xbmcaddon.Addon()
12+
artFallbackEnabled = __addon__.getSetting("enable-art-fallback") == 'true' # Kodi stores boolean settings as strings
13+
monthsBeforeArtsExpiration = int(__addon__.getSetting("arts-expire-after-months")) # Default is 2 months
14+
15+
# define the cache file to reside in the ..\Kodi\userdata\addon_data\(your addon)
16+
addonUserDataFolder = xbmc.translatePath(__addon__.getAddonInfo('profile'))
17+
ART_AVAILABILITY_CACHE_FILE = xbmc.translatePath(os.path.join(addonUserDataFolder, 'requests_cache_arts'))
18+
19+
cached_requests = requests_cache.core.CachedSession(ART_AVAILABILITY_CACHE_FILE, backend='sqlite',
20+
expire_after= timedelta(weeks=4*monthsBeforeArtsExpiration),
21+
allowable_methods=('HEAD',), allowable_codes=(200, 404),
22+
old_data_on_error=True,
23+
fast_save=True)
24+
# Existing Steam art types urls, to format to format with appid / img_icon_path
25+
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
26+
'poster': 'http://cdn.akamai.steamstatic.com/steam/apps/{appid}/library_600x900.jpg', # Can return 404
27+
'hero': 'http://cdn.akamai.steamstatic.com/steam/apps/{appid}/library_hero.jpg', # Can return 404
28+
'header': 'http://cdn.akamai.steamstatic.com/steam/apps/{appid}/header.jpg',
29+
'generated_bg': 'http://cdn.akamai.steamstatic.com/steam/apps/{appid}/page_bg_generated_v6b.jpg', # Auto generated background with a shade of blue.
30+
'icon': 'https://steamcdn-a.akamaihd.net/steamcommunity/public/images/apps/{appid}/{img_icon_path}.jpg',
31+
'clearlogo': 'http://cdn.akamai.steamstatic.com/steam/apps/{appid}/logo.png' # Can return 404
32+
}
33+
34+
# Dictionary containing for each art type, a url for the art (to format with appid / img_icon_path afterwards), and a fallback art type.
35+
# Having no fallback also means that the art url won't be tested
36+
ARTS_ASSIGNMENTS = {
37+
'poster': {'url': STEAM_ARTS_TYPES['poster'], 'fallback': 'landscape'},
38+
'banner': {'url': STEAM_ARTS_TYPES['hero'], 'fallback': 'landscape'},
39+
'fanart': {'url': STEAM_ARTS_TYPES['hero'], 'fallback': 'fanart1'},
40+
'fanart1': {'url': STEAM_ARTS_TYPES['header'], 'fallback': None},
41+
'fanart2': {'url': STEAM_ARTS_TYPES['generated_bg'], 'fallback': None}, # Multiple fanart https://kodi.wiki/view/Artwork_types#fanart.23
42+
'landscape': {'url': STEAM_ARTS_TYPES['header'], 'fallback': None},
43+
'thumb': {'url': STEAM_ARTS_TYPES['header'], 'fallback': None},
44+
'icon': {'url': STEAM_ARTS_TYPES['icon'], 'fallback': None},
45+
'clearlogo': {'url': STEAM_ARTS_TYPES['clearlogo'], 'fallback': None}
46+
}
47+
48+
49+
def is_art_url_available(url, timeout=2):
50+
"""
51+
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.
52+
53+
:param url: url to check availability
54+
:param timeout: timeout of the request in seconds. Default is 2
55+
:return: boolean False if the status code is between 400&600 , True otherwise
56+
"""
57+
result = False
58+
try:
59+
response = cached_requests.head(url, timeout=timeout)
60+
if not 400 <= response.status_code < 600: # We consider valid any status codes below 400 or above 600
61+
result = True
62+
except IOError:
63+
result = False
64+
return result
65+
66+
67+
def resolve_art_url(art_type, appid, img_icon_path='', art_fallback_enabled=artFallbackEnabled):
68+
"""
69+
Resolve the art url of a specified game/app, for a given art type defined in the :const:`ARTS_DATA` dictionary.
70+
Handles fallback to another art type if needed (ie the requested one is unavailable and fallback is enabled).
71+
72+
:param art_type: a valid art type, defined in :const:`ARTS_DATA`
73+
:param appid: appid of the game/app we want to get the art for.
74+
: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
75+
: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
76+
:return: resolved art URL. Can be the URL of another available art if .
77+
"""
78+
valid_art_url = None
79+
requested_art = ARTS_ASSIGNMENTS.get(art_type, None)
80+
81+
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
82+
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
83+
fallback_art_type = requested_art.get("fallback", None)
84+
if (not art_fallback_enabled) or (fallback_art_type is None) or is_art_url_available(art_url):
85+
# If art fallback is disabled, or if there is no fallback defined, we directly assume the art url as valid.
86+
# Otherwise, if art fallback is enabled and there is a fallback defined, we check if is_art_url_available before proceeding
87+
valid_art_url = art_url
88+
else: # If art fallback is enabled and art is not available, we set the current art data to the defined fallback, before retrying.
89+
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
90+
91+
if valid_art_url is None: # If the previous loop could not find a valid media url among the defined art types
92+
log("Issue resolving media {0} for app id {1}".format(art_type, appid))
93+
94+
return valid_art_url
95+
96+
97+
def delete_cache():
98+
"""
99+
Deletes the cache containing the data about which art types are available or not
100+
"""
101+
os.remove(ART_AVAILABILITY_CACHE_FILE + ".sqlite")

resources/main.py

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
import routing
33
import sys
44
import xbmcplugin
5+
6+
import arts
57
import registry
68
import steam
79
from util import *
@@ -114,13 +116,23 @@ def run(appid):
114116
steam.run(__addon__.getSetting('steam-exe'), __addon__.getSetting('steam-args'), appid)
115117

116118

119+
@plugin.route('/delete_cache')
120+
def delete_cache():
121+
arts.delete_cache()
122+
123+
117124
def create_directory_items(app_entries):
118125
"""
119126
Creates a list item for each game/app entry provided
120127
121128
:param app_entries: array of game entries, with each entry containing at least keys : appid, name, img_icon_url, img_logo_url, playtime_forever
122129
:returns: an array of list items of the game entries, formatted like so : [(url,listItem,bool),..]
123130
"""
131+
132+
# 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.
133+
xbmcplugin.setContent(plugin.handle, "movies")
134+
# TODO setContent to games when more skins support this content type.
135+
124136
directory_items = []
125137
for app_entry in app_entries:
126138
appid = str(app_entry['appid'])
@@ -130,7 +142,8 @@ def create_directory_items(app_entries):
130142
item = xbmcgui.ListItem(name)
131143

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

135148
art_dictionary = create_arts_dictionary(app_entry)
136149
item.setArt(art_dictionary)
@@ -143,11 +156,19 @@ def create_directory_items(app_entries):
143156
def create_arts_dictionary(app_entry):
144157
"""
145158
Creates a dictionary of arts keys and their associated links, for a given app entry.
146-
:param app_entry: dictionary of app informations, containing at least the keys : appid, img_icon_url, img_logo_url
159+
:param app_entry: dictionary of app information, containing at least the keys : appid, img_icon_url, img_logo_url
147160
:return: dictionary of arts for the app.
148161
"""
149-
art_dictionary = {'thumb': 'http://cdn.akamai.steamstatic.com/steam/apps/' + str(app_entry['appid']) + '/header.jpg',
150-
'fanart': 'http://cdn.akamai.steamstatic.com/steam/apps/' + str(app_entry['appid']) + '/page_bg_generated_v6b.jpg'}
162+
163+
appid = str(app_entry['appid'])
164+
img_icon_url = app_entry['img_icon_url']
165+
art_dictionary = {}
166+
167+
# Multiple fanart https://kodi.wiki/view/Artwork_types#fanart.23
168+
SUPPORTED_ART_TYPES = ['poster', 'landscape', 'banner', 'clearlogo', 'thumb', 'fanart', 'fanart1', 'fanart2', 'icon']
169+
170+
for art_type in SUPPORTED_ART_TYPES:
171+
art_dictionary[art_type] = arts.resolve_art_url(art_type, appid, img_icon_url)
151172
return art_dictionary
152173

153174

resources/settings.xml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,7 @@
77
<setting id="steam-path" type="folder" label="Your Steam folder (contains registry.vdf)"/>
88
<setting id="steam-args" type="text" label="Arguments to pass to your Steam executable"/>
99
<setting id="version" type="text" label="Internal version number, do not modify" visible="false"/>
10+
<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."/>
11+
<setting id="arts-expire-after-months" type="number" default="2" label="Number of months before expiration of the arts availability cache"/>
12+
<setting id="delete-cache" type="action" label="Clean available games and arts cache" action="RunPlugin(plugin://plugin.program.steam.library/delete_cache)"/>
1013
</settings>

resources/util.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,17 @@ def log(msg, level=xbmc.LOGDEBUG):
1010
xbmc.log('[%s] %s' % (__addon__.getAddonInfo('id'), msg), level=level)
1111

1212

13-
def show_error(e, message):
13+
def show_error(e, message, display_notification=True):
1414
""" Displays an error message to the user and log the cause.
1515
1616
:type e: Exception
1717
:param e: An exception object to add to the error log
1818
:param message: An error message to display to the user
19+
:param display_notification: boolean indication whether or not an error notification is shown in Kodi
1920
"""
20-
notify = xbmcgui.Dialog()
21-
notify.notification('Error', message, xbmcgui.NOTIFICATION_ERROR)
21+
if display_notification:
22+
notify = xbmcgui.Dialog()
23+
notify.notification('Error', message, xbmcgui.NOTIFICATION_ERROR)
2224
log(str(e), xbmc.LOGERROR)
2325

2426

0 commit comments

Comments
 (0)