diff --git a/README.md b/README.md index c9ea2ea..4fe9231 100644 --- a/README.md +++ b/README.md @@ -17,13 +17,7 @@ Usage **This may not be accurate. Run mGameBits -h to see the required arguments.** -You can run the script by supplying either a URL to a MobyGames game page or simply the game name. - - ./mGameBits.py - -or - - ./mGameBits.py [] [] [] + ./mGameBits.py -c -l -f Note: CONSOLE should be equal to the one used on MG, which usually will be fairly similar to what you would expect, but in some cases may be different. For example, the Nintendo 64 has an id of n64. I recommend just trying it and seeing what happens: it will probably work in 99% of cases without much thought. diff --git a/mGameBits.py b/mGameBits.py index 7a0e73c..7b5174d 100755 --- a/mGameBits.py +++ b/mGameBits.py @@ -1,168 +1,334 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- +"""Usage: + mGamebits.py -f [-l ] [-c ] + [--no-screenshots] + mGamebits.py --list-platforms + mGamebits.py (-h | --help) + mGamebits.py (-v | --version) -# Really early initial implementation of game info grabber - there will be tons of bugs, beware -# MCn +Creates pretty printed bbcode for games. + +Options: + -h --help + -v --version + --list-platforms List supported consoles. + -f FORMAT --format=FORMAT Game format. + -l LANGUAGE --language=LANGUAGE Game Language. Default is English. + -c CONSOLE --console=CONSOLE Game platform. Defaults to PC. + --no-screenshots Don't take screenshots. + +Examples: + mGamebits.py "Command and Conquer" -f ISO + mGamebits.py "Pokemon Red" -f ROM -c GB + mGamebits.py "https://www.mobygames.com/game/playstation/metal-gear-solid" -f ISO +""" +from __future__ import (absolute_import, division, + print_function, unicode_literals) +from collections import namedtuple +from functools import partial +try: + from StringIO import StringIO +except ImportError: + from io import StringIO # py3k +from pprint import pprint -from pyquery import PyQuery as pq -import argparse -import json -import re import requests -import sys +from bs4 import BeautifulSoup as bs +from docopt import docopt + + +TEMPLATE_MAIN = """\ +Game Name: {title} +Released: {date} +Source: {source} +Language: {language} +Game Genre: {genre} + +[b]Review:[/b] +{review} + +[b]Description[/b] +[quote] +{description} +[/quote] + +""" + +TEMPLATE_EMULATOR = """\ +[b]Emulation:[/b] +[quote] +The best emulator to use is {emulator}. +{link} +[/quote] + +""" + +TEMPLATE_IMAGES = """\ +[b]Screenshots[/b] +{screenshots} +Screenshot gallery: {gallery} + +Cover image: {cover} +""" + +_info = namedtuple('info', ('emulator', 'link', 'id')) +CONSOLE_TO_EMULATOR_MAP = { + 'PS1': _info('EPSXE', + 'http://www.epsxe.com/download.php', + 6), + 'PS2': _info('PCSX2', + 'http://pcsx2.net/download.html', + 7), + 'NES': _info('FCEUX', + 'http://www.fceux.com/web/home.html', + 22), + 'SNES': _info('ZSNES', + 'http://www.zsnes.com/index.php?page=files', + 15), + 'N64': _info('Project 64', + 'http://www.pj64-emu.com/', + 9), + 'GB': _info('Virtual Boy Advanced', + 'http://vba.ngemu.com/downloads.shtml', + 10), + 'GBC': _info('Virtual Boy Advanced', + 'http://vba.ngemu.com/downloads.shtml', + 11), + 'GBA': _info('Virtual Boy Advanced', + 'http://vba.ngemu.com/downloads.shtml', + 12), + 'GC': _info('Dolphin', + 'http://www.dolphin-emulator.com/download.html', + 14), + 'WII': _info('The best Emulator to use is Dolphin.', + 'http://www.dolphin-emulator.com/download.html', + 82), + 'DS': _info('DSEmu', + 'http://dsemu.oopsilon.com/', + 44), + 'DOS': _info('DOSBox', + 'http://www.dosbox.com/download.php?main=1', + 2)} + + +class GameNotFoundError(Exception): + pass + + +class MobyGetter(object): + BASE_URL = 'https://www.mobygames.com' + HEADERS = {'user-agent': 'PrettyPrinter/1.0.0'} + + get = partial(requests.get, + headers=HEADERS) + + @classmethod + def make_absolute(cls, relative_url): + return cls.BASE_URL + relative_url + + +class Screenshots(MobyGetter): + RESOURCE = '/screenshots' + + def __init__(self, url, number=3): + self.url = url + self.RESOURCE + self.number = number + + def urls(self): + response = self.get(self.url) + if not response.ok: + raise GameNotFoundError( + "Can't get screenshots at %s" % self.url) + return self._extract_image_urls(bs(response.content)) + + def _extract_image_urls(self, tree): + urls = [a['style'] for a in + tree.select('a.thumbnail-image')][:self.number] + # clean string looking like 'background-image:url(RELATIVE_URL);' + urls = [url.split('(', 1)[1].split(')', 1)[0] for url in urls] + # thumbnail => full + urls = [self.thumbnail_to_full(url) for url in urls] + # relative => absolute + return [self.make_absolute(url) for url in urls] + + @staticmethod + def thumbnail_to_full(thumbnail_url): + return thumbnail_url.replace('/s/', '/l/') + + +class Game(MobyGetter): + def __init__(self, url): + self.url = url + response = self.get(url) + if not response.ok: + raise GameNotFoundError('Could not get url %s' % url) + self.tree = bs(response.content) + + @property + def title(self): + return self.tree.select('h1.niceHeaderTitle a')[0].text + + @property + def screenshots(self): + return Screenshots(self.url).urls() + @property + def description(self): + main_text = self.tree.select('.col-md-8.col-lg-8')[0] -# Codes used in search urls on MobyGames - add more if necessary -# MG_CONSOLE_CODES = {'pc': 3, 'gb': 10, 'gbc': 11, 'gba': 12} -# MG_CONSOLE_SLUGS = {'N64': 'N64', 'GBA': 'gameboy-advance', 'GBC': 'gameboy-color', 'Dreamcast': 'Dreamcast', 'gamecube': 'gamecube', 'Xbox': 'xbox', 'xbox360': 'xbox360', 'Wii U': 'Wii-u', 'Ps2': 'PS2', 'PS3': 'Ps3','Ps1': 'Playstation', 'genesis': 'genesis', 'android': 'android', 'PC': 'windows', 'nes': 'nes', '3ds': '3ds', 'DS': 'nintendo-ds', 'DSI': 'nintendo-dsi', 'snes': 'snes', 'iphone': 'iphone', 'wii': 'wii', 'mac':'macintosh', 'gameboy': 'gameboy'} + for br in main_text.select('br'): + br.replaceWith('\n') + main_text = main_text.text + try: + description, __ = main_text.split('[edit description') + except ValueError: + description, __ = main_text.split('[more descriptions') + + return description.split('Description', 1)[1].strip() + + @property + def genre(self): + return self.tree.select('#coreGameGenre a')[0].text + + @property + def released(self): + return self.tree.select('#coreGameRelease ' + 'a[href*="release-info"]')[0].text + + @property + def cover(self): + thumbnail = self.tree.select('#coreGameCover img')[0]['src'] + return thumbnail.replace('/small/', '/large/') + + @property + def review(self): + return self.url + '/mobyrank' + + @property + def gallery(self): + return self.url + '/screenshots' + + +class MobyGames(MobyGetter): + def __init__(self): + pass + + def search(self, name, console=None): + RESOURCE = '/search/quick' + + params = {'q': name} + if console: + try: + mobygames_console_id = CONSOLE_TO_EMULATOR_MAP[console].id + except KeyError: + raise GameNotFoundError( + "I don't know the correct console code for '{}'. " + "Try again without specifying the console." + .format(console)) + params['p'] = mobygames_console_id + + response = self.get(self.BASE_URL + RESOURCE, + params=params) + if not response.ok: + raise GameNotFoundError(response.request_url) + + url = self._extract_result(response.content) + return Game(url) + + @staticmethod + def _extract_result(content): + tree = bs(content) + + game_a_elements = tree.select('#searchResults ' + '.searchSubSection ' + '.searchResult ' + '.searchTitle ' + 'a') + game_urls = [a['href'] for a in + game_a_elements] + if game_urls: + return game_urls[0] # most precise match + else: + raise GameNotFoundError('No matching urls found') + + +class ImageUploadError(Exception): + pass + + +def upload(url): + BASE_URL = 'https://images.baconbits.org' -def upload_image(url): try: - # dealing with pesky relative url - if 'http://' not in url: - url = 'https://www.mobygames.com/' + url - # Check for weird url bug where sometimes the image would have two http:// - if '/https://' in url or '/http://' in url: - url = url[len('https://www.mobygames.com/'):] - # Remove double slashes - url = url.replace('com//', 'com/') - headers = {"Authorization": "Client-ID d656b9b04c8ff24"} - params = {"image": url} - r = requests.post("https://api.imgur.com/3/image", headers=headers, params=params) - return json.loads(r.content)["data"]["link"] - except: - return '' - - -def main(): - parser = argparse.ArgumentParser() - parser.add_argument("url", help="url of a mobygames game page") - parser.add_argument("--source", help="source/format of the game") - parser.add_argument("--language", help="language of the game") - parser.add_argument("--console", help="the console this game plays on - used for emulator suggestion information", choices=["PS1", "PS2", "NES", "SNES", "N64", "GB", "GBC", "GBA", "GC", "WII", "DS", "DOS"]) - args = parser.parse_args() - - if 'http' in args.url: - game_page = args.url + j = requests.post(BASE_URL + '/upload.php', + data={'url': url}).json() + except ValueError: + raise ImageUploadError("Failed to upload '%s'!" % url) + + if 'ImgName' in j: + return BASE_URL + '/images/' + j['ImgName'] else: - print 'Search is currently not enabled, it needs to be fixed. Try passing in a URL.' - return + raise ImageUploadError("Failed to upload '%s'!" % url, + repr(j)) - page = pq(url=game_page).make_links_absolute() - proper_game_title = page('.niceHeaderTitle a').eq(0).text() +def is_url(s): + if any(s.startswith(substr) for substr in ('http://', 'https://')): + return True + else: + return False - date = page('#coreGameRelease a[href*="release-info"]').text() - # NOTE: u'\xa0' is   - replace it with a space - genre = page('#coreGameGenre a[href*="genre"]').eq(0).text().replace(u'\xa0', ' ') +def main(name, format, language, console, no_screenshots): + out = StringIO() # print everything at the end - review = game_page + '/mobyrank' + if is_url(name): + g = Game(name) + else: + g = MobyGames().search(name, console) - page = pq(url='http://www.mobygames.com/game/windows/dragon-age-origins') - dirty_description_text = pq(page('h2:contains("Description")').parent().html().replace('
', '\n')).text() - description_text = re.search(r'Description\s(.*)\s\[ edit', dirty_description_text, re.DOTALL).group(1) + print(TEMPLATE_MAIN.format(title=g.title, + date=g.released, + source=format, + language=language, + genre=g.genre, + review=g.review, + description=g.description), + file=out) - # Get box art - game_box_data = page('#coreGameCover img') - if game_box_data: - img_url = game_box_data.attr('src').replace('small', 'large') - imgur_game_box_url = upload_image(img_url) - else: - imgur_game_box_url = None - - # Get screenshots - screenshot_gallery_url = game_page + '/screenshots' - screenshot_page = pq(url=screenshot_gallery_url).make_links_absolute() - screenshot_images = screenshot_page('.mobythumbnail img') - screenshot_one_url, screenshot_two_url = None, None - if len(screenshot_images) > 0: - screenshot_one_url = upload_image(screenshot_images.eq(0).attr('src').replace('/s/', '/l/')) - if len(screenshot_images) > 1: - screenshot_two_url = upload_image(screenshot_images.eq(1).attr('src').replace('/s/', '/l/')) - - # Print everything - print "Game Name: " + proper_game_title - print "Released: " + date - if args.source: - print "Source: " + args.source - if args.language: - print "Language: " + args.language - print "Game Genre: " + genre + "\n" - - print "[b]Review:[/b] " + review + "\n" - - print "[b]Description:[/b] " - print "[quote]" - print description_text - print "[/quote]" - print "" - - if len(sys.argv) > 2: - # Emulator Suggestion - if args.console == "PS1": - print "[b]Emulation:[/b]" - print "[quote]The best Emulator to use is EPSXE." - print "http://www.epsxe.com/download.php[/quote]" - elif args.console == "PS2": - print "[b]Emulation:[/b]" - print "[quote]The best Emulator to use is PCSX2." - print "http://pcsx2.net/download.html[/quote]" - elif args.console == "NES": - print "[b]Emulation:[/b]" - print "[quote]The best Emulator to use is FCEUX." - print "http://www.fceux.com/web/home.html[/quote]" - elif args.console == "SNES": - print "[b]Emulation:[/b]" - print "[quote]The best Emulator to use is ZSNES." - print "http://www.zsnes.com/index.php?page=files[/quote]" - elif args.console == "N64": - print "[b]Emulation:[/b]" - print "[quote]The best Emulator to use is Project 64." - print "http://www.pj64-emu.com/[/quote]" - elif args.console == "GB": - print "[b]Emulation:[/b]" - print "[quote]The best Emulator to use is Virtual Boy Advanced." - print "http://vba.ngemu.com/downloads.shtml[/quote]" - elif args.console == "GBC": - print "[b]Emulation:[/b]" - print "[quote]The best Emulator to use is Virtual Boy Advanced." - print "http://vba.ngemu.com/downloads.shtml[/quote]" - elif args.console == "GBA": - print "[b]Emulation:[/b]" - print "[quote]The best Emulator to use is Virtual Boy Advanced." - print "http://vba.ngemu.com/downloads.shtml[/quote]" - elif args.console == "GC": - print "[b]Emulation:[/b]" - print "[quote]The best Emulator to use is Dolphin." - print "http://www.dolphin-emulator.com/download.html[/quote]" - elif args.console == "WII": - print "[b]Emulation:[/b]" - print "[quote]The best Emulator to use is Dolphin." - print "http://www.dolphin-emulator.com/download.html[/quote]" - elif args.console == "DS": - print "[b]Emulation:[/b]" - print "[quote]The best Emulator to use is DSEmu." - print "http://dsemu.oopsilon.com/[/quote]" - elif args.console == "DOS": - print "[b]Emulation:[/b]" - print "[quote]The best Emulator to use is DOSBox." - print "http://www.dosbox.com/download.php?main=1[/quote]" - - if screenshot_one_url: - print "\n[b]Screenshots:[/b]\n" - print "[img]{0}[/img]".format(screenshot_one_url) - if screenshot_two_url: - print "[img]{0}[/img]".format(screenshot_two_url) - if screenshot_one_url: - print "Screenshot gallery: " + screenshot_gallery_url - - if imgur_game_box_url: - print "\n\nCover image: " + imgur_game_box_url - - print "\n-------------------------------------------------\n" + if console: + emu = CONSOLE_TO_EMULATOR_MAP.get(console.upper(), None) + if not emu: + raise ValueError('Unknown Console %s' % console) + print(TEMPLATE_EMULATOR.format(link=emu.link, + emulator=emu.emulator), + file=out) + + if not no_screenshots: + screenshots = "\n".join("[img]{}[/img]".format(upload(url)) + for url in g.screenshots) + + print(TEMPLATE_IMAGES.format( + screenshots=screenshots, + gallery=g.gallery, + cover=upload(g.cover)), file=out) + + print('-' * 80, file=out) + print(out.getvalue()) if __name__ == '__main__': - main() + arguments = docopt(__doc__, version='Pythonbits {}'.format('1.0.1')) + defaults = {'console': None, 'language': 'English'} + + if arguments['--list-platforms']: + pprint(CONSOLE_TO_EMULATOR_MAP) + exit(0) + + main(arguments[''], + arguments['--format'], + arguments['--language'] or defaults['language'], + arguments['--console'] or defaults['console'], + arguments['--no-screenshots']) diff --git a/requirements.txt b/requirements.txt index 909a916..4d2cde5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,3 @@ -cssselect==0.9.1 -lxml==3.4.2 -pyquery==1.2.9 -requests==2.5.1 +beautifulsoup4>=4.3.2 +requests>=2.6.2 +docopt>=0.6.2