buypeeb/buypeeb.py

324 lines
9.7 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, 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)