buypeeb/buypeeb.py

273 lines
7.8 KiB
Python
Executable File

#!/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 <https://www.gnu.org/licenses/>.
#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)