diff --git a/README.md b/README.md index ce28602..fce6d65 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,13 @@ -![Example of Ice](ice-example.png "Example") +![Example of Ice using thegamesdb.net boxart images](ice-example-new.png "Example") -##Ice +##Ice (TheGamesDB.net Addon) ###Description -The purpose of this project is to leverage Steam's Big Picture mode to turn it into an emulator frontend (similar to Hyperspin). It accomplishes this by creating folders in specified locations on the user's hard drive, then adding any ROMs that are placed in these folders to Steam as non-Steam games. Emulators are installed and configured by the user before Ice is run. +This fork adds a new provider to download images from thegamesdb.net +It also generates nicer coverimages if the coverimage doesnt fit steam banner size. + +Have fun with it. ###License @@ -24,6 +27,3 @@ Next, you will need to download all of Ice's dependencies. To do so, run `python Once all of that is finished, simply run `python -m ice` from the repository's root directory. -###Ice GUI - -A GUI for Ice is currently being developed, but is very far from being production ready. As of writing, it is basically non-functional. Do not attempt to use the GUI, doing so will only bring you pain and heartache. diff --git a/bin/Ice-TheGamesDB.zip b/bin/Ice-TheGamesDB.zip new file mode 100644 index 0000000..337e7ee Binary files /dev/null and b/bin/Ice-TheGamesDB.zip differ diff --git a/config.txt b/config.txt index 48f4bad..901652e 100644 --- a/config.txt +++ b/config.txt @@ -45,9 +45,11 @@ Userdata Directory= # a game of the same name. # * `consolegrid` - Hits the ConsoleGrid API looking for a game of that name. # +# * `thegamesdb` - Hits the TheGamesDB API looking for a game of that name. +# # Ice will ask these providers for images in order, stopping when it one is -# found. This means order matters, and `local, consolegrid` is not the same as -# `consolegrid, local`. +# found. This means order matters, and `local, consolegrid, thegamesdb` is not the same as +# `consolegrid, local, thegamesdb`. # -# The default is `local, consolegrid` -Providers=local, consolegrid \ No newline at end of file +# The default is `local, consolegrid, thegamesdb` +Providers=local, thegamesdb \ No newline at end of file diff --git a/ice-example-new.png b/ice-example-new.png new file mode 100644 index 0000000..15d2a13 Binary files /dev/null and b/ice-example-new.png differ diff --git a/ice/gridproviders/__init__.py b/ice/gridproviders/__init__.py index 1144c8d..45df17c 100644 --- a/ice/gridproviders/__init__.py +++ b/ice/gridproviders/__init__.py @@ -10,4 +10,5 @@ "consolegrid_provider", "grid_image_provider", "local_provider", + "thegamesdb_provider", ] diff --git a/ice/gridproviders/thegamesdb_provider.py b/ice/gridproviders/thegamesdb_provider.py new file mode 100644 index 0000000..1c50cfc --- /dev/null +++ b/ice/gridproviders/thegamesdb_provider.py @@ -0,0 +1,134 @@ +#!/usr/bin/env python +# encoding: utf-8 +""" +thegamesdb_provider.py +Created by Wolfgang Bergbauer on 2018-02-09. +Copyright (c) 2018 Wolfgang Bergbauer. All rights reserved. +""" +import requests + +import xml.etree.ElementTree + +import shutil + +import grid_image_provider + +from ice.logs import logger +from ice.steam_banner_creator import SteamBannerCreator + + +class TheGamesDBProvider(grid_image_provider.GridImageProvider): + + @staticmethod + def user_agent(): + return "Mozilla/5.0" + + @staticmethod + def api_url_findId(): + return "http://thegamesdb.net/api/GetGamesList.php?name=" + + @staticmethod + def api_url_getArt(): + return "http://thegamesdb.net/api/GetArt.php?id=" + + def http_get(self, url): + return requests.get(url, {'User-agent': self.user_agent()}) + + def searchTheGamesDb(self, rom): + url = self.api_url_findId() + str(rom.name) + try: + response = self.http_get(url) + body = response.content + gameList = xml.etree.ElementTree.fromstring(body) + foundImage = self.findImageForPlattformGame(gameList, rom) + return foundImage + except IOError as error: + logger.debug( + "There was an error contacting %s" % url + ) + + def getFileType(self, url): + return url[-4:] + + def download_image(self, url): + + filetype = self.getFileType(url) + logger.debug( + "Downloading %s \n Extrated filetype: %s " % (url, filetype) + ) + if filetype is not None and len(filetype) > 3: + + from tempfile import mkstemp + fd, path = mkstemp(suffix=filetype) + response = requests.get(url, stream=True) + with open(path, 'wb') as out_file: + shutil.copyfileobj(response.raw, out_file) + del response + return path + + def findArtWorkUrl(self, gameId): + url = self.api_url_getArt() + str(gameId) + try: + response = self.http_get(url) + body = response.content + xmlResult = xml.etree.ElementTree.fromstring(body) + baseImgUrl = xmlResult._children[0].text + imagesXml = xmlResult._children[1] + imgUrl = None + + # try to find a fanart + boxarts = imagesXml.findall("boxart") + frontBoxArt = self.findFrontBoxArt(boxarts) + + if frontBoxArt is not None: + imgUrl = frontBoxArt.text + else: + fanart = imagesXml.find("fanart") + if fanart is not None: + imgUrl = fanart.text + else: + if len(imagesXml._children) > 0: + # take any image which is available + imgUrl = imagesXml._children[0].text + + if imgUrl is not None: + return baseImgUrl + imgUrl + else: + return None + except IOError as error: + logger.debug( + "There was an error contacting %s" % url + ) + + def image_for_rom(self, rom): + imgUrl = self.searchTheGamesDb(rom) + if imgUrl is not None: + img = self.download_image(imgUrl) + SteamBannerCreator().convertToSteamBannerImage(img) + return img + + + def findFrontBoxArt(self, boxarts): + if boxarts is not None: + for boxart in boxarts: + if boxart.get('side') == "front": + return boxart + + def findImageForPlattformGame(self, gameList, rom): + romConsole = rom.console.fullname + foundgame = None + foundImage = None + for game in gameList: + gameConsole = game.find('Platform').text + if gameConsole == romConsole or gameConsole in romConsole or romConsole in gameConsole: + foundImage = self.findArtWorkUrl(game.find('id').text) + if foundImage is not None: + foundgame = game + break + + # If no suitable console was found, take the first one + if foundgame is None and len(gameList._children) > 0: + foundgame = gameList._children[0] + foundImage = self.findArtWorkUrl(foundgame.find('id').text) + + return foundImage diff --git a/ice/settings.py b/ice/settings.py index 442c5f4..454780b 100644 --- a/ice/settings.py +++ b/ice/settings.py @@ -5,6 +5,7 @@ import configuration import model import paths +from gridproviders.thegamesdb_provider import TheGamesDBProvider from logs import logger from gridproviders.combined_provider import CombinedProvider @@ -73,6 +74,7 @@ def image_provider(config): providerByName = { "local": LocalProvider, "consolegrid": ConsoleGridProvider, + "thegamesdb": TheGamesDBProvider, } normalize = lambda s: s.strip().lower() names = map(normalize, config.provider_spec.split(",")) diff --git a/ice/steam_banner_creator.py b/ice/steam_banner_creator.py new file mode 100644 index 0000000..0cf2ff3 --- /dev/null +++ b/ice/steam_banner_creator.py @@ -0,0 +1,46 @@ +# encoding: utf-8 +""" +steam_banner_creator.py + +Created by Wolfgang on 2018-02-10. +Copyright (c) 2018 Wolfgang Bergbauer. All rights reserved. + +This class creates a steam banner image from an given image. +""" +from PIL import Image, ImageFilter + +from ice.logs import logger + + +class SteamBannerCreator(): + + @staticmethod + def STEAM_BANNER_HEIGHT(): + return 215 + + @staticmethod + def STEAM_BANNER_SIZE(): return 460, 215 + + def convertToSteamBannerImage(self, oldImagePath): + try: + background = self.__createGaussianBackground(oldImagePath) + foreground = self.resizeToHeight(self.STEAM_BANNER_HEIGHT(), oldImagePath) + background.paste(foreground, (background.size[0]/2 - foreground.size[0] / 2, 0)) + background.save(oldImagePath) + except IOError as error: + logger.debug( + "There was an error converting the image " + oldImagePath + ": " + error.message + ) + + def __createGaussianBackground(self, oldImagePath): + background = Image.open(oldImagePath) + background = background.resize(self.STEAM_BANNER_SIZE(), Image.ANTIALIAS) + background = background.filter(ImageFilter.GaussianBlur(radius=20)) + return background + + def resizeToHeight(self, height, oldImagePath): + img = Image.open(oldImagePath) + hpercent = (height / float(img.size[1])) + wsize = int((float(img.size[0]) * float(hpercent))) + img = img.resize((wsize, height), Image.ANTIALIAS) + return img \ No newline at end of file diff --git a/tests/steam_banner_creator_tests.py b/tests/steam_banner_creator_tests.py new file mode 100644 index 0000000..8f80ab7 --- /dev/null +++ b/tests/steam_banner_creator_tests.py @@ -0,0 +1,31 @@ +# encoding: utf-8 +""" +steam_banner_creator_tests.py + +Created by Wolfgang on 2018-02-10. +Copyright (c) 2018 Wolfgang Bergbauer. All rights reserved. +""" + +import unittest + +import os +from mockito import * + +# I need to do this instead of importing the class explicitly so that I can +# override the urllib2 function. +# TODO: Use dependency injection so I don't need to use that hack. +from ice.gridproviders import thegamesdb_provider +from ice.steam_banner_creator import SteamBannerCreator + + +class SteamBannerCreatorTests(unittest.TestCase): + + def test_convertAll(self): + folder = "D:\\Steam\\userdata\\17910320\\config\\grid\\" + converter = SteamBannerCreator() + for filename in os.listdir(folder): + converter.convertToSteamBannerImage(folder + filename) + + def test_createImage(self): + testImagePath = "C:\\Users\\bergb\\test.jpg" + SteamBannerCreator().convertToSteamBannerImage(testImagePath) \ No newline at end of file diff --git a/tests/thegamesdb_provider_tests.py b/tests/thegamesdb_provider_tests.py new file mode 100644 index 0000000..467de25 --- /dev/null +++ b/tests/thegamesdb_provider_tests.py @@ -0,0 +1,51 @@ +# encoding: utf-8 +""" +consolegrid_provider_tests.py + +Created by Wolfgang on 2018-02-09. +Copyright (c) 2018 Wolfgang Bergbauer. All rights reserved. +""" + +import os +import unittest +from mockito import * +from urllib2 import URLError + +# I need to do this instead of importing the class explicitly so that I can +# override the urllib2 function. +# TODO: Use dependency injection so I don't need to use that hack. +from ice.gridproviders import thegamesdb_provider + + +class TheGamesDBProviderTests(unittest.TestCase): + + def setUp(self): + self.provider = thegamesdb_provider.TheGamesDBProvider() + + def tearDown(self): + pass + + def create_mock_rom(self, rom_name="Test ROM", console_name="Test"): + console = mock() + console.fullname = console_name + console.shortname = console_name + + rom = mock() + rom.name = rom_name + rom.console = console + return rom + + def test_findId(self): + rom = self.create_mock_rom("Mega Man") + id = self.provider.findGamesDBId(rom) + self.assertIsNotNone(id) + + def test_findArt(self): + testid = 1093 + url = self.provider.findArtWorkUrl(testid) + self.assertIsNotNone(url) + + def test_imageForRom(self): + rom = self.create_mock_rom("Mega Man") + image = self.provider.image_for_rom(rom) + self.assertIsNotNone(image) \ No newline at end of file