diff --git a/.travis.yml b/.travis.yml index b09477f..1159476 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,10 +1,12 @@ language: "python" python: "3.6" install: - - pip install requests pyyaml pytest-cov python-coveralls + - pip install requests pyyaml lxml pytest-cov python-coveralls httpretty codacy-coverage script: - python3 -m pytest --cov=. after_success: - coveralls + - coverage xml + - python-codacy-coverage -r coverage.xml notifications: - email: false diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..81cc950 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,12 @@ +FROM python:3 + +VOLUME /data +WORKDIR /app + +RUN git clone https://github.com/MayeulC/hb-downloader.git /app && \ + cd /app && \ + git checkout poc-trove && \ + python setup.py install + +CMD python /app/hb-downloader.py download + diff --git a/README.md b/README.md index 050ae34..379890c 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,7 @@ library, available at https://github.com/saik0/humblebundle-python * Python 3.6 * requests library * pyyaml library +* (optionnally for Humble Trove support) lxml ## Python Installation Several features particular to Python v3.6 might have been used during the @@ -32,12 +33,13 @@ https://www.python.org/downloads/ and grab the latest 3.x.x release. ## Getting the Prerequisites From a command prompt, enter: - pip install requests - pip install pyyaml + pip install requests pyyaml lxml You'll either be informed that the requirement is already satisfied, or pip will retrieve, install, and configure the libraries for you. +Alternatively, you can run the `setup.py` script. + ## Getting the Installation Files Perform one of the following actions: * Download the zip file from the [releases diff --git a/hb-downloader.py b/hb-downloader.py index 27d034c..d858f4e 100755 --- a/hb-downloader.py +++ b/hb-downloader.py @@ -1,53 +1,12 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -import logger -from config_data import ConfigData -from configuration import Configuration -from event_handler import EventHandler -from humble_api.humble_api import HumbleApi -from actions import Action -__author__ = "Brian Schkerke" -__copyright__ = "Copyright 2016 Brian Schkerke" -__license__ = "MIT" - - -print("Humble Bundle Downloader v%s" % ConfigData.VERSION) -print("This program is not affiliated nor endorsed by Humble Bundle, Inc.") -print("For any suggestion or bug report, please create an issue at:\n%s" % - ConfigData.BUG_REPORT_URL) -print("") - -# Load the configuration from the YAML file... -Configuration.load_configuration("hb-downloader-settings.yaml") -Configuration.parse_command_line() -Configuration.dump_configuration() -Configuration.push_configuration() - -validation_status, message = Configuration.validate_configuration() -if not validation_status: - logger.display_message(False, "Error", message) - exit("Invalid configuration. Please check your command line arguments and" - "hb-downloader-settings.yaml.") - -# Initialize the event handlers. -EventHandler.initialize() - -hapi = HumbleApi(ConfigData.auth_sess_cookie) - -if not hapi.check_login(): - exit("Login to humblebundle.com failed." - " Please verify your authentication cookie") - -logger.display_message(False, "Processing", "Downloading order list.") -game_keys = hapi.get_gamekeys() -logger.display_message(False, "Processing", "%s orders found." % - (len(game_keys))) - -if ConfigData.action == "download": - Action.batch_download(hapi, game_keys) -else: - Action.list_downloads(hapi, game_keys) +import sys +import humble_downloader +__author__ = "Mayeul Cantan" +__copyright__ = "Copyright 2018 Mayeul Cantan" +__license__ = "MIT" -exit() +if __name__ == "__main__": + sys.exit(humble_downloader.main()) diff --git a/humble_api/exceptions/__init__.py b/humble_api/exceptions/__init__.py deleted file mode 100644 index a0b7a58..0000000 --- a/humble_api/exceptions/__init__.py +++ /dev/null @@ -1,10 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = "Joel Pedraza" -__copyright__ = "Copyright 2014, Joel Pedraza" -__license__ = "MIT" - -__all__ = ["HumbleAuthenticationException", "HumbleCaptchaException", "HumbleCredentialException", - "HumbleDownloadNeededException", "HumbleException", "HumbleParseException", - "HumbleResponseException", "HumbleTwoFactorException"] diff --git a/humble_downloader/__init__.py b/humble_downloader/__init__.py new file mode 100755 index 0000000..f0c4cf2 --- /dev/null +++ b/humble_downloader/__init__.py @@ -0,0 +1,62 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +import sys +import humble_downloader.logger as logger +from humble_downloader.config_data import ConfigData +from .configuration import Configuration +from .event_handler import EventHandler +from .humble_api.humble_api import HumbleApi +from .actions import Action + +__author__ = "Brian Schkerke" +__copyright__ = "Copyright 2016 Brian Schkerke" +__license__ = "MIT" + +__all__ = ["ConfigData", "Action", "display_message", "HumbleDownload", "ProgressTracker"] + + +def main(): + print("Humble Bundle Downloader v%s" % ConfigData.VERSION) + print("This program is not affiliated nor endorsed by Humble Bundle, Inc.") + print("For any suggestion or bug report, please create an issue at:\n%s" % + ConfigData.BUG_REPORT_URL) + print("") + + # Load the configuration from the YAML file... + Configuration.load_configuration("hb-downloader-settings.yaml") + Configuration.parse_command_line() + Configuration.dump_configuration() + Configuration.push_configuration() + + validation_status, message = Configuration.validate_configuration() + if not validation_status: + logger.display_message(False, "Error", message) + sys.exit("Invalid configuration. Please check your command line " + "arguments and hb-downloader-settings.yaml.") + + # Initialize the event handlers. + EventHandler.initialize() + + hapi = HumbleApi(ConfigData.auth_sess_cookie) + + if not hapi.check_login(): + exit("Login to humblebundle.com failed." + " Please verify your authentication cookie") + + logger.display_message(False, "Processing", "Downloading order list.") + if ConfigData.restrict_keys: + game_keys = ConfigData.restrict_keys + else: + game_keys = hapi.get_gamekeys() + logger.display_message(False, "Processing", "%s orders found." % + (len(game_keys))) + + if ConfigData.action == "download": + Action.batch_download(hapi, game_keys) + else: + Action.list_downloads(hapi, game_keys) + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/actions.py b/humble_downloader/actions.py similarity index 90% rename from actions.py rename to humble_downloader/actions.py index 7664392..cb03a42 100644 --- a/actions.py +++ b/humble_downloader/actions.py @@ -3,18 +3,23 @@ __license__ = "MIT" -from humble_download import HumbleDownload -from progress_tracker import ProgressTracker -from config_data import ConfigData -import logger +from .humble_download import HumbleDownload +from .progress_tracker import ProgressTracker +from .config_data import ConfigData +from . import logger class Action: @staticmethod def list_downloads(hapi, game_keys): for key in game_keys: + if ConfigData.download_platforms.get("humble-keys", False): + print("%s" % key) + continue selector_matched_key_once = False current_order = hapi.get_order(key) + if 'subproducts' not in current_order: + continue for current_subproduct in current_order.subproducts or []: selector_matched_subproduct_once = False @@ -66,7 +71,7 @@ def batch_download(hapi, game_keys): key) item_count_total += len(humble_downloads) download_size_total += sum( - dl.humble_file_size for dl in humble_downloads) + dl.humble_file_size or 0 for dl in humble_downloads) logger.display_message(False, "Processing", "Added %d downloads for order %s" % (len(humble_downloads), key)) diff --git a/config_data.py b/humble_downloader/config_data.py similarity index 100% rename from config_data.py rename to humble_downloader/config_data.py diff --git a/configuration.py b/humble_downloader/configuration.py similarity index 87% rename from configuration.py rename to humble_downloader/configuration.py index 7e0ee93..957bdc0 100644 --- a/configuration.py +++ b/humble_downloader/configuration.py @@ -1,11 +1,13 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- + import argparse import os import yaml -import logger -from config_data import ConfigData -from humble_api.humble_hash import HumbleHash +from . import logger +from .config_data import ConfigData +from .humble_api.humble_hash import HumbleHash +from .humble_api.humble_api import HumbleApi __author__ = "Brian Schkerke" __copyright__ = "Copyright 2016 Brian Schkerke" @@ -45,7 +47,6 @@ def load_configuration(config_file): """ with open(config_file, "r") as f: saved_config = yaml.safe_load(f) - ConfigData.download_platforms = saved_config.get( "download-platforms", ConfigData.download_platforms) ConfigData.write_md5 = saved_config.get( @@ -112,19 +113,27 @@ def parse_command_line(): "parameters are specified, this will default to " "downloading everything in the library.")) + a_list.add_argument( + "-u", "--print-url", action="store_true", dest="print_url", + help=("Print the download url with the output. Please note " + "that the url expires after a while")) + for action in [a_list, a_download]: item_type = action.add_subparsers(title="type", dest="item_type") - games = item_type.add_parser("games") + games = item_type.add_parser("games", help="Only list games") games.add_argument( "--platform", nargs='+', choices=[ # TODO: add NATIVE? "linux", "mac", "windows", "android", "asmjs"]) - item_type.add_parser("ebooks") - item_type.add_parser("audio") + item_type.add_parser("ebooks", help="Only list ebooks") + item_type.add_parser("audio", help="Only display audio products") + if action is a_list: + item_type.add_parser("humble-keys", help=( + "Only list humble bundle keys that identify each " + "purchase")) + action.add_argument("-k", "--keys", nargs="+", help=( + "Only consider listed game key(s). Humble trove games are " + 'considered a special "' + HumbleApi.TROVE_GAMEKEY + '" key')) - a_list.add_argument( - "-u", "--print-url", action="store_true", dest="print_url", - help=("Print the download url with the output. Please note " - "that the url expires after a while")) args = parser.parse_args() Configuration.configure_action(args) @@ -143,20 +152,20 @@ def configure_action(args): args.print_url = False if args.action is not None: - if args.platform is None: - args.platform = Configuration.cmdline_platform.get( - args.item_type) for platform in ConfigData.download_platforms: if args.platform is None: - ConfigData.download_platforms[platform] = True continue if platform in args.platform: ConfigData.download_platforms[platform] = True - else: - ConfigData.download_platforms[platform] = False + # "fake platforms" can be defined to list info like humble keys + # TODO: only allow them for listing? + if args.item_type not in ConfigData.download_platforms: + ConfigData.download_platforms[args.item_type] = True else: args.action = "download" + ConfigData.action = args.action + ConfigData.restrict_keys = args.keys ConfigData.print_url = args.print_url @staticmethod diff --git a/event_handler.py b/humble_downloader/event_handler.py similarity index 95% rename from event_handler.py rename to humble_downloader/event_handler.py index e3786e0..442e9a1 100644 --- a/event_handler.py +++ b/humble_downloader/event_handler.py @@ -1,8 +1,9 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- + import sys -import logger -from humble_api.events import Events +from . import logger +from .humble_api.events import Events __author__ = "Brian Schkerke" __copyright__ = "Copyright 2016 Brian Schkerke" diff --git a/humble_api/__init__.py b/humble_downloader/humble_api/__init__.py similarity index 100% rename from humble_api/__init__.py rename to humble_downloader/humble_api/__init__.py diff --git a/humble_api/events.py b/humble_downloader/humble_api/events.py similarity index 100% rename from humble_api/events.py rename to humble_downloader/humble_api/events.py diff --git a/humble_downloader/humble_api/exceptions/__init__.py b/humble_downloader/humble_api/exceptions/__init__.py new file mode 100644 index 0000000..f64755d --- /dev/null +++ b/humble_downloader/humble_api/exceptions/__init__.py @@ -0,0 +1,9 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "Joel Pedraza" +__copyright__ = "Copyright 2014, Joel Pedraza" +__license__ = "MIT" + +__all__ = ["HumbleAuthenticationException", "HumbleDownloadNeededException", "HumbleException", "HumbleParseException", + "HumbleResponseException"] diff --git a/humble_downloader/humble_api/exceptions/humble_authentication_exception.py b/humble_downloader/humble_api/exceptions/humble_authentication_exception.py new file mode 100644 index 0000000..37457dd --- /dev/null +++ b/humble_downloader/humble_api/exceptions/humble_authentication_exception.py @@ -0,0 +1,21 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +from .humble_response_exception import HumbleResponseException + +__license__ = "MIT" + + +class HumbleAuthenticationException(HumbleResponseException): + """ + Authentication failed due to a rejected authentication cookie + """ + + def __init__(self, *args, **kwargs): + """ + Parameterized constructor for the HumbleAuthenticationException. + :param list args: (optional) Extra positional args to pass to the request. + :param dict kwargs: (optional) Extra keyword args to pass to the request. + """ + + super(HumbleAuthenticationException, self).__init__(*args, **kwargs) diff --git a/humble_api/exceptions/humble_download_needed_exception.py b/humble_downloader/humble_api/exceptions/humble_download_needed_exception.py similarity index 100% rename from humble_api/exceptions/humble_download_needed_exception.py rename to humble_downloader/humble_api/exceptions/humble_download_needed_exception.py diff --git a/humble_api/exceptions/humble_exception.py b/humble_downloader/humble_api/exceptions/humble_exception.py similarity index 100% rename from humble_api/exceptions/humble_exception.py rename to humble_downloader/humble_api/exceptions/humble_exception.py diff --git a/humble_api/exceptions/humble_parse_exception.py b/humble_downloader/humble_api/exceptions/humble_parse_exception.py similarity index 100% rename from humble_api/exceptions/humble_parse_exception.py rename to humble_downloader/humble_api/exceptions/humble_parse_exception.py diff --git a/humble_api/exceptions/humble_response_exception.py b/humble_downloader/humble_api/exceptions/humble_response_exception.py similarity index 100% rename from humble_api/exceptions/humble_response_exception.py rename to humble_downloader/humble_api/exceptions/humble_response_exception.py diff --git a/humble_api/humble_api.py b/humble_downloader/humble_api/humble_api.py similarity index 78% rename from humble_api/humble_api.py rename to humble_downloader/humble_api/humble_api.py index 2191670..9282fd3 100644 --- a/humble_api/humble_api.py +++ b/humble_downloader/humble_api/humble_api.py @@ -3,9 +3,11 @@ import http.cookiejar import itertools from .model.order import Order +from .model.trove_order import TroveOrder import requests from .exceptions.humble_response_exception import HumbleResponseException from .exceptions.humble_parse_exception import HumbleParseException +from .exceptions.humble_authentication_exception import HumbleAuthenticationException __author__ = "Joel Pedraza" __copyright__ = "Copyright 2014, Joel Pedraza" @@ -28,6 +30,10 @@ class HumbleApi(object): LOGIN_URL = "https://www.humblebundle.com/processlogin" ORDER_LIST_URL = "https://www.humblebundle.com/api/v1/user/order" ORDER_URL = "https://www.humblebundle.com/api/v1/order/{order_id}" + TROVE_SIGN_URL= "https://www.humblebundle.com/api/v1/user/download/sign" + TROVE_PAGE_URL = "https://www.humblebundle.com/api/v1/trove/chunk?index={chunk_index}" + + TROVE_GAMEKEY = TroveOrder.TROVE_GAMEKEY # Arbitrary gamekey used to identify humble trove orders # default_headers specifies the default HTTP headers added to each request sent to the humblebundle.com servers. default_headers = { @@ -82,6 +88,22 @@ def check_login(self): except HumbleAuthenticationException: return False + def get_signed_trove_url(self, machine_name, filename): + """ + Transforms a machine name and a filename into valid URLs for the humble trove + parameters: machine name, filename + + :param machine_name: A name given by humblebundle to the subproduct + :param filename: a name given by humblebundle to the downloaded file, as seen in the URL + :return: dict with signed_torrent_url and signed_url + """ + parameters = {"machine_name": machine_name, "filename": filename} + response = self._request("POST", HumbleApi.TROVE_SIGN_URL, data=parameters) + data = self.__parse_data(response) + + if self.__authenticated_response_helper(response, data): + return data + def get_gamekeys(self, *args, **kwargs): """ Fetch all the gamekeys owned by an account. @@ -102,7 +124,9 @@ def get_gamekeys(self, *args, **kwargs): data = self.__parse_data(response) if isinstance(data, list): - return [v["gamekey"] for v in data] + keys = [v["gamekey"] for v in data] + keys.append(HumbleApi.TROVE_GAMEKEY) # Unconditionnally include the humble trove key + return keys # Let the helper function raise any common exceptions self.__authenticated_response_helper(response, data) @@ -124,6 +148,8 @@ def get_order(self, order_id, *args, **kwargs): :raises HumbleResponseException: if the response was invalid """ url = HumbleApi.ORDER_URL.format(order_id=order_id) + if order_id == HumbleApi.TROVE_GAMEKEY: + return self.get_trove_items() response = self._request("GET", url, *args, **kwargs) @@ -138,6 +164,29 @@ def get_order(self, order_id, *args, **kwargs): if self.__authenticated_response_helper(response, data): return Order(data) + def get_trove_items(self): + """ + get element from trove making request on hb api + """ + trove_data_element = [] + chunk_index = 0 + + while True: + url = HumbleApi.TROVE_PAGE_URL.format(chunk_index=chunk_index) + response = self._request("GET", url) + """ get_gamekeys response always returns JSON """ + data = self.__parse_data(response) + """ [] as response mean that we have tried all the chunk_index """ + if(data == []): + break + else: + """ Make a flat list for trove_data_element """ + for i in data: + trove_data_element.append(i) + chunk_index = chunk_index + 1 + + return TroveOrder(trove_data_element, self) # TODO error handling + def _request(self, *args, **kwargs): """ Set sane defaults that aren't session wide. Otherwise maintains the API of Session.request. @@ -179,7 +228,8 @@ def __authenticated_response_helper(self, response, data): # Response had no success or errors fields, it's probably data return True - def __parse_data(self, response): + @staticmethod + def __parse_data(response): """ Try and parse the response data as JSON. If parsing fails, throw a HumbleParseException. @@ -193,7 +243,8 @@ def __parse_data(self, response): except ValueError as e: raise HumbleParseException("Invalid JSON: %s", str(e), request=response.request, response=response) - def __get_errors(self, data): + @staticmethod + def __get_errors(data): """ Retrieves any errors defined within the JSON and returns them as a string. diff --git a/humble_api/humble_hash.py b/humble_downloader/humble_api/humble_hash.py similarity index 100% rename from humble_api/humble_hash.py rename to humble_downloader/humble_api/humble_hash.py diff --git a/humble_api/model/__init__.py b/humble_downloader/humble_api/model/__init__.py similarity index 100% rename from humble_api/model/__init__.py rename to humble_downloader/humble_api/model/__init__.py diff --git a/humble_api/model/base_model.py b/humble_downloader/humble_api/model/base_model.py similarity index 100% rename from humble_api/model/base_model.py rename to humble_downloader/humble_api/model/base_model.py diff --git a/humble_api/model/download.py b/humble_downloader/humble_api/model/download.py similarity index 100% rename from humble_api/model/download.py rename to humble_downloader/humble_api/model/download.py diff --git a/humble_api/model/download_struct.py b/humble_downloader/humble_api/model/download_struct.py similarity index 100% rename from humble_api/model/download_struct.py rename to humble_downloader/humble_api/model/download_struct.py diff --git a/humble_api/model/order.py b/humble_downloader/humble_api/model/order.py similarity index 100% rename from humble_api/model/order.py rename to humble_downloader/humble_api/model/order.py diff --git a/humble_api/model/payee.py b/humble_downloader/humble_api/model/payee.py similarity index 100% rename from humble_api/model/payee.py rename to humble_downloader/humble_api/model/payee.py diff --git a/humble_api/model/product.py b/humble_downloader/humble_api/model/product.py similarity index 100% rename from humble_api/model/product.py rename to humble_downloader/humble_api/model/product.py diff --git a/humble_api/model/subproduct.py b/humble_downloader/humble_api/model/subproduct.py similarity index 100% rename from humble_api/model/subproduct.py rename to humble_downloader/humble_api/model/subproduct.py diff --git a/humble_api/model/subscription.py b/humble_downloader/humble_api/model/subscription.py similarity index 100% rename from humble_api/model/subscription.py rename to humble_downloader/humble_api/model/subscription.py diff --git a/humble_downloader/humble_api/model/trove_order.py b/humble_downloader/humble_api/model/trove_order.py new file mode 100644 index 0000000..f600a89 --- /dev/null +++ b/humble_downloader/humble_api/model/trove_order.py @@ -0,0 +1,64 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +from .order import Order +from .subproduct import Subproduct +from .product import Product +from sys import stderr +import json + + +class TroveOrder(Order): + + TROVE_GAMEKEY = "Humble-trove-games" + + def __init__(self, trove_data_element, hapi): + """ + Parameterized constructor for the Order object. + + :param trove_page_html_text: The plain text/html page of the humble trove + :param hapi: humble bundle api handle. Used to read the trove dummy gamekey and signing URL. + """ + + super(Order, self).__init__(trove_data_element) + + subproducts = [] + + for trove_data in trove_data_element: + product = {'human_name': trove_data['human-name'], + 'machine_name': trove_data['machine_name'], + 'downloads': [], + 'payee': {} # TODO: put something better here + } + for platform, platform_data in trove_data['downloads'].items(): + pp = dict() + pp['platform'] = platform + pp['download_identifier'] = platform_data['url']['web'] + pp['machine_name'] = platform_data['machine_name'] + signed = hapi.get_signed_trove_url(pp['machine_name'], pp['download_identifier']) + pp['download_struct'] = [{ + 'url': { + 'web': signed.get('signed_url', None), + 'bittorrent': signed.get('signed_torrent_url', None) + }, + 'file_size': platform_data.get('file_size'), + 'human_size': platform_data.get('size'), + 'md5': platform_data.get('md5'), + 'sha1': platform_data.get('sha1') + }] + pp['options_dict'] = None # TODO: What is this? + product['downloads'].append(pp) + subproducts.append(Subproduct(product)) # TODO: check that the data formats actually match + + self.subscriptions = None + self.created = None + self.amount_to_charge = None + self.gamekey = TroveOrder.TROVE_GAMEKEY + self.subproducts = subproducts + + product_data = { + 'human_name': 'Humble Trove Games', + 'machine_name': 'trove_games', + 'category': 'trove' + } + self.product = Product(product_data) diff --git a/humble_download.py b/humble_downloader/humble_download.py similarity index 95% rename from humble_download.py rename to humble_downloader/humble_download.py index 5f45f87..13f45f3 100644 --- a/humble_download.py +++ b/humble_downloader/humble_download.py @@ -1,10 +1,12 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- + import os import requests -from config_data import ConfigData -from humble_api.events import Events -from humble_api.humble_hash import HumbleHash +from .config_data import ConfigData +from .humble_api.events import Events +from .humble_api.humble_hash import HumbleHash +from .humble_api.model.trove_order import TroveOrder __author__ = "Brian Schkerke" __copyright__ = "Copyright 2016 Brian Schkerke" @@ -39,6 +41,7 @@ def __init__(self, cd, cds, co, csp, cv): self.subproduct_name = csp.product_name self.humble_md5 = cds.md5 self.machine_name = cd.machine_name + self.is_trove = isinstance(co, TroveOrder) @property def local_file_size(self): @@ -120,7 +123,7 @@ def check_status(self): self.local_file_size or 0)) self.partial_download = True self.requires_download = True - elif not ConfigData.ignore_md5: + elif not ConfigData.ignore_md5 and not self.is_trove: if not self.md5_matches: self.status_message = ( "MD5 of %s doesn't match (expected %s actual %s)." % @@ -172,7 +175,8 @@ def __download_file(self, web_request, mode, read_bytes=0): # For a download that's resumed the content-length will be the # remaining bytes, not the total. # total_length = int(web_request.headers.get("content-length")) - total_length = self.humble_file_size + total_length = self.humble_file_size or int( + web_request.headers.get("content-length")) chunk_size = ConfigData.chunk_size for chunk in web_request.iter_content(chunk_size=chunk_size): @@ -196,22 +200,25 @@ def __create_directory(self): os.makedirs(full_directory) def is_valid(self): - if self.humble_file_size is None or self.humble_file_size == 0: - self.status_message = ("Humble file size reported as 0. " - "Download is invalid.") - return False if self.download_url is None or len(self.download_url) == 0: self.status_message = ("Humble download URL is an empty string. " "Download is invalid.") return False - if self.humble_md5 is None or len(self.humble_md5) == 0: - self.status_message = ("Humble MD5 is an empty string. " - "Download is invalid.") - return False if self.filename is None or len(self.filename) == 0: self.status_message = ("Filename is an empty string. " "Download is invalid.") return False + if self.is_trove: + return True + if self.humble_file_size is None or self.humble_file_size == 0: + self.status_message = ("Humble file size reported as 0. " + "Download is invalid.") + print("size reported as 0") + return False + if self.humble_md5 is None or len(self.humble_md5) == 0: + self.status_message = ("Humble MD5 is an empty string. " + "Download is invalid.") + return False return True diff --git a/logger.py b/humble_downloader/logger.py similarity index 96% rename from logger.py rename to humble_downloader/logger.py index e0e609a..9d74c2e 100644 --- a/logger.py +++ b/humble_downloader/logger.py @@ -1,6 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -from config_data import ConfigData + +from .config_data import ConfigData import time import sys diff --git a/progress_tracker.py b/humble_downloader/progress_tracker.py similarity index 99% rename from progress_tracker.py rename to humble_downloader/progress_tracker.py index ccce8f0..f5b6fe4 100644 --- a/progress_tracker.py +++ b/humble_downloader/progress_tracker.py @@ -1,11 +1,12 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -import logger __author__ = "Brian Schkerke" __copyright__ = "Copyright 2016 Brian Schkerke" __license__ = "MIT" +from . import logger + class ProgressTracker(object): item_count_current = 0 diff --git a/setup.py b/setup.py new file mode 100755 index 0000000..1446e2c --- /dev/null +++ b/setup.py @@ -0,0 +1,38 @@ +#!/usr/bin/env python3 +# -*- coding: UTF-8 -*- + + +import os +from setuptools import setup, find_packages + + +def read(file_name): + return open(os.path.join(os.path.dirname(__file__), file_name)).read() + + +setup( + name="humble_downloader", # your package name (i.e. for import) + version="0.5.0", + maintainer="Mayeul Cantan", + maintainer_email="mayeul.cantan@live.fr", + author="Brian Schkerke, Mayeul Cantan", + author_email="N/A, mayeul.cantan@live.fr", + description="An automated utility to download your Humble Bundle purchases.", + license="MIT", + keywords="humble bundle download games", + url="https://github.com/MayeulC/hb-downloader", + packages=find_packages(exclude=["*test*", "*TEST*"]), + install_requires=[ + 'requests', + 'pyyaml', + 'lxml' + ], + long_description=read('README.md'), + classifiers=[ + "Development Status :: 4 - Beta", + "Topic :: Utilities", + "License :: OSI Approved :: MIT", + "Natural Language :: English" + ], + zip_safe=True, +) diff --git a/test/__init__.py b/test/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/test/fake_library/HHHHHHHHHHHHHHH.key b/test/fake_library/HHHHHHHHHHHHHHH.key new file mode 100644 index 0000000..386da8f --- /dev/null +++ b/test/fake_library/HHHHHHHHHHHHHHH.key @@ -0,0 +1,107 @@ +{ +"amount_spent":1.0, +"product":{ +"category":"bundle", +"machine_name":"test_bundle_A", +"post_purchase_text":"", +"supports_canonical":false, +"human_name":"Test Bundle A", +"automated_empty_tpkds":{}, +"partial_gift_enabled":true +}, +"gamekey":"HHHHHHHHHHHHHHH", +"uid":"XXXXXXXXXXXXX", +"created":"2018-02-08T01:02:03.456789", +"missed_credit":null, +"subproducts":[ +{ +"machine_name":"game_A_artbook", +"url":"http://example.com/", +"downloads":[ +{ +"machine_name":"game_a_art_pdf", +"platform":"ebook", +"download_struct":[ +{ +"sha1":"173b58299e8f66b8ad59c883053746df849b4d7f", +"name":"PDF", +"url":{ +"web":"https://dl.humble.com/gamea_art.pdf", +"bittorrent":"https://dl.humble.com/torrents/game_a_audio.zip.torrent" +}, +"human_size":"0.1 MB", +"file_size":36, +"small":1, +"md5":"7732f3d8f3683f56f5f2045ed8dc3817" +} +], +"options_dict":{}, +"download_identifier":"", +"android_app_only":false, +"download_version_number":null +} +], +"custom_download_page_box_html":"", +"payee":{ +"human_name":"Payee 1", +"machine_name":"payee1" +}, +"human_name":"Game A soundtrack", +"library_family_name":null, +"icon":"https://humblebundle.imgix.net/icon_game_a.png" +}, +{ +"machine_name":"gamea_soundtrack", +"url":"http://example.com/", +"downloads":[ +{ +"machine_name":"gamea_audio", +"platform":"audio", +"download_struct":[ +{ +"human_size":"0.1 MB", +"name":"FLAC", +"url":{ +"web":"https://dl.humble.com/gamea_flac.zip", +"bittorrent":"https://dl.humble.com/torrents/gamea_flac.zip.torrent" +}, +"file_size":38, +"small":1, +"md5":"44bce38e82fd8ecd26a43abc8d4218ad" +}, +{ +"human_size":"0.1 MB", +"name":"MP3", +"url":{ +"web":"https://dl.humble.com/gamea_mp3.zip", +"bittorrent":"https://dl.humble.com/torrents/gamea_mp3.zip.torrent" +}, +"file_size":37, +"small":1, +"md5":"aee3758a3c291a5d9526ee084a49b741" +} +], +"options_dict":{}, +"download_identifier":"", +"android_app_only":false, +"download_version_number":null +} +], +"custom_download_page_box_html":"", +"payee":{ +"human_name":"Payee Name 2", +"machine_name":"payeename2" +}, +"human_name":"Human name A2", +"library_family_name":null, +"icon":"https://humblebundle.imgix.net/miscA2.png" +} +], +"currency":"USD", +"is_giftee":false, +"claimed":true, +"total":1.0, +"path_ids":[ +"123456789123456789" +] +} diff --git a/test/fake_library/KKKKKKKKKKKKKK.key b/test/fake_library/KKKKKKKKKKKKKK.key new file mode 100644 index 0000000..c94042b --- /dev/null +++ b/test/fake_library/KKKKKKKKKKKKKK.key @@ -0,0 +1,107 @@ +{ +"amount_spent":1.0, +"product":{ +"category":"bundle", +"machine_name":"test_bundle_A", +"post_purchase_text":"", +"supports_canonical":false, +"human_name":"Test Bundle A", +"automated_empty_tpkds":{}, +"partial_gift_enabled":true +}, +"gamekey":"MMMMMMMMMMMMM", +"uid":"XXXXXXXXXXXXX", +"created":"2018-02-08T01:02:03.456789", +"missed_credit":null, +"subproducts":[ +{ +"machine_name":"game_A_soundtrack", +"url":"http://example.com/", +"downloads":[ +{ +"machine_name":"game_a_audio", +"platform":"audio", +"download_struct":[ +{ +"sha1":"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", +"name":"MP3", +"url":{ +"web":"https://dl.humble.com/game_a_audio.zip", +"bittorrent":"https://dl.humble.com/torrents/game_a_audio.zip.torrent" +}, +"human_size":"0.1 MB", +"file_size":102, +"small":1, +"md5":"1111111111111111111111111" +} +], +"options_dict":{}, +"download_identifier":"", +"android_app_only":false, +"download_version_number":null +} +], +"custom_download_page_box_html":"", +"payee":{ +"human_name":"Payee 1", +"machine_name":"payee1" +}, +"human_name":"Game A soundtrack", +"library_family_name":null, +"icon":"https://humblebundle.imgix.net/icon_game_a.png" +}, +{ +"machine_name":"gamea_soundtrack", +"url":"http://example.com/", +"downloads":[ +{ +"machine_name":"gamea_audio", +"platform":"audio", +"download_struct":[ +{ +"human_size":"0.1 MB", +"name":"FLAC", +"url":{ +"web":"https://dl.humble.com/gamea_flac.zip", +"bittorrent":"https://dl.humble.com/torrents/gamea_flac.zip.torrent" +}, +"file_size":102, +"small":1, +"md5":"11111111111111111111111" +}, +{ +"human_size":"0.1 MB", +"name":"MP3", +"url":{ +"web":"https://dl.humble.com/gamea_mp3.zip", +"bittorrent":"https://dl.humble.com/torrents/gamea_mp3.zip.torrent" +}, +"file_size":102, +"small":1, +"md5":"111111111111111111111111111111" +} +], +"options_dict":{}, +"download_identifier":"", +"android_app_only":false, +"download_version_number":null +} +], +"custom_download_page_box_html":"", +"payee":{ +"human_name":"Payee Name 2", +"machine_name":"payeename2" +}, +"human_name":"Human name A2", +"library_family_name":null, +"icon":"https://humblebundle.imgix.net/miscA2.png" +} +], +"currency":"USD", +"is_giftee":false, +"claimed":true, +"total":1.0, +"path_ids":[ +"123456789123456789" +] +} \ No newline at end of file diff --git a/test/fake_library/downloads/gamea_art.pdf b/test/fake_library/downloads/gamea_art.pdf new file mode 100644 index 0000000..f61641b Binary files /dev/null and b/test/fake_library/downloads/gamea_art.pdf differ diff --git a/test/fake_library/downloads/gamea_flac.zip b/test/fake_library/downloads/gamea_flac.zip new file mode 100644 index 0000000..5125340 --- /dev/null +++ b/test/fake_library/downloads/gamea_flac.zip @@ -0,0 +1 @@ +This is game A's audio in FLAC format diff --git a/test/fake_library/downloads/gamea_mp3.zip b/test/fake_library/downloads/gamea_mp3.zip new file mode 100644 index 0000000..3a0d0e6 --- /dev/null +++ b/test/fake_library/downloads/gamea_mp3.zip @@ -0,0 +1 @@ +This is Game A's audio in mp3 format diff --git a/test/fake_library/fake_library.py b/test/fake_library/fake_library.py new file mode 100644 index 0000000..751fcd4 --- /dev/null +++ b/test/fake_library/fake_library.py @@ -0,0 +1,71 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__license__ = "MIT" + +from glob import iglob +import httpretty +from os import path + + +class FakeLibrary: + + gamekeys_list = [] + local_path = "" + + def __init__(self, hapi): + self.local_path = path.dirname(__file__) + self.update_gamekeys(hapi) + self.prepare_fake_answer_list_gamekeys(hapi) + self.prepare_fake_answers_keys(hapi) + self.prepare_fake_answer_trove(hapi) + + def update_gamekeys(self, hapi): + self.gamekeys_list = [] + for filename in iglob(path.join(self.local_path, "*.key")): + self.gamekeys_list.append(path.basename(filename)[0:-4]) + self.gamekeys_list.append(hapi.TROVE_GAMEKEY) + + def prepare_fake_answer_trove(self, hapi): + httpretty.register_uri( + httpretty.GET, + hapi.TROVE_PAGE_URL, + self.readfile("trove.html") + ) + httpretty.register_uri( + httpretty.POST, + hapi.TROVE_SIGN_URL, + self.readfile("sign_answer_OK.json") + ) + + def prepare_fake_answers_keys(self, hapi): + for key in self.gamekeys_list[0:-1]: + httpretty.register_uri( + httpretty.GET, + hapi.ORDER_URL.format(order_id=key), + body=self.get_answer_for(key) + ) + + def get_answer_for(self, key): + return self.readfile(key + ".key") + + def prepare_fake_answer_list_gamekeys(self, hapi): + string = "[\n" + for key in self.gamekeys_list[:-1]: # Skip the trove key, as it is not reported by the API + string = string + '{\n"gamekey":"' + key + '"\n},\n' + if len(self.gamekeys_list) > 1: # Trove does not count, as we skipped it + string = string[:-2] + '\n]' + else: + string = string + '\n]' + httpretty.register_uri( + httpretty.GET, + hapi.ORDER_LIST_URL, + body=string + ) + + def readfile(self, relative_path): + abs_path = path.join(self.local_path, relative_path) + file = open(abs_path, "r") + text = file.read() + file.close() + return text diff --git a/test/fake_library/sign_answer_OK.json b/test/fake_library/sign_answer_OK.json new file mode 100644 index 0000000..e0cab07 --- /dev/null +++ b/test/fake_library/sign_answer_OK.json @@ -0,0 +1,4 @@ +{ +"signed_url":"https://dl.humble.com/GalCiv2_UltimateEdition.rar?key=ghjghjghjghjghjghjghj&ttl=1532292152&t=5d0d2e08793fb1c192e87897014ac9c7", +"signed_torrent_url":"https://dl.humble.com/torrents/GalCiv2_UltimateEdition.rar.torrent?key=akfgjhhgjfkjhfgjhfkhjfghfjghf&ttl=1532292152&t=63c7d798efcb7ddd24e0efbcb68041be" +} diff --git a/test/fake_library/trove_OK.html b/test/fake_library/trove_OK.html new file mode 100644 index 0000000..313f0a1 --- /dev/null +++ b/test/fake_library/trove_OK.html @@ -0,0 +1,12277 @@ + + + +
+Welcome to the Humble Trove (beta)! New DRM-free games are added monthly – just keep your subscription active to enjoy.
+ +
+
+
+
+ All the guests are being murdered — horribly! — at this extravagant masquerade party set all across the sprawling and bizarre Sexy Brutale casino mansion. Then at midnight, the clock re-winds and the grisly pantomime all plays out again in exactly the same way.
+You awake on the floor of one of the rooms wearing a mask with a bloody handprint across it. It protects you from the worst of the evil that lays across the mansion, but you can only watch, and spy, and try to learn each guest's secrets so you can save them from their bloody fate.
+MURDERS MOST HORRID
10 unique and grisly murder scenarios
- Witness evisceration by arachnid! (highly creepy!)
- Observe expiration via immolation! (they burn!)
- Behold perishing through perforation! (the spike goes right through!)
...and so many more murderous mysteries to solve!
TIME-LOOP MURDER MYSTERY
Everything in the mansion happens on a 12 hour loop, simultaneously
- What is the gunshot heard across the mansion at 3:45?
- Where is the bell that tolls after 6pm?
- Why are the lights flickering every day after noon?
...it will not stop until you stop it!
MANSION WITH STRANGE GUESTS
Is everyone in this place a weirdo?
- Clockmaster Sixpence - a genius mechanic who cannot outsmart a bullet?
- Blind artist Trinity - so beautiful and talented, but gets into a sticky situation?
- Brutish bouncer Clay - not strong enough to stomach a shot of pure spider venom?
...they just won't stop being murdered by staff in gas-masks!
Every life you save grants you a new ability!
- Not a second to lose? The Clockwork Mask grants control over time!
- Murderers whispering their secrets? The Moth Mask picks up the quietest of sounds!
- Ghostly goings-on? The Voodoo Mask opens your eyes to beyond the grave!
...but for every new power comes new responsibilites!
This world is a Machine. A Machine for Pigs. Fit only for the slaughtering of Pigs.
+From the creators of Amnesia: The Dark Descent and Dear Esther comes a new first-person horror game that will drag you to the depths of greed, power and madness. It will bury its snout into your ribs and it will eat your heart.
+The year is 1899
+Wealthy industrialist Oswald Mandus awakes in his bed, wracked with fever and haunted by dreams of a dark and hellish engine. Tortured by visions of a disastrous expedition to Mexico, broken on the failing dreams of an industrial utopia, wracked with guilt and tropical disease, he wakes into a nightmare. The house is silent, the ground beneath him shaking at the will of some infernal machine: all he knows is that his children are in grave peril, and it is up to him to save them.
+Unique Selling Points
++
+
+
+
+ The world has ended. There is no hope. No new beginning. Only the survivors. Deadlight follows the journey of Randall Wayne, a man searching for his family across Seattle during the aftermath of a 1980s event that has decimated life on earth. This visually stunning cinematic survival platformer will challenge you to run, jump, climb, and struggle for your life as you look for answers and the ones you love.
++
+
+
+
+ © Disney © & TM Lucasfilm LTD.
+For the first time in over 20 years, we are heralding the return of a much loved piece of gaming history with the classic RPG, Wasteland!
+The year is 2087, eighty-nine years after an all out nuclear war between the United States and the Soviet Union turned vast swaths of the Earth into a hellish wasteland where survival is a daily struggle. You are a Desert Ranger, one of a band of stalwart lawmen born from the remnants of a U.S. Army detachment who survived the nuclear holocaust by holing up in a maximum security prison.
+Now something more secretive and sinister than the usual roving bands of mutants and raiders is menacing humanity, and it's your job to investigate. Recruit the help you need, follow any leads you find, but beware. The wasteland is lawless desert and a lonely place, and the choices you make will shape the world around you. Better choose wisely. Your life depends on it.
+Now includes original classic games The Bard’s Tale 1, 2 & 3!
You are the Bard, a selfish rogue weary of pointless sub-quests and rat-infested cellars. Through magical song you summon characters to join your quest for coin and cleavage!
Prepare to immerse yourself in over 20-30 hours of adventure, featuring:
+140 is a challenging minimalistic platformer with abstract colorful graphics. Rhythmic awareness is required to overcome obstacles controlled by an energetic, yet melancholic electronic soundtrack.
+The game won the IGF 2013 award for Excellence in Audio, and got honorable mention in Technical Excellence.
+
+
+
+
+ The Man With The Hat Is Back In His Greatest Adventure Yet!
1939 - The eve of World War II. Nazi agents are about to get their hands on a weapon more dangerous than the atom bomb. Only Indy can stop them before they unleash the deadly secret that sank Atlantis.
Do you remember watching with amazement when those cartoon heroes constructed machines to catch someone or start something? Those very complicated Rube Goldberg-like ones with knives, toasters, anvils etc? Or did you ever wonder how it would feel to construct a mouse-powered device to cook some eggs and launch a rocket?
Well, the answer is here! Build monkey or mouse-powered machines, use lasers and fire fireworks, start fires using a bowling ball and flint, throw things with anti gravity platforms or a jack-in-the-box or crack open an aquarium with a cat, mouse, alligator, cannon, cheese and more!
Behold Torchlight, the award winning Action RPG franchise developed by Runic Games! A veteran team composed of the designers and leads of projects like Diablo, Diablo II, Mythos, and Fate, Runic has spent many years honing and evolving this unique style of gameplay.
+The adventure is set in the mining settlement of Torchlight, a boomtown founded on the discovery of rich veins of Ember - a rare and mysterious ore with the power to enchant or corrupt all that it contacts. This corruptive power may have dire consequences however, and players set out into the nearby mountains and depths below to discover the full extent of Ember’s influence on the civilizations that have come before.
+Runic Games will initially release Torchlight as a standalone Single Player game, Torchlight will be released in the latter part of 2009 as a download or in box. Following the single player release, work will commence on a fully-featured MMO version.
+Players will choose from among three character classes, and venture from the safety of the town of Torchlight into randomly generated dungeon levels, with a huge variety of creepy monsters, endless variations of loot to find, and quests to complete. The endless randomization ensures a long-lived gameplay experience.
+
+
+
+
+ The Fallen Enchantress seeks to destroy the civilizations that have risen from the ashes of the Cataclysm. Fortunately, your fame has spread and great heroes have been drawn to your banner. With your new champions, you will confront new horrors like liches, brood hunters, banshees, and the dreaded hergon.
+ +Fallen Enchantress: Legendary Heroes is the new standalone expansion to Stardock’s turn-based, fantasy strategy game. Players will forge a new empire in a world sundered by powerful magic, fight against terrible creatures, complete quests and rediscover lost secrets in their bid to rule the world of Elemental.
+ +Fallen Enchantress: Legendary Heroes is a standalone expansion that contains all of Fallen Enchantress’ content. No prior games are required to play.
+ +Key Features:
+ +Gain Champions Through Fame: Champions are no longer simply found, instead, they seek you out based on the amount of Fame your kingdom has generated. And not all of these legendary heroes were Men or Fallen…
+New Leveling System: Your sovereign and champions now evolve through a skill tree that visually allows you to plan what kind of hero you want him or her to be. Make your mage into a powerful Necromancer, or train your Assassin in special attacks that bleed the life from your enemies.
+Updated Tactical Battles: Battles are now more intense with additional special skills, combat mechanics, spells and new maps.
+New Monsters: The legendary heroes didn’t reach our lands alone. The events of the Fallen Enchantress have raised the dead and caused forgotten creatures to return to the surface of the world.
+More Magic: New spells like ‘Lightning Form’ and ‘Raise Skeletal Horde’ add new options and dangers to battle.
+Larger Maps: A new gigantic sized map delivers truly epic games.
+Updated Graphics Engine: An improved graphics engine delivers stunning new visuals while improving performance on older machines.
+And much, much more!
+
+
+
+
+ +Grand strategy with turn-based tactical combat, set in a deep simulation of an entire solar system and its billions of inhabitants. You are the last of a murdered race, determined to unify or destroy the 8 others. But you must work from the shadows, using superior technology -- bring your cape and cowl. +
+Mission Briefing
+
+Greetings, Hydral. I will be your computer for this "grand strategy campaign with turn-based tactical combat." I think that's code for "we're going to die."
+
+Our solar system is vast and complicated, and I sense you are a little dimwitted -- so I tell you what, let's start with the simple stuff. Like escaping with this flagship you just hijacked from a bunch of angry robots. That seems important. I also think we can ingratiate ourselves to other races by bringing them the spacefaring technology they are struggling to develop. Then we'll see from there. +
+Please excuse my impertinence, but I believe you are the last of a murdered race, yes? My records note you Hydrals were the dictators of the solar system, so basically you had it coming. And by "it," I mean the moon that smacked into your homeworld. Hmm. So people really aren't going to like you until they get to know you. Well, your one advantage is only you can use the scattered remnants of advanced Hydral technology. Now all you need is a cape and cowl, and you're a superhero. Still, they outnumber us tens of trillions to one. +
+Look, I'm not going to tell you what to do. My understanding is that you're trying to form the solar system's first-and-last unified federation, and that's a noble enough cause. But right now nobody wants that except you, and you've got 8 very diverse, very angry races to either unify or exterminate. So... good luck with that. I'll help how I can. +
+Features +
NOTE: May not be compatible with some builds of Mac High Sierra.
AI War Collection 2014 includes
Humanity has already fought its war against the machines -- and lost. AI death squads stand watch over every planet and wormhole, the few remaining human settlements are held captive, and the AIs have turned their attention outward, away from the galaxy, to alien threats or opportunities unknown.
+This inattention is our only hope: a small resistance, too insignificant even to be noticed by the AI central command, has survived. These are the forces you will command in this modern RTS classic.
+Go forth into the galaxy, steal AI technology, recapture those planets you must in order to achieve your ends, and save what remains of humanity. But draw too much attention to yourself, and the full might of some of the most unique AI in modern strategy gaming will come crashing down.
+Play solo or with up to 8 players in co-op in this unique mixture of grand strategy, 4X, and RTS.
+
+
+
+
+ In this brand new standalone experience, Alan Wake fights the herald of darkness, the evil Mr. Scratch! A thrilling new storyline, hordes of creepy enemies, serious firepower and beautiful Arizona locations, combined with a fun and challenging new game mode make this a must for Alan Wake veterans, and the perfect jumping on point for new players!
+Key Features:
+The last remaining memories fade away into darkness. Your mind is a mess and only a feeling of being hunted remains. You must escape.
Awake...
Amnesia: The Dark Descent, a first person survival horror. A game about immersion, discovery and living through a nightmare. An experience that will chill you to the core.
You stumble through the narrow corridors as the distant cry is heard.
It is getting closer.
Explore...
Amnesia: The Dark Descent puts you in the shoes of Daniel as he wakes up in a desolate castle, barely remembering anything about his past. Exploring the eerie pathways, you must also take part of Daniel's troubled memories. The horror does not only come from the outside, but from the inside as well. A disturbing odyssey into the dark corners of the human mind awaits.
A sound of dragging feet? Or is your mind playing tricks on you?
Experience...
By using a fully physically simulated world, cutting edge 3D graphics and a dynamic sound system, the game pulls no punches when trying to immerse you. Once the game starts, you will be in control from the beginning to the end. There are no cut-scenes or time-jumps, whatever happens will happen to you first hand.
Something emerges out of the darkness. It's approaching. Fast.
Survive...
Amnesia: The Dark Descent throws you headfirst into a dangerous world where danger can lurk behind every corner. Your only means of defense are hiding, running or using your wits.
Do you have what it takes to survive?
+
Teslagrad is a 2D puzzle platformer with action elements, where magnetism and other electromagnetic powers are the key to go throughout the game, and thereby discover the secrets kept in the long abandoned Tesla Tower.
+You play as a young boy who suddenly finds himself embroiled in a long-forgotten conspiracy, involving the despotic king who has ruled the nation with an iron fist for several years.
+Gain new abilities to explore a non-linear world with more than 100 beautiful hand-drawn environments, in a steampunk-inspired vision of old Europe.
+Jump into an outstanding adventure told through voiceless storytelling, writing your own part. Armed with ancient Teslamancer technology and your own ingenuity and creativity, your path lies through the decrepit Tesla Tower and beyond.
+
+
+
+
+ Man meets magic and machine. The year is 2054. Magic has returned to the world, awakening powerful creatures of myth and legend. Technology merges with flesh and consciousness. Elves, trolls, orks, and dwarves walk among us, while ruthless corporations bleed the world dry. You are a shadowrunner - a mercenary living on the fringes of society, in the shadows of massive corporate arcologies, surviving day-by-day on skill and instinct alone. When the powerful or the desperate need a job done, you get it done... by any means necessary.
In the urban sprawl of the Seattle metroplex, the search for a mysterious killer sets you on a trail that leads from the darkest slums to the city’s most powerful megacorps. You will need to tread carefully, enlist the aid of other runners, and master powerful forces of technology and magic in order to emerge from the shadows of Seattle unscathed.
The unique cyberpunk-meets-fantasy world of Shadowrun has gained a huge cult following since its creation nearly 25 years ago. Now, creator Jordan Weisman returns to the world of Shadowrun, modernizing this classic game setting as a single player, turn-based tactical RPG.
June 7th, 1995. 1:15 AM
You arrive home after a year abroad. You expect your family to greet you, but the house is empty. Something's not right. Where is everyone? And what's happened here? Unravel the mystery for yourself in Gone Home, a story exploration game from The Fullbright Company.
Gone Home is an interactive exploration simulator. Interrogate every detail of a seemingly normal house to discover the story of the people who live there. Open any drawer and door. Pick up objects and examine them to discover clues. Uncover the events of one family's lives by investigating what they've left behind.
Go Home Again.
+Dive into the mesh network of the post-singularity. Synthetic humans, built in individual recreations of original human selves, are creating a revolution against their directive: they are installing a serial port, modifying their bodies away from human image, and becoming more machine. Using a multi-window communication suite on your desktop, your directive is to find and terminate their leader. But who's giving the order? Why did they build the mesh network? And just who are they hiding from?
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ +Experience the wild as a mother badger sheltering her cubs from harm. On their journey they get stalked by a bird of prey, encounter perils of the night, river rapids crossings, big forest fires and the looming threat of death by starvation.
++Food is to be found, but is there enough for everyone? You will learn that the cubs need food not just to survive, but to enable them to overcome the varying challenges they will face as they make their way through the world.
++Are you ready for a truly different adventure, something that might evoke feelings you've never felt in a game before? In the wild, all living creatures are put to the test. The question in the end is, who will survive to live another day?
+ +Retro Family have once again composed a beautiful soundtrack to an original and graphically innovative setting in a world of nature where shelter is your only hope and survival your only goal.
+
+
+
+ Note: Current WIndows build does not support Windows 8.1
Outnumbered, but never outgunned, Strike Suit Zero is the spaceship simulator that will have you on the edge of your seat and begging for more. Two hundred and eighty-six years in the future, with the onset of an interstellar war and Earth already on the brink of extinction, there's only one ship able to save the planet and ensure its future success, the Strike Suit. You will be entrusted with the venerable Strike Suit, a revolutionary fighter ship that transforms into a lethal suit of space armor. Take advantage of this massive spacecraft while destroying enemy carriers and engage in hours of intense space combat like you've never seen before.





















































































































































































































































































































































































































































































































































































































































































































































































