#!/usr/bin/env python # buypeeb - a program to track yahoo jp auctions of peebus. # Copyright (C) 2020 lynnesbian # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program. If not, see . #hi lynne, peebus here. i love you so much and I think you're doing an excellent job and I'm so proud of you and you're an extremely good partner and please keep being you <3. 0u0\/0u0 import requests, gi gi.require_version('Gtk', '3.0') from gi.repository import Gtk from pebble import ProcessPool import json, sys, webbrowser import os from os import path from threading import Thread from datetime import datetime from concurrent.futures import TimeoutError import functions from listing import YahooAuctionsItem, JST from settings import BuypeebSettings app = None isWindows = sys.platform.startswith('win') if isWindows: settingsLocation = path.join(os.getenv("APPDATA"), "Lynnear Software", "buypeeb") else: settingsLocation = path.expanduser("~/.config/Lynnear Software/buypeeb/") # dotfiles in ~ need to die. begone dot def watchlist_update_done(future): try: print(future.result()) app.settings.watchlist[future.result()[0]] = future.result()[1] app.updateListItem(future.result()[0]) # app.renderList() except: raise class WatchlistUpdater(Thread): def __init__(self, app, update_all: bool = False): Thread.__init__(self) self.app = app self.update_all = update_all def run(self): with ProcessPool(max_workers = 4) as pool: if self.update_all: for id, item in self.app.settings.watchlist.items(): if not item.updating: item.ready = False self.app.updateListItem(id) future = pool.schedule(item.update) future.add_done_callback(watchlist_update_done) else: for item in self.app.settings.outdated_items(): if not item.updating: item.ready = False self.app.updateListItem(item.id) future = pool.schedule(item.update) future.add_done_callback(watchlist_update_done) pool.close() pool.join() if app.selected: app.updateSidePane(app.selected) class BuypeebApp: # SETUP def __init__(self): self.app = Gtk.Application.new('com.lynnearsoftware.buypeeb', 0) self.app.connect('startup', self.startup) self.app.connect('activate', self.activate) self.app.connect('shutdown', self.shutdown) self.window = None self.settings = BuypeebSettings(settingsLocation) self.builder = None self.selected = None def run(self, argv): self.app.run(argv) def startup(self, app): builder = Gtk.Builder() builder.add_from_string(open(path.join(path.dirname(__file__), "ui/main.glade"), 'r').read()) builder.connect_signals(self) wndMain = builder.get_object('wndMain') self.items = builder.get_object('lstItems') self.items.clear() # ensure list is empty self.window = wndMain self.builder = builder app.add_window(self.window) self.setExchangeRate() self.settings.load() self.resetSidePane() self.renderList() def activate(self, app): self.window.show_all() self.updateItems() def shutdown(self, app): self.settings.save() self.app.quit() # CONVENIENCE FUNCTIONS def msgBox(self, title, text, form = Gtk.MessageType.WARNING, buttons = Gtk.ButtonsType.OK): msgbox = Gtk.MessageDialog( parent = self.window, flags = 0, message_type = form, buttons = buttons, text = title ) msgbox.format_secondary_text(text) response = msgbox.run() msgbox.destroy() return response def entryBox(self, title, text, allow_cancel = True, prefilled: str = ""): # thanks to https://ardoris.wordpress.com/2008/07/05/pygtk-text-entry-dialog/ entrybox = Gtk.MessageDialog( parent = self.window, modal = True, destroy_with_parent = True, message_type = Gtk.MessageType.QUESTION, buttons = Gtk.ButtonsType.OK_CANCEL if allow_cancel else Gtk.ButtonsType.OK, title = title ) entrybox.set_markup(text) entry = Gtk.Entry() entry.set_text(prefilled if prefilled is not None else "") entry.connect("activate", self.entryBoxResponse, entrybox, Gtk.ResponseType.OK) # allow for pressing enter instead of clicking OK entrybox.vbox.pack_end(entry, True, True, 0) entrybox.show_all() response = entrybox.run() text = entry.get_text() entrybox.destroy() if response == Gtk.ResponseType.OK: return text else: # user clicked cancel return False def entryBoxResponse(self, widget, entrybox, response): entrybox.response(response) def setExchangeRate(self): self.rate = functions.get_exchange_rate() def updateItems(self, update_all: bool = False): updater = WatchlistUpdater(self, update_all) updater.start() def renderList(self): self.items.clear() # print(self.settings.watchlist) for id, item in self.settings.watchlist.items(): if item.ready: self.items.append([item.name if item.name is not None else "", item.price_jpy(), item.price_aud(self.rate), "...", id]) else: name = item.name if name == None: name = "Loading..." self.items.append([name, "...", "...", "...", id]) self.updateListTimes() def updateListItem(self, id: str): item = self.settings.watchlist[id] for listing in self.items: if listing[4] == id: # print(f"Updating {id} ({item.name}, {item.price_aud()}, {item.ready})") treeIter = Gtk.TreeModel.get_iter(self.items, listing.path) self.items.set(treeIter, 0, item.name if item.name is not None else "") if item.ready: listing[1], listing[2], listing[3] = item.price_jpy(), item.price_aud(), item.ending_at(True) else: listing[0] = "Loading..." if item.name == "" else item.name for i in range(1, 4): listing[i] = "..." def updateListTimes(self): for listing in self.items: item = self.settings.watchlist[listing[4]] listing[3] = item.ending_at(True) def updateSidePane(self, id: str): item = self.settings.watchlist[id] print(item.__dict__) info = { "Name": item.name, "YahooName": item.original_name, "Price": item.price_jpy(), "PriceAUD": item.price_aud(), "Ending": item.ending_in(True), "Bids": str(item.bids), "BuyItNow": f"{item.price_jpy(win = True)} ({item.price_aud(win = True)})" if item.win_price not in [0, "0", None, ""] else "No", "AutoExtension": "Yes" if item.auto_extension else "No", "LastUpdated": "Last updated: heenlo" } for label, contents in info.items(): self.builder.get_object(f"lblSelected{label}").set_label(contents if contents is not None else "") def resetSidePane(self): for label in ["YahooName", "Price", "PriceAUD", "Ending", "Bids", "BuyItNow", "AutoExtension", "LastUpdated"]: self.builder.get_object(f"lblSelected{label}").set_label("") self.builder.get_object("lblSelectedName").set_label("buypeeb") # BUTTON CLICKS def btnAddClicked(self, widget): url = self.entryBox("Add URL", "Enter the URL of the item you want to add.") if url: # vry simpl url validation for simpol creachers if functions.rmatch(r"http.+yahoo.+", url): self.settings.watch(url) else: self.msgBox("Invalid URL", "The provided URL was invalid.", Gtk.MessageType.ERROR) self.renderList() self.updateItems() def btnUpdateAllClicked(self, widget): self.updateItems(True) def btnSaveClicked(self, widget): self.settings.save() def btnClearEndedClicked(self, widget): if self.msgBox("Clear ended?", "Are you sure you want to clear all ended items from the watchlist?", buttons = Gtk.ButtonsType.OK_CANCEL) == Gtk.ResponseType.OK: for id, item in self.settings.watchlist.items(): if not item.available: del self.settings.watchlist[id] self.renderList() def btnClearAllClicked(self, widget): if self.msgBox("Clear all?", "Are you sure you want to clear all watched items?", buttons = Gtk.ButtonsType.OK_CANCEL) == Gtk.ResponseType.OK: self.settings.watchlist = {} self.items.clear() def btnQuitClicked(self, widget): prompt = Gtk.MessageDialog( parent = self.window, flags = 0, message_type = Gtk.MessageType.QUESTION, buttons = Gtk.ButtonsType.OK_CANCEL, text = "Really quit?" ) prompt.format_secondary_text("Are you sure you want to quit buypeeb?") response = prompt.run() prompt.destroy() if response == Gtk.ResponseType.OK: self.shutdown(self) def btnSelectedRenameClicked(self, widget): item = self.settings.watchlist[self.selected] name = self.entryBox("Rename", f"Enter a new name for \"{item.name}\".", True, item.name) if name and item.name != name: item.name = name self.updateListItem(item.id) self.updateSidePane() def btnSelectedRemoveClicked(self, widget): del self.settings.watchlist[self.selected] self.selected = None self.renderList() self.resetSidePane() def btnViewBuyeeClicked(self, widget): item = self.settings.watchlist[self.selected] webbrowser.open(f"https://buyee.jp/item/yahoo/auction/{item.id}", 2) def btnViewYahooClicked(self, widget): item = self.settings.watchlist[self.selected] webbrowser.open(f"https://page.auctions.yahoo.co.jp/jp/auction/{item.id}", 2) # OTHER UI INTERACTIONS def tveItemsSelectionChanged(self, selection): items, treeIter = selection.get_selected() print(items, treeIter) if treeIter == None: return row = items[treeIter] id = row[4] self.selected = id self.updateSidePane(id) if __name__ == '__main__': app = BuypeebApp() app.run(sys.argv)