Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Upgrade to the latest version of python! #4

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
26 changes: 14 additions & 12 deletions README.rst
Expand Up @@ -2,27 +2,26 @@
bggcli - Command Line Interface for BoardGameGeek.com
=====================================================

.. image:: https://travis-ci.org/syllant/bggcli.svg?branch=master
:target: https://travis-ci.org/syllant/bggcli


.. image:: https://coveralls.io/repos/syllant/bggcli/badge.svg?branch=master
:target: https://coveralls.io/r/syllant/bggcli?branch=master

Introduction
============


``bggcli`` is a Command Line Interface providing automation for tedious tasks on
`BoardGameGeek <http://www .boardgamegeek.com>`__ (aka BGG). It relies on the Web UI and not on the
`official API <https://www.boardgamegeek.com/wiki/page/BGG_XML_API2>`__ which doesn't offer all features available.
Originally written by Sylvain Francois and updated for Python 3.6 and the 2018 BGG interface by Greg Smith.

Only 3 operations are implemented at this time:

Updated for BGG 2018

* bulk import/update for your game collection from a CSV file
* bulk delete from a CSV file
* bulk export as a CSV file, WITH version information (game's version is missing in the default export)

Not tested with BGG 2018

* bulk delete from a CSV file

Warning:

* Use it at your own risks, you may damage your game collection by doing mistakes! Ensure you have a backup of you
Expand All @@ -36,11 +35,12 @@ Warning:

Installation
============
Python 2.7 is required.
Python 3.6 is required.

::

pip install bggcli
pip install bggcli[2018]


Usage
=====
Expand Down Expand Up @@ -126,7 +126,8 @@ Ideas for future versions
Here are some ideas of additional tasks that could be implemented:

* Generic import for collections, based on game names and not on the BGG internal identifier. A confirmation would be
required for each ambiguous name to choose among matching games provided by BGG
required for each ambiguous name to choose among matching games provided by BGG, this is done by BGGUPLOAD
available on github.
* Update/Delete for plays
* Update/Delete for forum subscriptions

Expand All @@ -136,8 +137,9 @@ Links
* *BoardGameGeek*: http://www.boardgamegeek.com
* *Officiel XML API 2*: https://www.boardgamegeek.com/wiki/page/BGG_XML_API2
* *boardgamegeek - A Python API for boardgamegeek.com*: https://github.com/lcosmin/boardgamegeek
* *bggupload - This program helps you interactively find the BGGID from a partial name or inexact match.*: https://github.com/HiGregSmith/bggupload

Final note
Final note (by Sylvain Francois)
==========

Does it really deserve such a development? Probably not, but my second goal was to discover the Python ecosystem!
96 changes: 96 additions & 0 deletions bggcli/__init__.py
@@ -1,3 +1,6 @@
# Updated BGG_SUPPORTED_FIELDS
# Added BGG_EXPORTED_FIELDS and BGG_GAMEDB_FIELDS

import os

if os.environ.get('CI') == 'true':
Expand All @@ -18,3 +21,96 @@
'pp_currency', 'currvalue', 'cv_currency', 'acquisitiondate',
'acquiredfrom', 'quantity', 'privatecomment',
'_versionid']

BGG_SUPPORTED_FIELDS = ['own',
'want', 'wanttobuy', 'wanttoplay', 'prevowned',
'preordered',
'fortrade', 'conditiontext', # these must be in this order
'wishlist', 'wishlistpriority', 'wishlistcomment', # these must be in this order
'comment', 'rating',
'pricepaid', 'currvalue',
'acquisitiondate', 'acquiredfrom', 'quantity', 'privatecomment',
'haspartslist', 'wantpartslist','publisherid', 'imageid',
'year', 'language', 'other',
#'cv_currency', 'pp_currency',
'objectname',
'objectid', '_versionid', 'invlocation'
]

# More fields in the add/edit collection dialog:
# Inventory Date
# Inventory Location
# Barcode
# Aquisition Date
# Inventory Date
# Inventory Location
# Custom Title
# Custom Image Id
# Publisher Id
# Year
# Other
# Barcode

# list of headings in direct downloaded collection.csv
BGG_EXPORTED_FIELDS = [
'objectname',
'objectid',
'rating',
'numplays',
'weight',
'own',
'fortrade',
'want',
'wanttobuy',
'wanttoplay',
'prevowned',
'preordered',
'wishlist',
'wishlistpriority',
'wishlistcomment',
'comment',
'conditiontext',
'haspartslist',
'wantpartslist',
'numowned',
'publisherid',
'imageid',
'year',
'language',
'other',
'pricepaid',
'pp_currency',
'currvalue',
'cv_currency',
'acquisitiondate',
'acquiredfrom',
'quantity',
'privatecomment',
'version_publishers',
'version_languages',
'version_yearpublished',
'version_nickname',
]

BGG_GAMEDB_FIELDS = [
'collid',
'baverage',
'average',
'avgweight',
'rank',
'objecttype',
'originalname',
'minplayers',
'maxplayers',
'playingtime',
'maxplaytime',
'minplaytime',
'yearpublished',
'bggrecplayers',
'bggbestplayers',
'bggrecagerange',
'bgglanguagedependence',
'itemtype',
]

#print(set(BGG_EXPORTED_FIELDS)-set(BGG_SUPPORTED_FIELDS))
26 changes: 18 additions & 8 deletions bggcli/commands/collection_export.py
Expand Up @@ -22,7 +22,12 @@
<file> The CSV file to generate
"""
import csv
import urllib2
import codecs
try:
from urllib2 import quote, Request, urlopen
except:
from urllib.request import Request, urlopen
from urllib.parse import quote
import time
import sys
import xml.etree.ElementTree as ET
Expand Down Expand Up @@ -57,8 +62,8 @@ def execute(args, options):
# Use XML2 API, see https://www.boardgamegeek.com/wiki/page/BGG_XML_API2#Collection
# Default CSV export doesn't provide version info!
url = '%s/xmlapi2/collection?username=%s&version=1&showprivate=1&stats=1' \
% (BGG_BASE_URL, urllib2.quote(login))
req = urllib2.Request(url, None, {'Cookie': '%s=%s' % (BGG_SESSION_COOKIE_NAME, auth_cookie)})
% (BGG_BASE_URL, quote(login))
req = Request(url, None, {'Cookie': '%s=%s' % (BGG_SESSION_COOKIE_NAME, auth_cookie)})

# Get a BadStatusLine error most of times without this delay!
# Related to Selenium, but in some conditions that I have not identified
Expand All @@ -75,7 +80,8 @@ def execute(args, options):
if xml_file == 'true':
xml_file_path = write_xml_file(response, dest_path)
Logger.info("XML file save as %s" % xml_file_path)
source = open(xml_file_path, 'rU')
#source = open(xml_file_path, 'rU')
source = codecs.open(xml_file_path, mode='rb', encoding='utf-8', errors='replace')
else:
source = response

Expand All @@ -93,7 +99,7 @@ def execute(args, options):


def default_export(req):
response = urllib2.urlopen(req)
response = urlopen(req)

if response.code == 202:
Logger.info('Export is queued, will retry in %ss' % EXPORT_QUERY_INTERVAL)
Expand All @@ -105,7 +111,8 @@ def default_export(req):

# Write response in a text file otherwise
try:
with open(ERROR_FILE_PATH, "wb") as error_file:
#with open(ERROR_FILE_PATH, "wb") as error_file:
with codecs.open(ERROR_FILE_PATH, mode='wb', encoding='utf-8', errors='replace') as error_file:
error_file.write(response.read())
Logger.error("Unexpected response, content has been written in %s" % ERROR_FILE_PATH)
except Exception as e:
Expand All @@ -117,14 +124,17 @@ def default_export(req):

def write_xml_file(response, csv_dest_path):
dest_path = '.'.join(csv_dest_path.split('.')[:-1]) + '.xml'
with open(dest_path, "wb") as dest_file:
#with open(dest_path, "wb") as dest_file:
with codecs.open(dest_path, mode='wb', encoding='utf-8', errors='replace') as dest_file:
dest_file.write(response.read())

return dest_path


def write_csv(source, dest_path):
with open(dest_path, "wb") as dest_file:
#with open(dest_path, "wb") as dest_file:
with codecs.open(dest_path, mode='w', encoding='utf-8', errors='replace') as dest_file:
BGG_SUPPORTED_FIELDS.extend(['pp_currency','cv_currency'])
csv_writer = csv.DictWriter(dest_file, fieldnames=BGG_SUPPORTED_FIELDS,
quoting=csv.QUOTE_ALL)
# csv_writer.writeheader() use quotes
Expand Down
78 changes: 70 additions & 8 deletions bggcli/commands/collection_import.py
Expand Up @@ -22,30 +22,92 @@
Arguments:
<file> The CSV file with games to import
"""
# Updated for BGG 2018
import sys
from bggcli.commands import check_file
from bggcli.ui.gamepage import GamePage
from bggcli.ui.loginpage import LoginPage
from bggcli.util.csvreader import CsvReader
from bggcli.util.logger import Logger
from bggcli.util.webdriver import WebDriver
from selenium.common.exceptions import WebDriverException

import traceback

LOOPLIMIT = 5
def execute(args, options):
print('Executing!')
login = args['--login']

file_path = check_file(args)

csv_reader = CsvReader(file_path)
csv_reader.open()
rows = []
# try:
Logger.info("Parsing input file '{}'...".format(file_path))
csv_reader.iterate(lambda row: rows.append(row))
#Logger.info("Found %s games to put in collection..." % csv_reader.rowCount)
rows.reverse()
firstrow = rows[0]
loop = 0

Logger.info("Importing {} games to collection of '{}' ...".format(csv_reader.rowCount,login))
while rows:
try:
with WebDriver('collection-import', args, options) as web_driver:
if not LoginPage(web_driver.driver).authenticate(login, args['--password']):
sys.exit(1)
#input("Kill Firefox, then Press Enter to continue...")
game_page = GamePage(web_driver.driver)
while rows:
row = rows.pop()
if firstrow is None or firstrow == row:
loop += 1
if loop >= LOOPLIMIT:
Logger.info("Loop limit of {} reached.".format(loop))
return
Logger.info('Loop {} (maximum {})'.format(loop,LOOPLIMIT))
if rows:
firstrow = rows[0]
Logger.info('First assigned {}'.format(firstrow['objectname']))
else:
firstrow = None
Logger.info('First assigned None')

Logger.info('(BGGID {}) Name: {} ({} game left)'.format(row['objectid'],row['objectname'],len(rows)+1))

try:
val = game_page.update(row)
Logger.info('update returned {}'.format(val))

if val:
#Logger.info('Updated (BGGID {0}) "{1}"'.format(row['objectid'],row['objectname']))
Logger.info('(BGGID {}) Name: {} UPDATED!'.format(row['objectid'],row['objectname'],len(rows)))
# ({} game left)
else:
rows.insert(0,row)
Logger.info('returned False??, back in queue.'.format(len(rows))) # ({} game left)

Logger.info("Importing games for '%s' account..." % login)
except WebDriverException:
rows.insert(0,row)
Logger.info('Exception occurred, back in queue.'.format(len(rows))) # ({} left)
Logger.info('WebDriverException occurred, restarting browser.')
raise

except Exception as e:
traceback.print_exc(limit=2, file=sys.stdout)

with WebDriver('collection-import', args, options) as web_driver:
if not LoginPage(web_driver.driver).authenticate(login, args['--password']):
sys.exit(1)
rows.insert(0,row)
Logger.info('Exception occurred, back in queue.'.format(len(rows))) # ({} left)

Logger.info("Importing %s games..." % csv_reader.rowCount)
game_page = GamePage(web_driver.driver)
csv_reader.iterate(lambda row: game_page.update(row))
Logger.info("Import has finished.")
#badrows.append(row)
# for row in rows:
# try:
# game_page.update(row)
# except:
# badrows.append(row)
# print
except WebDriverException:
pass
Logger.info("Import has finished.")