Skip to content

Commit

Permalink
add Trove support (#4)
Browse files Browse the repository at this point in the history
* add Trove support
  • Loading branch information
UncleGoogle authored Jul 25, 2019
1 parent cc5a56d commit 3920858
Show file tree
Hide file tree
Showing 8 changed files with 291 additions and 71 deletions.
10 changes: 8 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@ Integration for GOG Galaxy 2.0.

This plugin is currenly in early development stage.

* Listing DRM free games from HumbleBundle library (those with downloads for Windows, MacOS or Linux)
* Simple download
* Library: Listing DRM free games from HumbleBundle library (those with downloads for Windows, MacOS or Linux)
* Library: Humble Trove support
* Install: Simple download via webbrowser

## Installation

Expand All @@ -22,4 +23,9 @@ or build from source code (python3.6 or higher required):
3. `python tasks.py install`
4. `python tasks.py dist`


## See also
- https://github.com/gogcom/galaxy-integrations-python-api
- https://github.com/MayeulC/hb-downloader

[1]: https://github.com/UncleGoogle/galaxy-integration-humblebundle/releases
9 changes: 8 additions & 1 deletion src/consts.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import enum
import sys
import typing


class PlatformNotSupported(Exception):
Expand All @@ -20,8 +21,11 @@ def __eq__(self, other):
return self.value == other
return super().__eq__(other)

def __hash__(self):
return hash(self.value)

GAME_PLATFORMS = [HP.WINDOWS, HP.MAC, HP.LINUX]

GAME_PLATFORMS = set([HP.WINDOWS, HP.MAC, HP.LINUX])
DLC_PLATFORMS = [HP.AUDIO, HP.EBOOK] # TODO push those with base game

if sys.platform == 'win32':
Expand All @@ -30,3 +34,6 @@ def __eq__(self, other):
CURRENT_SYSTEM = HP.MAC
else:
raise PlatformNotSupported('GOG Galaxy 2.0 supports only Windows and macos for now')

# typing aliases
TP_PLATFORM = typing.Union[HP, str]
44 changes: 44 additions & 0 deletions src/humbledownloader.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
from typing import Dict

from humblegame import DownloadStruct, HumbleGame, TroveGame, Subproduct, SubproductDownload, TroveDownload, DownloadStruct
from consts import CURRENT_SYSTEM, PlatformNotSupported, TP_PLATFORM


class HumbleDownloadResolver:
"""Prepares downloads for specific conditionals"""
def __init__(self, target_platform=CURRENT_SYSTEM, target_bitness=None):
self.platform = target_platform
self.bitness = target_bitness

def __call__(self, game: HumbleGame) -> DownloadStruct:
if isinstance(game, TroveGame):
return self._find_best_trove_download(game)
elif isinstance(game, Subproduct):
return self._find_best_subproduct_download(game)

def _find_best_trove_download(self, game: TroveGame) -> TroveDownload:
try:
return game.downloads[self.platform]
except KeyError as e:
self.__platform_not_supporter_handler()

def _find_best_subproduct_download(self, game: Subproduct) -> SubproductDownload:
try:
system_downloads = game.downloads[self.platform]
except KeyError:
self.__platform_not_supporter_handler()

assert len(system_downloads) > 0

if len(system_downloads) == 1:
return system_downloads[0]
else:
download_items = list(filter(lambda x: x.name == 'Download', download_struct))

if len(download_items) == 1:
return download_items[0]
else:
raise NotImplementedError(f'Found downloads: {len(download_items)}. All: {downloads}')

def __platform_not_supporter_handler(self, game):
raise PlatformNotSupported(f'{self.human_name} has only downloads for [{game.downloads.keys()}]')
130 changes: 104 additions & 26 deletions src/humblegame.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,82 @@
from typing import Dict
import abc
import copy
from typing import Dict, Optional, List
from dataclasses import dataclass

from galaxy.api.types import Game, LicenseType, LicenseInfo

from consts import CURRENT_SYSTEM, PlatformNotSupported
from consts import TP_PLATFORM


class HumbleGame:
def __init__(self, data):
for k, v in data.items():
setattr(self, k, v)
class DownloadStruct(abc.ABC):
"""
url: Dict[str, str] # {'bittorent': str, 'web': str}
file_size: str
md5: str
name: str
uploaded_at: Optional[str] # ex.: 2019-07-10T21:48:11.976780
"""
def __init__(self, data: dict):
self._data = data
self.url = data['url']

@property
def web(self):
return self.url['web']

@property
def bittorrent(self):
return self.url['bittorrent']

@abc.abstractmethod
def human_size(self):
pass


class TroveDownload(DownloadStruct):
""" Additional fields:
sha1: str
timestamp: int # ?
machine_name: str
size: str
"""
@property
def human_size(self):
return self._data['size']

@property
def machine_name(self):
return self._data['machine_name']


class SubproductDownload(DownloadStruct):
""" Additional fields:
human_size: str
"""
@property
def human_size(self):
return self._data['human_size']


class HumbleGame(abc.ABC):
def __init__(self, data: dict):
self._data = data

@abc.abstractmethod
def downloads(self):
pass

@abc.abstractproperty
def license(self) -> LicenseInfo:
pass

@abc.abstractproperty
def human_name(self):
pass

@property
def machine_name(self):
return self._data['machine_name']

def in_galaxy_format(self):
licence = LicenseInfo(LicenseType.SinglePurchase)
Expand All @@ -19,29 +87,39 @@ def __repr__(self):
return str(self)

def __str__(self):
return f"HumbleGame({self.human_name}, {self.downloads})"
return f"HumbleGame ({self.__class__.__name__}): ({self.human_name}, {self.downloads})"


class TroveGame(HumbleGame):
@property
def downloads(self) -> Dict[TP_PLATFORM, TroveDownload]:
return {k: TroveDownload(v) for k, v in self._data['downloads'].items()}

@property
def license(self) -> LicenseInfo:
"""There is currently not 'subscription' type license"""
LicenseInfo(LicenseType.OtherUserLicense)

@property
def human_name(self):
return self._data['human-name']


class HumbleDownloader:
"""Prepares downloads for specific conditionals"""
def __init__(self, target_platrofm=CURRENT_SYSTEM, target_bitness=None, use_torrent=False):
self.platform = target_platrofm
self.bitness = target_bitness
class Subproduct(HumbleGame):
@property
def downloads(self) -> Dict[TP_PLATFORM, List[SubproductDownload]]:
return {dw['platform']: [SubproductDownload(x) for x in dw['download_struct']] for dw in self._data['downloads']}

def find_best_url(self, downloads: dict) -> Dict[str, str]:
system_downloads = list(filter(lambda x: x['platform'] == self.platform, downloads))
@property
def license(self) -> LicenseInfo:
"""There is currently not 'subscription' type license"""
LicenseInfo(LicenseType.SinglePurchase)

if len(system_downloads) == 1:
download_struct = system_downloads[0]['download_struct']
elif len(system_downloads) == 0:
platforms = [dw.platform for dw in downloads]
raise PlatformNotSupported(f'{self.human_name} has only downloads for {platforms}')
elif len(system_downloads) > 1:
raise NotImplementedError('More system level conditionals required')
@property
def human_name(self):
return self._data['human_name']

download_items = list(filter(lambda x: x['name'] == 'Download', download_struct))
@property
def name(self):
return self._data['name']

if len(download_items) == 1:
return download_items[0]['url']
else:
raise NotImplementedError(f'Found downloads: {len(download_items)}. All: {downloads}')
61 changes: 37 additions & 24 deletions src/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,10 @@
from galaxy.api.errors import InvalidCredentials

from version import __version__
from consts import GAME_PLATFORMS
from webservice import AuthorizedHumbleAPI
from humblegame import HumbleGame, HumbleDownloader
from consts import PlatformNotSupported, GAME_PLATFORMS
from humblegame import TroveGame, Subproduct
from humbledownloader import HumbleDownloadResolver


AUTH_PARAMS = {
Expand All @@ -34,7 +35,7 @@ def __init__(self, reader, writer, token):
super().__init__(Platform.HumbleBundle, __version__, reader, writer, token)
self._api = AuthorizedHumbleAPI()
self._games = {}
self._downloader = HumbleDownloader()
self._download_resolver = HumbleDownloadResolver()

async def authenticate(self, stored_credentials=None):
if not stored_credentials:
Expand All @@ -53,43 +54,55 @@ async def pass_login_credentials(self, step, credentials, cookies):
return Authentication(user_id, user_name)

async def get_owned_games(self):

def is_game(sub):
default = False
return next(filter(lambda x: x['platform'] in GAME_PLATFORMS, sub['downloads']), default)

games = {}
gamekeys = await self._api.get_gamekeys()
requests = [self._api.get_order_details(x) for x in gamekeys]
orders = [self._api.get_order_details(x) for x in gamekeys]

logging.info(f'Fetching info about {len(requests)} orders started...')
all_games_details = await asyncio.gather(*requests)
logging.info(f'Fetching info about {len(orders)} orders started...')
all_games_details = await asyncio.gather(*orders)
logging.info('Fetching info finished')

products = []

if await self._api.is_trove_subscribed():
logging.info(f'Fetching trove info started...')
troves = await self._api.get_trove_details()
logging.info('Fetching info finished')
for trove in troves:
products.append(TroveGame(trove))

for details in all_games_details:
for sub in details['subproducts']:
try:
if is_game(sub):
games[sub['machine_name']] = HumbleGame(sub)
except Exception as e:
logging.error(f'Error while parsing subproduct {sub}: {repr(e)}')
continue
prod = Subproduct(sub)
if not set(prod.downloads).isdisjoint(GAME_PLATFORMS):
# at least one download is for supported OS
products.append(prod)

self._games = games
return [g.in_galaxy_format() for g in games.values()]
self._games = {
product.machine_name: product
for product in products
}

return [g.in_galaxy_format() for g in self._games.values()]

async def get_local_games(self):
return []

async def install_game(self, game_id):
game = self._games.get(game_id)
if game is None:
logging.error(f'Install game: game {game_id} not found')
return
raise RuntimeError(f'Install game: game {game_id} not found')

try:
url = self._downloader.find_best_url(game.downloads)
chosen_download = self._download_resolver(game)
except Exception as e:
logging.exception(e)
raise

if isinstance(game, TroveGame):
url = await self._api.get_trove_sign_url(chosen_download, game.machine_name)
webbrowser.open(url['signed_url'])
else:
webbrowser.open(url['web'])
webbrowser.open(chosen_download.web)

# async def launch_game(self, game_id):
# pass
Expand Down
2 changes: 1 addition & 1 deletion src/version.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = "0.1.2"
__version__ = "0.2.0"
Loading

0 comments on commit 3920858

Please sign in to comment.