From ac29fc4f4096817200b8d1c8f150285e3d72bcd2 Mon Sep 17 00:00:00 2001 From: J0J0 Todos Date: Mon, 11 Nov 2024 20:19:01 +0100 Subject: [PATCH] Implement dsc sell --- discodos/cmd23/__init__.py | 2 + discodos/cmd23/sell.py | 120 +++++++++++++++++++++++++++++++++++++ discodos/model/discogs.py | 57 ++++++++++++++++++ 3 files changed, 179 insertions(+) create mode 100644 discodos/cmd23/sell.py diff --git a/discodos/cmd23/__init__.py b/discodos/cmd23/__init__.py index 1880c71..5829b7c 100644 --- a/discodos/cmd23/__init__.py +++ b/discodos/cmd23/__init__.py @@ -11,6 +11,7 @@ stats, suggest, ls, + sell, ) @@ -46,3 +47,4 @@ def main_cmd(context, verbose_count, offline_mode): main_cmd.add_command(stats.stats_cmd) main_cmd.add_command(setup.setup_cmd) main_cmd.add_command(ls.ls_cmd) +main_cmd.add_command(sell.sell_cmd) diff --git a/discodos/cmd23/sell.py b/discodos/cmd23/sell.py new file mode 100644 index 0000000..93aefa3 --- /dev/null +++ b/discodos/cmd23/sell.py @@ -0,0 +1,120 @@ +import logging +import click + +from discodos.ctrl import CollectionControlCommandline + +log = logging.getLogger('discodos') + +@click.command(name='sell') +@click.pass_obj +@click.argument("release_id", type=int) +@click.option( + "-c", "--condition", + type=click.Choice(["M", "NM", "VG+", "VG", "G+", "G", "F", "P"], + case_sensitive=True), + default="VG+", + prompt="Record condition (M, NM, VG+, VG, G+, G, F, P)", + help="Condition of the record." +) +@click.option( + "-s", "--sleeve-condition", + type=click.Choice(["M", "NM", "VG+", "VG", "G+", "G", "F", "P"], + case_sensitive=True), + default="VG+", + prompt="Sleeve condition", + help="Condition of the sleeve." +) +@click.option( + "-p", "--price", + type=float, + prompt="Price (enter 0 to fetch Discogs suggested price)", + default=None, + help="Listing price for the record. Leave blank for suggested price." +) +@click.option( + "-a", "--status", + type=click.Choice(["For Sale", "Draft"], case_sensitive=True), + prompt="Status", + default="For Sale", + help="Initial status of the listing." +) +@click.option( + "-l", "--location", + type=str, + prompt="Storage location", + help="Location of the record in storage (e.g., shelf or bin label)." +) +@click.option( + "-o", "--allow-offers", + is_flag=True, + default=True, + prompt="Accept offers? (yes/no)", + help="Allow buyers to make offers on this listing." +) +@click.option( + "-m", "--comments", + type=str, + prompt="Public comments", + help="Public comments about the listing." +) +@click.option( + "-n", "--private-comments", + type=str, + prompt="Private comments", + help="Private comments about the listing." +) +def sell_cmd(helper, release_id, condition, sleeve_condition, price, status, + location, allow_offers, comments, private_comments): + """ + List a record for sale on Discogs. + + Lists the specified record for sale with details such as condition, price, quantity, + and so on. Leave price empty to fetch a suggestion for the given record condition. + """ + def update_user_interaction_helper(user): + log.debug("Entered sell mode.") + user.WANTS_ONLINE = True + return user + + user = update_user_interaction_helper(helper) + log.info("user.WANTS_ONLINE: %s", user.WANTS_ONLINE) + coll_ctrl = CollectionControlCommandline( + False, user, user.conf.discogs_token, user.conf.discogs_appid, + user.conf.discobase, user.conf.musicbrainz_user, + user.conf.musicbrainz_password) + + if not coll_ctrl.ONLINE: + log.warning("Online mode is required to list a record for sale.") + return + + if not price: + suggested_price = coll_ctrl.collection.fetch_price_suggestion( + release_id, condition + ) + if suggested_price: + click.echo( + f"Suggested price for condition '{condition}': " + f"{suggested_price.currency} {suggested_price.value}" + ) + price = click.prompt( + "Accept?", + type=float, + default=round(suggested_price.value, 2), + ) + else: + click.echo("No suggested price available; please enter a price manually.") + price = click.prompt("Price", type=float) + + log.info(f"Attempting to list record {release_id} for sale.") + coll_ctrl.collection.list_for_sale( + release_id=release_id, + condition=condition, + sleeve_condition=sleeve_condition, + price=price, + status=status, + location=location, + allow_offers=allow_offers, + comments=comments, + private_comments=private_comments + ) + coll_ctrl.cli.p("Listed for sale.") diff --git a/discodos/model/discogs.py b/discodos/model/discogs.py index 76cb66c..14b6ac5 100644 --- a/discodos/model/discogs.py +++ b/discodos/model/discogs.py @@ -4,6 +4,7 @@ from socket import gaierror import discogs_client import discogs_client.exceptions +from discogs_client import Condition, Status import requests.exceptions import urllib3.exceptions @@ -11,6 +12,19 @@ log = logging.getLogger('discodos') +CONDITIONS = { + "M": Condition.MINT, + "NM": Condition.NEAR_MINT, + "VG+": Condition.VERY_GOOD_PLUS, + "VG": Condition.VERY_GOOD, + "G+": Condition.GOOD_PLUS, + "G": Condition.GOOD, + "F": Condition.FAIR, +} +STATUS = { + "For Sale": Status.FOR_SALE, + "Draft": Status.DRAFT, +} class DiscogsMixin: """Discogs connection, fetchers and helpers.""" @@ -187,6 +201,49 @@ def fetch_marketplace_stats(self, release_id): } return r if r else None + def fetch_price_suggestion(self, release_id, condition): + release = self.d.release(release_id) + r = { + "M": release.price_suggestions.mint, + "VG+": release.price_suggestions.very_good_plus, + "VG": release.price_suggestions.very_good, + "G+": release.price_suggestions.good_plus, + "G": release.price_suggestions.good, + "F": release.price_suggestions.fair, + } + return r[condition.upper()] if r else None + + def list_for_sale( # pylint: disable=too-many-positional-arguments,too-many-arguments + self, + release_id=None, + condition=None, + sleeve_condition=None, + price=None, + status=None, + location=None, + allow_offers=None, + comments=None, + private_comments=None, + ): + """Lists a record for sale.""" + try: + self.me.inventory.add_listing( + release_id, + CONDITIONS[condition], + price, + STATUS[status], + sleeve_condition=CONDITIONS[sleeve_condition], + comments=comments, + allow_offers="true" if allow_offers else "false", + external_id=private_comments, + location=location, + # weight=None, + # format_quantity=None, + ) + except Exception as Exc: + log.error("Exception while trying to list for sale: %s", Exc) + return False + def rate_limit_slow_downer(self, remaining=10, sleep=2): '''Discogs util: stay in 60/min rate limit''' if int(self.d._fetcher.rate_limit_remaining) < remaining: