From 9ec059873772b201ff8fc3b124f6d2b942493e84 Mon Sep 17 00:00:00 2001 From: zlg Date: Sun, 18 Mar 2018 21:48:41 -0700 Subject: Flesh out filter types and ownership status It's time for a refactor to a module; the functionality and interface are clashing. --- README.mdown | 43 +++++++++----- scripts/helpers.sh | 19 ++----- vgstash | 164 ++++++++++++++++++++++++++++++++++++----------------- 3 files changed, 144 insertions(+), 82 deletions(-) diff --git a/README.mdown b/README.mdown index 9425866..ace50e6 100644 --- a/README.mdown +++ b/README.mdown @@ -150,29 +150,44 @@ about after you've added your massive 500 game collection? Nobody wants to sort through screenfulls of text. That's where filtering comes in. The `list` command accepts the following filters: -* **completed** - List games that have been completed. +* **arcade** + Games you own that cannot be beaten. + +* **backlog** + Games you own that have not been beaten or completed. + +* **borrowing** + Games you don't own and are playing. + +* **complete** + Games that have been completed. + +* **digital** + Games you own digitally. * **done** -List games that have been beaten *or* completed. + Games that have been beaten *or* completed. -* **owned** -List games that you own. +* **incomplete** + Games you own that have been beaten, but not completed. -* **unowned** -List games that you don't own. +* **new** + Games you own and haven't played yet. -* **wishlist** -List games that you don't own AND are fresh. +* **owned** + Games you own. + +* **physical** + Games you own physically. * **playlog** -List games that are marked 'in-progress', that you also own. + Games you own that are marked 'playing'. -* **incomplete** -List games that have been beaten, but not completed, that you also own. +* **unowned** + Games you don't own. -* **backlog** -List games that have not been beaten or completed, that you also own. +* **wishlist** + Games you don't own *and* haven't played yet. ## Update diff --git a/scripts/helpers.sh b/scripts/helpers.sh index 174305f..3b2d335 100644 --- a/scripts/helpers.sh +++ b/scripts/helpers.sh @@ -3,14 +3,6 @@ # This is a set of helper bash functions that are too small for their own file, # but useful enough to be worth `source`ing in your bashrc. -# Reports how many physical games you own -function vgphys() { - # Note: This assumes the system has an asterisk, "VC", or "Steam" in its name - # Change this to reflect how you differentiate physical and digital games. - # TODO: Add 'digital' ownership option to obsolete the regex - vgstash list owned | cut -d '|' -f 1-3,5 | grep -viE '\*| VC|Steam' | wc -l -} - # Faster general searching function vgsrc() { case $# in @@ -21,7 +13,10 @@ function vgsrc() { vgstash list "$1" | grep -iE "$2" ;; *) - echo "Dumbass, search for something." + echo "usage: vgsrc [game name]" + echo " or vgsrc [view name] [game name]" + echo + echo "Ex: vgsrc physical Borderlands" ;; esac } @@ -30,9 +25,3 @@ function vgsrc() { function vgadd() { vgstash add "$@" } - -# Quick way to update a game -# TODO: Put this in a better place -function vgup() { - "$HOME"/projects/vgstash/scripts/updater.sh "$@" -} diff --git a/vgstash b/vgstash index 54101f1..365140f 100755 --- a/vgstash +++ b/vgstash @@ -2,6 +2,7 @@ # TODO: Consider putting all help messages into a dict for easier management # TODO: Decide on docstring delimiter and stick to it +# TODO: refactor import sys import os @@ -10,6 +11,8 @@ import argparse import yaml import subprocess # to handle the piping use case +__version__ = '0.2' + DB_LOCATION = '' OWNERSHIP = 1 PROGRESS = 1 @@ -29,15 +32,15 @@ def set_env(): # This makes outside-scope referencing and assignment possible global DB_LOCATION, OWNERSHIP, PROGRESS, TABLE_WIDTH - # Precedence = $VGSTASH_DB_LOCATION, $HOME/.vgstash.db - DB_LOCATION = os.getenv('VGSTASH_DB_LOCATION', os.path.join(os.getenv('HOME'), '.vgstash.db')) - print(DB_LOCATION) + # Precedence = $VGSTASH_DB_LOCATION, $HOME/.vgstash.db, ./.vgstash.db + DB_LOCATION = os.getenv('VGSTASH_DB_LOCATION', os.path.join(os.getenv('HOME', '.'), '.vgstash.db')) - try: - assert(os.path.isfile(DB_LOCATION) and os.path.exists(DB_LOCATION)) - except AssertionError: - print("VGSTASH_DB_LOCATION is not a file. Unset it to fall back to $HOME/.vgstash.db, or correct the environment variable.") - sys.exit() + # Can't decide what to do with this yet; the first run of vgstash doesn't let you init the db if we uncomment this + # try: + # assert(os.path.isfile(DB_LOCATION) and os.path.exists(DB_LOCATION)) + # except AssertionError: + # print("VGSTASH_DB_LOCATION is not a file. Unset it to fall back to $HOME/.vgstash.db, or correct the environment variable.") + # sys.exit() OWNERSHIP = int(os.getenv('VGSTASH_DEFAULT_OWNERSHIP', OWNERSHIP)) try: @@ -73,7 +76,7 @@ def init_db(args): print("The database could not be created and/or connected to.") sys.exit() except AssertionError: - print("The database already exists! Delete '%s' and init the database again." % DB_LOCATION) + print("The database already exists! Delete or move '%s' and init the database again." % DB_LOCATION) sys.exit() # Now let's run a bunch of queries! Fun... @@ -87,15 +90,19 @@ def init_db(args): print("Table created.") # TODO: Consider executescript() conn.execute("CREATE VIEW allgames AS SELECT rowid,* FROM games ORDER BY system,title ASC;") + conn.execute("CREATE VIEW arcade AS SELECT rowid,* FROM games WHERE ownership >= 1 AND progress = -1 ORDER BY system,title ASC;") + conn.execute("CREATE VIEW backlog AS SELECT rowid,* FROM games WHERE ownership >= 1 AND progress < 2 ORDER BY system,title ASC;") + conn.execute("CREATE VIEW borrowing AS SELECT rowid,* FROM games WHERE ownership = 0 AND progress = 1 ORDER BY system,title ASC;") conn.execute("CREATE VIEW complete AS SELECT rowid,* FROM games WHERE progress = 3 ORDER BY system,title ASC;") - conn.execute("CREATE VIEW backlog AS SELECT rowid,* FROM games WHERE ownership = 1 AND progress < 2 ORDER BY system,title ASC;") + conn.execute("CREATE VIEW digital AS SELECT rowid,* FROM games WHERE ownership >= 2 ORDER BY system,title ASC;") conn.execute("CREATE VIEW done AS SELECT rowid,* FROM games WHERE progress >= 2 ORDER BY system,title ASC;") - conn.execute("CREATE VIEW fresh AS SELECT rowid,* FROM games WHERE progress = 0 ORDER BY system,title ASC;") - conn.execute("CREATE VIEW incomplete AS SELECT rowid,* FROM games WHERE ownership = 1 AND progress = 2 ORDER BY system,title ASC;") - conn.execute("CREATE VIEW owned AS SELECT rowid,* FROM games WHERE ownership = 1 ORDER BY system,title ASC;") + conn.execute("CREATE VIEW incomplete AS SELECT rowid,* FROM games WHERE ownership >= 1 AND progress = 2 ORDER BY system,title ASC;") + conn.execute("CREATE VIEW new AS SELECT rowid,* FROM games WHERE progress = 0 ORDER BY system,title ASC;") + conn.execute("CREATE VIEW owned AS SELECT rowid,* FROM games WHERE ownership >= 1 ORDER BY system,title ASC;") + conn.execute("CREATE VIEW physical AS SELECT rowid,* FROM games WHERE ownership = 1 OR ownership = 3 ORDER BY system,title ASC;") + conn.execute("CREATE VIEW playlog AS SELECT rowid,* FROM games WHERE ownership >= 1 AND progress = 1 ORDER BY system,title ASC;") conn.execute("CREATE VIEW unowned AS SELECT rowid,* FROM games WHERE ownership = 0 ORDER BY system,title ASC;") - conn.execute("CREATE VIEW wishlist AS SELECT rowid,* FROM games WHERE ownership = 0 AND progress < 2 ORDER BY system,title ASC;") - conn.execute("CREATE VIEW playlog AS SELECT rowid,* FROM games WHERE ownership = 1 AND progress = 1 ORDER BY system,title ASC;") + conn.execute("CREATE VIEW wishlist AS SELECT rowid,* FROM games WHERE ownership = 0 AND progress = 0 ORDER BY system,title ASC;") print("Views created.") conn.commit() except sqlite3.OperationalError as e: @@ -117,7 +124,7 @@ def export_db(args): conn = sqlite3.connect(DB_LOCATION) conn.row_factory = sqlite3.Row c = conn.cursor() - fp.write("# vgstash DB file version 0.1\n") + fp.write("# vgstash DB file version 0.2\n") db_tree = [] # The .format() call supports the optional -i flag for row in c.execute("SELECT {}title,system,ownership,progress FROM games".format("rowid," if args.ids else '')): @@ -153,14 +160,14 @@ def import_db(args): conn.close() print("Imported {} games.".format(c.rowcount)) - def add_game(args): '''Adds a game to the database. Requires at least a title and system. - Ownership can be 0 (not owned) or 1 (owned). The default is 1. + Ownership can be 0 to 3:(not owned) or 1 (owned). The default is 1. - Completion can be 0 to 3: + Completion can be -1 to 3: + -1 (unbeatable) 0 (fresh, never played) 1 (in-progress) (default) 2 (beaten) @@ -171,19 +178,28 @@ def add_game(args): if args.progress == '-': args.progress = PROGRESS # Translate our args so they can be added and reflected correctly - args.ownership = translate_arg(args.ownership) - args.progress = translate_arg(args.progress) + args.ownership = translate_ownership(args.ownership) + args.progress = translate_progress(args.progress) conn = sqlite3.connect(DB_LOCATION) game = (args.title, args.system, args.ownership, args.progress) conn.execute("INSERT INTO games VALUES(:title, :system, :ownership, :progress)", game) conn.commit() conn.close() - qual = "don't own" if args.ownership == 0 else 'own' - comp = ("fresh", "in-progress", "beaten", "completed") - # I shouldn't have to coerce that last argument but I don't know a better - # solution. - print("Added {0} for {1}. You {2} it and it's {3}.".format(args.title, args.system, qual, comp[int(args.progress)])) + qual = ( + "don't own it", + "own it physically", + "own it digitally", + "own it physically and digitally" + ) + comp = { + -1: "it's unbeatable", + 0: "it's new", + 1: "you're playing it", + 2: "it's been beaten", + 3: "it's been completed" + } + print("Added {0} for {1}. You {2} and {3}.".format(args.title, args.system, qual[int(args.ownership)], comp[int(args.progress)])) def delete_game(args): '''Removes a game from the database.''' @@ -202,27 +218,52 @@ def delete_game(args): else: print("That game ID does not exist in the database.") -def translate_arg(arg): - '''Translate approximate argument names to their internal values for - inserting into the database.''' - # Verify we're working with a string. +def translate_progress(arg): + '''Translate a letter progress value into a numeric value for the database.''' try: if len(arg) != 1: print("Argument must be a single character.") sys.exit() if not arg.isnumeric(): - # We know it can't be coerced to a number, so it must be a string - vals = {'f': 0, 'n': 0, 'i': 1, 'y': 1, 'b': 2, 'c': 3} + vals = { + 'u': -1, + 'n': 0, + 'p': 1, + 'b': 2, + 'c': 3, + } try: return vals[arg] except KeyError: # Note to self, doubling a brace escapes it. This was tucked away in the Python docs... - print("Value '{}' not valid. Try one of {{y, n, f, i, b, c}}.".format(arg)) + print("Value '{}' not valid. Try one of {{{}}}.".format(arg, ', '.join(vals))) + sys.exit() + else: + return arg + except TypeError: + return arg + +def translate_ownership(arg): + '''Translate a letter ownership value into a numeric value for the database.''' + try: + if len(arg) != 1: + print("Argument must be a single character.") + sys.exit() + if not arg.isnumeric(): + vals = { + 'n': 0, + 'p': 1, + 'd': 2, + 'b': 3 + } + try: + return vals[arg] + except KeyError: + print("Value '{}' not valid. Try one of {{{}}}.".format(arg, ', '.join(vals))) sys.exit() else: return arg except TypeError: - # It's a number, go ahead and return it return arg def update_game(args): @@ -238,8 +279,10 @@ def update_game(args): print("Invalid field name indicated! Please choose from {}".format(', '.join(opts))) sys.exit() # Translate y, n, f, i, b, and c - if args.field == 'ownership' or args.field == 'progress': - args.value = translate_arg(args.value) + if args.field == 'ownership': + args.value = translate_ownership(args.value) + if args.field == 'progress': + args.value = translate_progress(args.value) # We need this workaround because execute() doesn't like variable column names update_stmt = "UPDATE games SET {} = :val WHERE rowid = :id".format(args.field) conn = sqlite3.connect(DB_LOCATION) @@ -247,14 +290,20 @@ def update_game(args): c.execute(update_stmt, {'id': args.gid, 'val': args.value}) conn.commit() if c.rowcount == 1: - own_msg = ('not owned', 'owned') - prog_msg = ('fresh', 'in-progress', 'beaten', 'complete') + own_msg = ('not owned', 'physically owned', 'digitally owned', 'physically and digitally owned') + prog_msg = { + -1: 'unbeatable', + 0: 'new', + 1: 'playing', + 2: 'beaten', + 3: 'complete' + } update_msg = { - 'title': "{ot} on {os} is now named {val}.", - 'system': "{ot} on {os} is now on {val}.", - 'ownership': "{ot} on {os} is now marked {val}.", - 'progress': "{ot} on {os} is now marked {val}." - } + 'title': "{ot} on {os} is now named {val}.", + 'system': "{ot} on {os} is now on {val}.", + 'ownership': "{ot} on {os} is now marked {val}.", + 'progress': "{ot} on {os} is now marked {val}." + } print(update_msg[args.field].format(ot = target['title'], os = target['system'], val = args.value if args.field == 'title' or args.field == 'system' else own_msg[int(args.value)] if args.field == 'ownership' else prog_msg[int(args.value)])) else: print("Could not update game information. Check the DB's permissions.") @@ -284,18 +333,21 @@ def list_games(args): # works around that limitation. conn = sqlite3.connect(DB_LOCATION) conn.row_factory = sqlite3.Row - # TODO: turn this into an object. The command parser must match filter names, so there's no reason to duplicate work. select_stmts = { 'allgames': "SELECT * FROM allgames", + 'arcade': "SELECT * FROM arcade", 'backlog': "SELECT * FROM backlog", + 'borrowing': "SELECT * FROM borrowing", 'complete': "SELECT * FROM complete", + 'digital': "SELECT * FROM digital", 'done': "SELECT * FROM done", - 'fresh': "SELECT * FROM fresh", 'incomplete': "SELECT * FROM incomplete", + 'new': "SELECT * FROM new", 'owned': "SELECT * FROM owned", + 'physical': "SELECT * FROM physical", + 'playlog': "SELECT * FROM playlog", 'unowned': "SELECT * FROM unowned", 'wishlist': "SELECT * FROM wishlist", - 'playlog': "SELECT * FROM playlog" } # We're emulating a do-while loop first_pass = True @@ -336,7 +388,7 @@ def row_format(args, header): with open('/dev/tty') as tty: curwidth = int(subprocess.check_output(['stty', 'size']).split()[1]) maxwidth = curwidth - twidth = maxwidth - 35 + twidth = maxwidth - 37 if header == True: print("{:^4s} | {:<{w}s} | {:<8s} | {:^3s} | {:<7s}".format("ID", "Title", "System", "Own", "Status", w=twidth)) print("-" * maxwidth) @@ -344,13 +396,20 @@ def row_format(args, header): gidstr = "{: >4d}".format(args['rowid']) titlestr = "{: <{w}s}".format(args['title'][:twidth], w=twidth) systemstr = "{: ^8s}".format(args['system'][:8]) - ownstr = "{: ^3s}".format("*" if args['ownership'] == 1 else " ") - statltr = ['F', 'I', 'B', 'C'] + ownltr = [' ', 'P', ' D', 'P D'] + ownstr = "{: <3s}".format(ownltr[args['ownership']]) + statltr = { + -1: 'U', + 0: 'N', + 1: 'P', + 2: 'B', + 3: 'C' + } statstr = "{: <7s}".format((" " * args['progress'] * 2) + statltr[args['progress']]) """ ID | Title | System | Own | Status - ------------------------------------------------- - 1234 | This is a title | Wii U VC | * | F I B C + --------------------------------------------------- + 1234 | This is a title | Wii U VC | * | U N P B C """ safe_print(" | ".join((gidstr, titlestr, systemstr, ownstr, statstr))) @@ -382,8 +441,7 @@ def main(): # 'list' command parser_list = subparsers.add_parser('list', help="List your games with preset views") - # TODO: don't repeat yourself! The filter list is essentially copied in list_games() - list_filters = ['all', 'backlog', 'complete', 'done', 'fresh', 'incomplete', 'owned', 'unowned', 'wishlist', 'playlog'] + list_filters = ['all', 'arcade', 'backlog', 'borrowing', 'complete', 'digital', 'done', 'incomplete', 'new', 'owned', 'physical', 'unowned', 'wishlist', 'playlog'] parser_list.add_argument('filter', nargs='?', choices=list_filters, default='all', help='Filter games accerding to preset queries. Valid filters: {}'.format(', '.join(list_filters))) parser_list.add_argument('-r', '--raw', action="store_true", help="Output the list in a machine-readable format, separated by pipe characters.") parser_list.set_defaults(func=list_games) -- cgit v1.2.3-54-g00ecf