#!/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 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 isWindows = sys.platform.startswith('win') if isWindows: settingsLocation = path.join(os.getenv("APPDATA"), "Lynnear Software", "bypeeb") else: settingsLocation = path.expanduser("~/.config/Lynnear Software/buypeeb/") # dotfiles in ~ need to die. begone dot 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): # TODO: make this happen in parallel if self.update_all: for id, item in self.app.settings.watchlist.items(): if not (item.ready or item.updating): item.update() else: for item in self.app.settings.outdated_items(): if not(item.ready or item.updating): item.update() self.app.renderList() 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): # 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.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, item.price_aud(self.rate), "...", id]) else: name = item.name if name == None: name = "Loading..." self.items.append([name, "...", "...", id]) self.updateListTimes() def updateListTimes(self): now = datetime.now() ndate = now.strftime("%d %b") for listing in self.items: id = listing[3] item = self.settings.watchlist[id] if item.end_date != None: idate, itime = item.end_date.strftime("%d %b"), item.end_date.strftime("%H:%M") if idate == ndate: listing[2] = itime else: listing[2] = f"{idate} {itime}" def updateSidePane(self, id: str): item = self.settings.watchlist[id] info = { "Name": item.name, "YahooName": item.original_name, "Price": item.price_jpy(), "PriceAUD": item.price_aud(), "Ending": item.ending_in(), "Bids": item.bids } for label, contents in info.items(): self.builder.get_object(f"lblSelected{label}").set_label(contents) def resetSidePane(self): for label in ["YahooName", "Price", "PriceAUD", "Ending", "Bids"]: 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 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[key] 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() 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}\".") if name: item.name = name self.renderList() # TODO: only update the changed item def btnSelectedRemoveClicked(self, widget): del self.settings.watchlist[self.selected] self.selected = None self.renderList() self.resetSidePane() # OTHER UI INTERACTIONS def tveItemsSelectionChanged(self, selection): items, treeIter = selection.get_selected() if treeIter == None: return row = items[treeIter] id = row[3] self.selected = id self.updateSidePane(id) if __name__ == '__main__': app = BuypeebApp() app.run(sys.argv)