├── api ├── __init__.py └── delayedcore.py ├── core ├── __init__.py ├── constants.py ├── watch.py ├── playercard.py ├── editabletreeview.py └── bid.py ├── frames ├── __init__.py ├── misc │ ├── __init__.py │ └── auctions.py ├── base.py ├── loading.py ├── watch.py ├── login.py ├── playersearch.py └── bid.py ├── logo.ico ├── logo.icns ├── images ├── coins.jpg ├── bgimage.jpg ├── loading.jpg ├── logo_icon.jpg └── cards │ ├── group0.png │ ├── group1.png │ ├── group2.png │ ├── cards_small.png │ ├── cards_small.json │ └── cards_big.json ├── fonts ├── OpenSans-Bold.ttf ├── OpenSans-Italic.ttf ├── OpenSans-Light.ttf ├── OpenSans-Regular.ttf ├── OpenSans-ExtraBold.ttf ├── OpenSans-Semibold.ttf ├── OpenSans-BoldItalic.ttf ├── OpenSans-LightItalic.ttf ├── OpenSans-SemiboldItalic.ttf └── OpenSans-ExtraBoldItalic.ttf ├── requirements.txt ├── .gitignore ├── __init__.py ├── Makefile ├── setup.py ├── macholib_patch.py ├── readme.md ├── menubar.py ├── FIFA 17 Auto Buyer.spec ├── FIFA 17 Auto Buyer.py ├── statusbar.py ├── application.py └── dmg └── settings.py /api/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /core/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frames/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frames/misc/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /logo.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hunterjm/futgui/HEAD/logo.ico -------------------------------------------------------------------------------- /logo.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hunterjm/futgui/HEAD/logo.icns -------------------------------------------------------------------------------- /images/coins.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hunterjm/futgui/HEAD/images/coins.jpg -------------------------------------------------------------------------------- /images/bgimage.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hunterjm/futgui/HEAD/images/bgimage.jpg -------------------------------------------------------------------------------- /images/loading.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hunterjm/futgui/HEAD/images/loading.jpg -------------------------------------------------------------------------------- /images/logo_icon.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hunterjm/futgui/HEAD/images/logo_icon.jpg -------------------------------------------------------------------------------- /fonts/OpenSans-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hunterjm/futgui/HEAD/fonts/OpenSans-Bold.ttf -------------------------------------------------------------------------------- /images/cards/group0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hunterjm/futgui/HEAD/images/cards/group0.png -------------------------------------------------------------------------------- /images/cards/group1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hunterjm/futgui/HEAD/images/cards/group1.png -------------------------------------------------------------------------------- /images/cards/group2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hunterjm/futgui/HEAD/images/cards/group2.png -------------------------------------------------------------------------------- /fonts/OpenSans-Italic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hunterjm/futgui/HEAD/fonts/OpenSans-Italic.ttf -------------------------------------------------------------------------------- /fonts/OpenSans-Light.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hunterjm/futgui/HEAD/fonts/OpenSans-Light.ttf -------------------------------------------------------------------------------- /fonts/OpenSans-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hunterjm/futgui/HEAD/fonts/OpenSans-Regular.ttf -------------------------------------------------------------------------------- /fonts/OpenSans-ExtraBold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hunterjm/futgui/HEAD/fonts/OpenSans-ExtraBold.ttf -------------------------------------------------------------------------------- /fonts/OpenSans-Semibold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hunterjm/futgui/HEAD/fonts/OpenSans-Semibold.ttf -------------------------------------------------------------------------------- /images/cards/cards_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hunterjm/futgui/HEAD/images/cards/cards_small.png -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | Pillow 2 | requests 3 | git+https://github.com/hunterjm/fut.git#egg=fut 4 | macholib 5 | -------------------------------------------------------------------------------- /fonts/OpenSans-BoldItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hunterjm/futgui/HEAD/fonts/OpenSans-BoldItalic.ttf -------------------------------------------------------------------------------- /fonts/OpenSans-LightItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hunterjm/futgui/HEAD/fonts/OpenSans-LightItalic.ttf -------------------------------------------------------------------------------- /fonts/OpenSans-SemiboldItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hunterjm/futgui/HEAD/fonts/OpenSans-SemiboldItalic.ttf -------------------------------------------------------------------------------- /fonts/OpenSans-ExtraBoldItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hunterjm/futgui/HEAD/fonts/OpenSans-ExtraBoldItalic.ttf -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | __pycache__/ 3 | dist/ 4 | build/ 5 | fut.log 6 | !requirements.txt 7 | .eggs 8 | .idea 9 | futgui.egg-info/ 10 | -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- 1 | __title__ = 'futgui' 2 | __version__ = '0.0.1' 3 | __author__ = 'Jason Hunter' 4 | __author_email__ = 'hunterjm@gmail.com' 5 | __license__ = 'GNU GPL v3' 6 | __copyright__ = 'Copyright 2016 Jason Hunter' 7 | -------------------------------------------------------------------------------- /core/constants.py: -------------------------------------------------------------------------------- 1 | from os.path import expanduser 2 | 3 | SETTINGS_DIR = expanduser("~") + '/.config/futgui/' 4 | SETTINGS_FILE = SETTINGS_DIR + 'settings.json' 5 | LOGIN_FILE = SETTINGS_DIR + 'login.json' 6 | PLAYERS_FILE = SETTINGS_DIR + 'players.json' 7 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | SHELL := /bin/bash 2 | DMGBUILD := $(shell command -v dmgbuild 2> /dev/null) 3 | 4 | macbundle: 5 | rm -rf build dist 6 | python3 setup.py py2app --packages=requests,PIL 7 | ifdef DMGBUILD 8 | dmgbuild -s dmg/settings.py "Auto Buyer Installer" dist/AutoBuyerInstaller.dmg 9 | endif -------------------------------------------------------------------------------- /frames/base.py: -------------------------------------------------------------------------------- 1 | import tkinter as tk 2 | 3 | 4 | class Base(tk.Frame): 5 | def __init__(self, master, controller): 6 | self.controller = controller 7 | self.args = {} 8 | # init frame 9 | tk.Frame.__init__(self, master, bg='#1d93ab') 10 | 11 | def set_args(self, argDict): 12 | self.args = argDict 13 | 14 | def active(self): 15 | self.update() 16 | -------------------------------------------------------------------------------- /frames/loading.py: -------------------------------------------------------------------------------- 1 | import tkinter as tk 2 | 3 | from frames.base import Base 4 | from PIL import Image, ImageTk 5 | 6 | 7 | class Loading(Base): 8 | def __init__(self, master, controller): 9 | Base.__init__(self, master, controller) 10 | loading = ImageTk.PhotoImage(Image.open('images/loading.jpg')) 11 | label = tk.Label(self, bg='#1d93ab', image=loading) 12 | label.image = loading 13 | label.pack(expand=True) 14 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | """ 2 | This is a setup.py script generated by py2applet 3 | 4 | Usage: 5 | python setup.py py2app 6 | """ 7 | import macholib_patch 8 | from setuptools import setup 9 | 10 | APP = ['FIFA 17 Auto Buyer.py'] 11 | DATA_FILES = ['images', 'fonts'] 12 | OPTIONS = {'argv_emulation': True, 'iconfile': './logo.icns'} 13 | 14 | setup( 15 | app=APP, 16 | data_files=DATA_FILES, 17 | options={'py2app': OPTIONS}, 18 | setup_requires=['py2app'], 19 | ) 20 | -------------------------------------------------------------------------------- /macholib_patch.py: -------------------------------------------------------------------------------- 1 | """ 2 | Monkey-patch macholib to fix "dyld_find() got an unexpected keyword argument 'loader'". 3 | 4 | Add 'import macholib_patch' to the top of set_py2app.py 5 | """ 6 | 7 | import macholib 8 | #print("~"*60 + "macholib verion: "+macholib.__version__) 9 | if macholib.__version__ <= "1.7": 10 | print("Applying macholib patch...") 11 | import macholib.dyld 12 | import macholib.MachOGraph 13 | dyld_find_1_7 = macholib.dyld.dyld_find 14 | def dyld_find(name, loader=None, **kwargs): 15 | #print("~"*60 + "calling alternate dyld_find") 16 | if loader is not None: 17 | kwargs['loader_path'] = loader 18 | return dyld_find_1_7(name, **kwargs) 19 | macholib.MachOGraph.dyld_find = dyld_find -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # NOTE: This auto buyer is no longer being actively maintained in favor of my [other project](https://github.com/hunterjm/fifa-autobuyer/releases). 2 | 3 | # Releases 4 | Download pre-built releases on the [releases](https://github.com/hunterjm/futgui/releases) page. 5 | 6 | ## Contributors 7 | [Jason Hunter](https://github.com/hunterjm) - Core Development 8 | 9 | [Piotr Staroszczyk](https://github.com/oczkers) & others - FUT API Library 10 | 11 | [Fabiano Francesconi](https://github.com/elbryan) 12 | 13 | ### Requirements 14 | This has only been tested and built with Python 3.5. 15 | ``` 16 | pip install -r requirements.txt 17 | ``` 18 | 19 | ### Build (Mac): 20 | ``` 21 | make macbundle 22 | ``` 23 | 24 | ### Build (Windows): 25 | Make sure to delete `build` and `dist` folders before re-building. 26 | ``` 27 | pyinstaller '.\FIFA 17 Auto Buyer.spec' 28 | ``` 29 | -------------------------------------------------------------------------------- /menubar.py: -------------------------------------------------------------------------------- 1 | import tkinter as tk 2 | 3 | 4 | class MenuBar(tk.Menu): 5 | def __init__(self, parent): 6 | tk.Menu.__init__(self, parent) 7 | 8 | # appmenu = tk.Menu(self, name='apple') 9 | # self.add_cascade(menu=appmenu) 10 | # appmenu.add_command(label='About My Application') 11 | # appmenu.add_separator() 12 | 13 | # filemenu = tk.Menu(self, tearoff=False) 14 | # self.add_cascade(label="File",underline=0, menu=filemenu) 15 | # filemenu.add_command(label="Load Buyer Plan", command=self.callback) 16 | # filemenu.add_command(label="Save Buyer Plan", command=self.callback) 17 | # filemenu.add_separator() 18 | # filemenu.add_command(label="Exit", underline=1, command=self.quit) 19 | 20 | # helpmenu = tk.Menu(self, tearoff=False) 21 | # self.add_cascade(label="Help", menu=helpmenu) 22 | # helpmenu.add_command(label="About...", command=self.callback) 23 | -------------------------------------------------------------------------------- /FIFA 17 Auto Buyer.spec: -------------------------------------------------------------------------------- 1 | # -*- mode: python -*- 2 | 3 | block_cipher = None 4 | my_data = [('.\\images', 'images'), 5 | ('.\\fonts', 'fonts')] 6 | 7 | 8 | a = Analysis(['FIFA 17 Auto Buyer.py'], 9 | pathex=['..\\fut', '.'], 10 | binaries=None, 11 | datas=my_data, 12 | hiddenimports=[], 13 | hookspath=None, 14 | runtime_hooks=None, 15 | excludes=None, 16 | win_no_prefer_redirects=None, 17 | win_private_assemblies=None, 18 | cipher=block_cipher) 19 | pyz = PYZ(a.pure, a.zipped_data, 20 | cipher=block_cipher) 21 | exe = EXE(pyz, 22 | a.scripts, 23 | exclude_binaries=True, 24 | name='FIFA 17 Auto Buyer', 25 | debug=False, 26 | strip=None, 27 | upx=True, 28 | console=True, icon='logo.ico') 29 | coll = COLLECT(exe, 30 | a.binaries, 31 | a.zipfiles, 32 | a.datas, 33 | strip=None, 34 | upx=True, 35 | name='FIFA 17 Auto Buyer') 36 | -------------------------------------------------------------------------------- /FIFA 17 Auto Buyer.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import tkinter as tk 3 | import multiprocessing as mp 4 | 5 | from sys import platform 6 | from menubar import MenuBar 7 | from statusbar import StatusBar 8 | from application import Application 9 | 10 | 11 | class MainApplication(tk.Tk): 12 | """Container for all frames within the application""" 13 | 14 | def __init__(self, *args, **kwargs): 15 | tk.Tk.__init__(self, *args, **kwargs) 16 | 17 | #initialize menu 18 | self.config(menu=MenuBar(self)) 19 | self.title('FIFA 17 Auto Buyer') 20 | self.geometry('950x650-5+40') 21 | self.minsize(width=650, height=450) 22 | 23 | # bind ctrl+a 24 | if(platform == 'darwin'): 25 | self.bind_class("Entry", "", self.selectall) 26 | else: 27 | self.bind_class("Entry", "", self.selectall) 28 | 29 | self.status = StatusBar(self) 30 | self.status.pack(side='bottom', fill='x') 31 | self.status.set_credits('0') 32 | 33 | self.appFrame = Application(self) 34 | self.appFrame.pack(side='top', fill='both', expand='True') 35 | 36 | def selectall(self, e): 37 | e.widget.select_range(0, tk.END) 38 | return 'break' 39 | 40 | 41 | if __name__ == '__main__': 42 | mp.freeze_support() 43 | app = MainApplication() 44 | app.lift() 45 | app.mainloop() 46 | -------------------------------------------------------------------------------- /statusbar.py: -------------------------------------------------------------------------------- 1 | import tkinter as tk 2 | import locale 3 | 4 | from PIL import ImageTk, Image 5 | 6 | locale.setlocale(locale.LC_ALL, '') 7 | 8 | 9 | class StatusBar(tk.Frame): 10 | 11 | def __init__(self, master): 12 | tk.Frame.__init__(self, master, bg='#31718f') 13 | self.label = tk.Label(self, fg='#fcca00', bg='#31718f', font=('KnulBold', 12, 'bold')) 14 | self.label.grid(column=0, row=0, sticky='w') 15 | self.stats = tk.Label(self, fg='#fcca00', bg='#31718f', font=('KnulBold', 12)) 16 | self.stats.grid(column=1, row=0, sticky='w') 17 | coinImg = ImageTk.PhotoImage(Image.open('images/coins.jpg')) 18 | coinLabel = tk.Label(self, bg='#31718f', image=coinImg) 19 | coinLabel.grid(column=2, row=0, sticky='e') 20 | coinLabel.image = coinImg 21 | self.credits = tk.Label(self, fg='#fcca00', bg='#31718f', font=('KnulBold', 12)) 22 | self.credits.grid(column=3, row=0, sticky='w') 23 | self.grid_columnconfigure(0, weight=1) 24 | self.grid_columnconfigure(1, weight=0) 25 | self.grid_columnconfigure(2, weight=0) 26 | self.grid_columnconfigure(3, weight=0) 27 | self.grid_rowconfigure(0, weight=1) 28 | 29 | def set_status(self, format, *args): 30 | self.label.config(text=format % args) 31 | self.label.update_idletasks() 32 | 33 | def clear_status(self): 34 | self.label.config(text="") 35 | self.label.update_idletasks() 36 | 37 | def set_stats(self, stats): 38 | self.stats.config(text='Won: %d - Sold: %d' % stats) 39 | self.stats.update_idletasks() 40 | 41 | def set_credits(self, credits): 42 | credits = locale.format("%d", int(credits), grouping=True) 43 | self.credits.config(text=credits) 44 | self.credits.update_idletasks() 45 | -------------------------------------------------------------------------------- /application.py: -------------------------------------------------------------------------------- 1 | import tkinter as tk 2 | import sys 3 | import core.constants as constants 4 | import os.path 5 | 6 | from tkinter import messagebox 7 | from PIL import ImageTk, Image 8 | from frames.loading import Loading 9 | from frames.playersearch import PlayerSearch 10 | from frames.login import Login 11 | from frames.bid import Bid 12 | from frames.watch import Watch 13 | 14 | 15 | class Application(tk.Frame): 16 | def __init__(self, master): 17 | if not self.prepare_environment(): 18 | messagebox.showerror("Error", "Unable to write to your home directory.\n" 19 | "Make sure {0} exists and it is writable".format(constants.SETTINGS_DIR)) 20 | sys.exit(1) 21 | 22 | self.api = None 23 | self.user = None 24 | self.status = master.status 25 | tk.Frame.__init__(self, master, bg='#1d93ab') 26 | 27 | # the container is where we'll stack a bunch of frames 28 | # on top of each other, then the one we want visible 29 | # will be raised above the others 30 | container = tk.Frame(self, bg='#1d93ab') 31 | container.pack(side="top", fill="both", expand=True, padx=15, pady=15) 32 | container.grid_rowconfigure(0, weight=0) 33 | container.grid_rowconfigure(1, weight=1) 34 | container.grid_columnconfigure(0, weight=1) 35 | 36 | fifaImg = ImageTk.PhotoImage(Image.open('images/logo_icon.jpg')) 37 | fifaLabel = tk.Label(container, bg='#1d93ab', image=fifaImg) 38 | fifaLabel.grid(column=0, row=0, sticky='w') 39 | fifaLabel.image = fifaImg 40 | 41 | self.frames = {} 42 | for F in (Loading, Login, PlayerSearch, Bid, Watch): 43 | frame = F(container, self) 44 | self.frames[F] = frame 45 | # put all of the pages in the same location; 46 | # the one on the top of the stacking order 47 | # will be the one that is visible. 48 | frame.grid(column=0, row=1, sticky='news') 49 | 50 | self.show_frame(Login) 51 | 52 | def show_frame(self, c, **kwargs): 53 | '''Show a frame for the given class''' 54 | frame = self.frames[c] 55 | frame.set_args(kwargs) 56 | frame.tkraise() 57 | frame.active() 58 | 59 | def get_frame(self, c): 60 | return self.frames[c] 61 | 62 | def prepare_environment(self): 63 | """ 64 | Prepare the environment, namely ensures that the settings folder exists and it is writeable 65 | :return: true if the environment is sane, false otherwise 66 | """ 67 | if not os.path.exists(constants.SETTINGS_DIR): 68 | os.makedirs(constants.SETTINGS_DIR) 69 | 70 | return os.access(constants.SETTINGS_DIR, os.W_OK) 71 | -------------------------------------------------------------------------------- /api/delayedcore.py: -------------------------------------------------------------------------------- 1 | import fut 2 | import requests 3 | import random 4 | from time import time, sleep 5 | 6 | cookies_file = 'cookies.txt' 7 | 8 | 9 | class DelayedCore(fut.Core): 10 | def __init__(self, email, passwd, secret_answer, platform='pc', code=None, emulate=None, debug=False, cookies=cookies_file): 11 | # Set initial delay 12 | self.delayInterval = 4 13 | self.delay = time() 14 | self.cardInfoCache = {} 15 | super(DelayedCore, self).__init__(email, passwd, secret_answer, platform, code, emulate, debug, cookies) 16 | 17 | def setRequestDelay(self, delay): 18 | self.delayInterval = delay 19 | 20 | def resetSession(self): 21 | cookies = self.r.cookies 22 | headers = self.r.headers 23 | self.r = requests.Session() 24 | self.r.cookies = cookies 25 | self.r.headers = headers 26 | 27 | def __login__(self, email, passwd, secret_answer, platform='pc', code=None, emulate=None): 28 | # no delay for login 29 | delayInterval = self.delayInterval 30 | self.delayInterval = 0 31 | result = super(DelayedCore, self).__login__(email, passwd, secret_answer, platform, code, emulate) 32 | self.delayInterval = delayInterval 33 | self.delay = time() + (self.delayInterval * random.uniform(0.75, 1.25)) 34 | return result 35 | 36 | def __request__(self, method, url, *args, **kwargs): 37 | """Prepares headers and sends request. Returns response as a json object.""" 38 | # Rate Limit requests based on delay interval 39 | if self.delay > time(): 40 | sleep(self.delay - time()) 41 | self.delay = time() + (self.delayInterval * random.uniform(0.75, 1.25)) 42 | return super(DelayedCore, self).__request__(method, url, *args, **kwargs) 43 | 44 | def bid(self, trade_id, bid): 45 | # no delay between getting trade info and bidding, but still want a delay before both 46 | if self.delay > time(): 47 | sleep(self.delay - time()) 48 | delayInterval = self.delayInterval 49 | self.delayInterval = 0 50 | try: 51 | result = super(DelayedCore, self).bid(trade_id, bid) 52 | except fut.exceptions.PermissionDenied as e: 53 | if e.code == '461': 54 | result = False 55 | else: 56 | raise 57 | finally: 58 | self.delayInterval = delayInterval 59 | self.delay = time() + (self.delayInterval * random.uniform(0.75, 1.25)) 60 | return result 61 | 62 | def cardInfo(self, resource_id): 63 | """Returns card info.""" 64 | if resource_id in self.cardInfoCache: 65 | result = self.cardInfoCache[resource_id] 66 | else: 67 | result = super(DelayedCore, self).cardInfo(resource_id) 68 | self.cardInfoCache[resource_id] = result 69 | return result 70 | -------------------------------------------------------------------------------- /images/cards/cards_small.json: -------------------------------------------------------------------------------- 1 | { 2 | "": { 3 | "position": 0, 4 | "height": 61, 5 | "width": 40 6 | }, 7 | "bronze": { 8 | "position": 61, 9 | "height": 61, 10 | "width": 40 11 | }, 12 | "fut_champions_bronze": { 13 | "position": 122, 14 | "height": 61, 15 | "width": 41 16 | }, 17 | "fut_champions_gold": { 18 | "position": 183, 19 | "height": 61, 20 | "width": 40 21 | }, 22 | "fut_champions_silver": { 23 | "position": 244, 24 | "height": 61, 25 | "width": 40 26 | }, 27 | "futties_winner": { 28 | "position": 305, 29 | "height": 61, 30 | "width": 40 31 | }, 32 | "gold": { 33 | "position": 366, 34 | "height": 61, 35 | "width": 40 36 | }, 37 | "imotm": { 38 | "position": 427, 39 | "height": 61, 40 | "width": 40 41 | }, 42 | "legend": { 43 | "position": 488, 44 | "height": 61, 45 | "width": 40 46 | }, 47 | "motm": { 48 | "position": 549, 49 | "height": 61, 50 | "width": 40 51 | }, 52 | "ones_to_watch": { 53 | "position": 610, 54 | "height": 61, 55 | "width": 40 56 | }, 57 | "pink": { 58 | "position": 671, 59 | "height": 61, 60 | "width": 40 61 | }, 62 | "purple": { 63 | "position": 732, 64 | "height": 61, 65 | "width": 40 66 | }, 67 | "rare_bronze": { 68 | "position": 793, 69 | "height": 61, 70 | "width": 40 71 | }, 72 | "rare_gold": { 73 | "position": 854, 74 | "height": 61, 75 | "width": 40 76 | }, 77 | "rare_silver": { 78 | "position": 915, 79 | "height": 61, 80 | "width": 40 81 | }, 82 | "record_breaker": { 83 | "position": 976, 84 | "height": 61, 85 | "width": 40 86 | }, 87 | "sbc_base": { 88 | "position": 1037, 89 | "height": 61, 90 | "width": 40 91 | }, 92 | "silver": { 93 | "position": 1098, 94 | "height": 61, 95 | "width": 40 96 | }, 97 | "teal": { 98 | "position": 1159, 99 | "height": 61, 100 | "width": 40 101 | }, 102 | "tots_bronze": { 103 | "position": 1220, 104 | "height": 61, 105 | "width": 40 106 | }, 107 | "tots_gold": { 108 | "position": 1281, 109 | "height": 61, 110 | "width": 40 111 | }, 112 | "tots_silver": { 113 | "position": 1342, 114 | "height": 61, 115 | "width": 40 116 | }, 117 | "totw_bronze": { 118 | "position": 1403, 119 | "height": 61, 120 | "width": 40 121 | }, 122 | "totw_gold": { 123 | "position": 1464, 124 | "height": 61, 125 | "width": 40 126 | }, 127 | "totw_silver": { 128 | "position": 1525, 129 | "height": 61, 130 | "width": 40 131 | }, 132 | "toty": { 133 | "position": 1586, 134 | "height": 61, 135 | "width": 40 136 | } 137 | } -------------------------------------------------------------------------------- /images/cards/cards_big.json: -------------------------------------------------------------------------------- 1 | { 2 | "": { 3 | "image": "group2", 4 | "position": 0, 5 | "height": 316, 6 | "width": 202 7 | }, 8 | "bronze": { 9 | "image": "group0", 10 | "position": 0, 11 | "height": 316, 12 | "width": 202 13 | }, 14 | "gold": { 15 | "image": "group0", 16 | "position": 316, 17 | "height": 316, 18 | "width": 202 19 | }, 20 | "legend": { 21 | "image": "group0", 22 | "position": 632, 23 | "height": 316, 24 | "width": 202 25 | }, 26 | "rare_bronze": { 27 | "image": "group0", 28 | "position": 948, 29 | "height": 316, 30 | "width": 202 31 | }, 32 | "rare_gold": { 33 | "image": "group0", 34 | "position": 1264, 35 | "height": 316, 36 | "width": 202 37 | }, 38 | "rare_silver": { 39 | "image": "group0", 40 | "position": 1580, 41 | "height": 316, 42 | "width": 202 43 | }, 44 | "silver": { 45 | "image": "group0", 46 | "position": 2260, 47 | "height": 316, 48 | "width": 202 49 | }, 50 | "champions_bronze": { 51 | "image": "group1", 52 | "position": 0, 53 | "height": 316, 54 | "width": 202 55 | }, 56 | "champions_gold": { 57 | "image": "group1", 58 | "position": 316, 59 | "height": 316, 60 | "width": 202 61 | }, 62 | "champions_silver": { 63 | "image": "group1", 64 | "position": 632, 65 | "height": 316, 66 | "width": 202 67 | }, 68 | "futties_winner": { 69 | "image": "group1", 70 | "position": 948, 71 | "height": 316, 72 | "width": 202 73 | }, 74 | "imotm": { 75 | "image": "group1", 76 | "position": 1264, 77 | "height": 316, 78 | "width": 202 79 | }, 80 | "motm": { 81 | "image": "group1", 82 | "position": 1580, 83 | "height": 316, 84 | "width": 202 85 | }, 86 | "ones_to_watch": { 87 | "image": "group1", 88 | "position": 1896, 89 | "height": 316, 90 | "width": 202 91 | }, 92 | "pink": { 93 | "image": "group1", 94 | "position": 2212, 95 | "height": 316, 96 | "width": 202 97 | }, 98 | "purple": { 99 | "image": "group1", 100 | "position": 2528, 101 | "height": 316, 102 | "width": 202 103 | }, 104 | "record_breaker": { 105 | "image": "group1", 106 | "position": 2844, 107 | "height": 316, 108 | "width": 202 109 | }, 110 | "sbc_base": { 111 | "image": "group1", 112 | "position": 3160, 113 | "height": 316, 114 | "width": 202 115 | }, 116 | "teal": { 117 | "image": "group1", 118 | "position": 3476, 119 | "height": 316, 120 | "width": 202 121 | }, 122 | "tots_bronze": { 123 | "image": "group1", 124 | "position": 3792, 125 | "height": 316, 126 | "width": 202 127 | }, 128 | "tots_gold": { 129 | "image": "group1", 130 | "position": 4108, 131 | "height": 316, 132 | "width": 202 133 | }, 134 | "tots_silver": { 135 | "image": "group1", 136 | "position": 4424, 137 | "height": 316, 138 | "width": 202 139 | }, 140 | "totw_bronze": { 141 | "image": "group1", 142 | "position": 4740, 143 | "height": 316, 144 | "width": 202 145 | }, 146 | "totw_gold": { 147 | "image": "group1", 148 | "position": 5056, 149 | "height": 316, 150 | "width": 202 151 | }, 152 | "totw_silver": { 153 | "image": "group1", 154 | "position": 5372, 155 | "height": 316, 156 | "width": 202 157 | }, 158 | "toty": { 159 | "image": "group1", 160 | "position": 5688, 161 | "height": 316, 162 | "width": 202 163 | }, 164 | "concept": { 165 | "image": "group2", 166 | "position": 0, 167 | "height": 316, 168 | "width": 202 169 | } 170 | } -------------------------------------------------------------------------------- /core/watch.py: -------------------------------------------------------------------------------- 1 | import math 2 | from core.bid import decrement 3 | 4 | def lowestBin(q, api, defIds): 5 | 6 | api.resetSession() 7 | 8 | if not isinstance(defIds, (list, tuple)): 9 | defIds = (defIds,) 10 | 11 | def find(api, defId, buy=0, num=0): 12 | lowest = buy 13 | items = api.searchAuctions('player', defId=defId, max_buy=buy, page_size=50) 14 | if items: 15 | lowest = min([i['buyNowPrice'] for i in items]) 16 | num = sum([i['buyNowPrice'] == lowest for i in items]) 17 | # If we have 50 of the same result, go one lower 18 | if num == 50: 19 | lowest -= decrement(lowest) 20 | if buy == 0 or lowest < buy: 21 | return find(api, defId, lowest, num) 22 | return (lowest, num) 23 | 24 | for defId in defIds: 25 | try: 26 | result = find(api, defId) 27 | q.put({ 28 | 'defId': defId, 29 | 'lowestBIN': result[0], 30 | 'num': result[1] 31 | }) 32 | except FutError as e: 33 | q.put(e) 34 | 35 | 36 | def watch(q, api, defIds, length=1200): 37 | 38 | api.resetSession() 39 | trades = {} 40 | 41 | if not isinstance(defIds, (list, tuple)): 42 | defIds = (defIds,) 43 | 44 | try: 45 | for defId in defIds: 46 | trades[defId] = {} 47 | 48 | for i in range(0, 5): 49 | stop = False 50 | # Look for any trades for this card and store off the tradeIds 51 | for item in api.searchAuctions('player', defId=defId, start=i*50+1, page_size=50): 52 | # 20 minutes should be PLENTY of time to watch for trends 53 | if item['expires'] > length: 54 | stop = True 55 | break 56 | 57 | trades[defId][item['tradeId']] = item 58 | 59 | if stop: 60 | break 61 | 62 | # need to have trades to continue 63 | if not len(trades): 64 | return 65 | 66 | # Watch these trades until there is no more to watch or we get a termination 67 | expired = False 68 | while not expired: 69 | expired = True 70 | 71 | for defId in trades.keys(): 72 | # update trade status 73 | for item in api.tradeStatus(list(trades[defId].keys())): 74 | trades[defId][item['tradeId']] = item 75 | if item['expires'] > 0: expired = False 76 | 77 | # start calculations 78 | lowest = 0 79 | median = 0 80 | mean = 0 81 | minUnsoldList = 0 82 | 83 | activeTrades = {k: v for (k, v) in trades[defId].items() if v['currentBid'] > 0} 84 | if len(activeTrades): 85 | activeBids = [v['currentBid'] for (k, v) in activeTrades.items()] 86 | activeBids.sort() 87 | lowest = min(activeTrades[k]['currentBid'] for k in activeTrades) 88 | median = activeBids[math.floor((len(activeBids)-1)/2)] 89 | mean = int(sum(activeTrades[k]['currentBid'] for k in activeTrades) / len(activeTrades)) 90 | 91 | expiredNotSold = {k: v for (k, v) in trades[defId].items() if v['currentBid'] == 0 and v['expires'] == -1} 92 | if len(expiredNotSold): 93 | minUnsoldList = min(expiredNotSold[k]['startingBid'] for k in expiredNotSold) 94 | 95 | q.put({ 96 | 'defId': defId, 97 | 'total': len(trades[defId]), 98 | 'active': sum(trades[defId][k]['expires'] > 0 for k in trades[defId]), 99 | 'bidding': len(activeTrades), 100 | 'lowest': lowest, 101 | 'median': median, 102 | 'mean': mean, 103 | 'minUnsoldList': minUnsoldList 104 | }) 105 | except (FutError, RequestException) as e: 106 | q.put(e) 107 | 108 | from fut.exceptions import FutError 109 | from requests.exceptions import RequestException 110 | -------------------------------------------------------------------------------- /core/playercard.py: -------------------------------------------------------------------------------- 1 | import json 2 | import requests 3 | 4 | from PIL import Image, ImageDraw, ImageFont 5 | from io import BytesIO 6 | 7 | 8 | def create(player, cards=None, cardinfo=None): 9 | 10 | cards = cards 11 | cardinfo = cardinfo 12 | _font25 = ImageFont.truetype('fonts/OpenSans-ExtraBold.ttf', 25) 13 | _font20b = ImageFont.truetype('fonts/OpenSans-Bold.ttf', 20) 14 | _font16 = ImageFont.truetype('fonts/OpenSans-ExtraBold.ttf', 16) 15 | _font16r = ImageFont.truetype('fonts/OpenSans-Regular.ttf', 16) 16 | 17 | if cards is None: 18 | cards = { 19 | 'group0': Image.open('images/cards/group0.png'), 20 | 'group1': Image.open('images/cards/group1.png'), 21 | 'group2': Image.open('images/cards/group2.png') 22 | } 23 | if cardinfo is None: 24 | with open('images/cards/cards_big.json', 'r') as f: 25 | cardinfo = json.load(f) 26 | 27 | # make card 28 | cardbg = cards[cardinfo[player['color']]['image']].crop(( 29 | 0, 30 | cardinfo[player['color']]['position'], 31 | cardinfo[player['color']]['width'], 32 | cardinfo[player['color']]['position'] + cardinfo[player['color']]['height'] 33 | )) 34 | 35 | card = Image.new("RGB", cardbg.size, (29, 147, 171)) 36 | card.paste(cardbg, cardbg) 37 | 38 | # headshot image 39 | if player['specialImages']['largeTOTWImgUrl'] is not None: 40 | r = requests.get(player['specialImages']['largeTOTWImgUrl']) 41 | else: 42 | r = requests.get(player['headshot']['largeImgUrl']) 43 | headshot = Image.open(BytesIO(r.content)).convert('RGBA') 44 | card.paste(headshot, (cardinfo[player['color']]['width']-headshot.size[1]-7, 40), headshot) 45 | 46 | # Rating 47 | renderedSize = _font25.getsize(str(player['rating'])) 48 | rating = Image.new('RGBA', renderedSize, (255, 255, 255, 0)) 49 | d = ImageDraw.Draw(rating) 50 | d.text((0, 0), str(player['rating']), font=_font25, fill=(54, 33, 27, 255)) 51 | card.paste(rating, (42, 30), rating) 52 | 53 | # Position 54 | renderedSize = _font25.getsize(str(player['position'])) 55 | position = Image.new('RGBA', renderedSize, (255, 255, 255, 0)) 56 | d = ImageDraw.Draw(position) 57 | d.text((0, 0), str(player['position']), font=_font16, fill=(54, 33, 27, 255)) 58 | card.paste(position, (42, 60), position) 59 | 60 | # club image 61 | r = requests.get(player['club']['imageUrls']['normal']['large']) 62 | club = Image.open(BytesIO(r.content)).convert('RGBA') 63 | card.paste(club, (35, 85), club) 64 | 65 | # nation image 66 | r = requests.get(player['nation']['imageUrls']['large']) 67 | nation = Image.open(BytesIO(r.content)).convert('RGBA') 68 | card.paste(nation, (35, 130), nation) 69 | 70 | if player['color'][0:3] == 'tot': 71 | fillColor = (255, 234, 128, 255) 72 | else: 73 | fillColor = (54, 33, 27, 255) 74 | 75 | # player name 76 | displayName = player['commonName'] if player['commonName'] is not '' else player['lastName'] 77 | renderedSize = _font16.getsize(displayName) 78 | name = Image.new('RGBA', renderedSize, (255, 255, 255, 0)) 79 | d = ImageDraw.Draw(name) 80 | d.text((0, 0), displayName, font=_font16, fill=fillColor) 81 | card.paste(name, (int((card.size[0]-renderedSize[0])/2), 162), name) 82 | 83 | # attributes 84 | pos = [ 85 | {"x": 65, "y": 188}, 86 | {"x": 65, "y": 213}, 87 | {"x": 65, "y": 238}, 88 | {"x": 130, "y": 188}, 89 | {"x": 130, "y": 213}, 90 | {"x": 130, "y": 238}, 91 | ] 92 | count = 0 93 | for attr in player['attributes']: 94 | 95 | value = str(attr['value']) 96 | renderedSize = _font16.getsize(value) 97 | img = Image.new('RGBA', renderedSize, (255, 255, 255, 0)) 98 | d = ImageDraw.Draw(img) 99 | d.text((0, 0), value, font=_font16, fill=fillColor) 100 | card.paste(img, (pos[count]['x']-25, pos[count]['y']), img) 101 | 102 | name = attr['name'][-3:] 103 | renderedSize = _font16r.getsize(name) 104 | img = Image.new('RGBA', renderedSize, (255, 255, 255, 0)) 105 | d = ImageDraw.Draw(img) 106 | d.text((0, 0), name, font=_font16r, fill=fillColor) 107 | card.paste(img, (pos[count]['x'], pos[count]['y']), img) 108 | count += 1 109 | 110 | # League 111 | renderedSize = _font20b.getsize(player['league']['abbrName']) 112 | league = Image.new('RGBA', renderedSize, (255, 255, 255, 0)) 113 | d = ImageDraw.Draw(league) 114 | d.text((0, 0), player['league']['abbrName'], font=_font20b, fill=fillColor) 115 | card.paste(league, (int((card.size[0]-renderedSize[0])/2), 260), league) 116 | 117 | return card 118 | -------------------------------------------------------------------------------- /frames/misc/auctions.py: -------------------------------------------------------------------------------- 1 | import tkinter as tk 2 | import tkinter.ttk as ttk 3 | from enum import Enum 4 | 5 | class EventType(Enum): 6 | SOLD = 1 7 | NEWBID = 2 8 | BIDWAR = 3 9 | BIN = 4 10 | BIDWON = 5 11 | LOST = 6 12 | OUTBID = 7 13 | UPDATE = 8 14 | SELLING = 9 15 | 16 | class Auctions(): 17 | cards = {} 18 | 19 | def __init__(self, frame): 20 | self.view = tk.Frame(frame) 21 | self.view.grid_rowconfigure(0, weight=1) 22 | self.view.grid_columnconfigure(0, weight=1) 23 | self.view.grid_columnconfigure(1, weight=0) 24 | 25 | self.tree = ttk.Treeview(self.view, columns=('timestamp', 'initial', 'current', 'bin', 'expires')) 26 | self.tree.column("#0", width=75) 27 | self.tree.column("timestamp", width=100) 28 | self.tree.column("initial", width=50) 29 | self.tree.column("current", width=50) 30 | self.tree.column("bin", width=50) 31 | self.tree.column("expires", width=50) 32 | 33 | self.tree.heading("#0", text="Name", anchor="w") 34 | self.tree.heading("timestamp", text="Time") 35 | self.tree.heading("initial", text="Initial Bid") 36 | self.tree.heading("current", text="Current Bid") 37 | self.tree.heading("bin", text="BIN") 38 | self.tree.heading("expires", text="Expires") 39 | 40 | self.tree.tag_configure('bid', foreground='#006400') 41 | self.tree.tag_configure('war', foreground='#B77600') 42 | self.tree.tag_configure('selling', foreground='#1C7CA9') 43 | self.tree.tag_configure('lost', foreground='#B70000', background='grey') 44 | self.tree.tag_configure('won', foreground='#006400', background='grey') 45 | self.tree.tag_configure('sold', foreground='#1C7CA9', background='grey') 46 | 47 | # scrollbar 48 | ysb = ttk.Scrollbar(self.view, orient='vertical', command=self.tree.yview) 49 | self.tree.configure(yscroll=ysb.set) 50 | self.tree.grid(row=0, column=0, sticky='news') 51 | ysb.grid(row=0, column=1, sticky='ns') 52 | 53 | # Timer to decrease estimates seconds 54 | self.decreaseExpires() 55 | 56 | def get_view(self): 57 | return self.view 58 | 59 | def update_status(self, card, timestamp, currbid, tag='', highlight=True): 60 | if not card.cardid in self.cards: 61 | self.tree.insert("", 'end', card.cardid, text=card.cardname, values=(timestamp, card.startingBid, 62 | currbid, card.buyNowPrice, 63 | card.expires), tags=(tag,)) 64 | else: 65 | options = self.tree.item(card.cardid) 66 | options['values'] = (timestamp, card.startingBid, 67 | currbid, card.buyNowPrice, 68 | card.expires) 69 | if tag: 70 | options['tags'] = (tag,) 71 | self.tree.item(card.cardid, text=options['text'], values=options['values'], tags=options['tags']) 72 | 73 | # Highlight the row and make sure it is visible 74 | if highlight: 75 | self.tree.see(card.cardid) 76 | self.tree.selection_set([card.cardid]) 77 | 78 | # Update the cards dictionary with the new entry 79 | self.cards[card.cardid] = card 80 | 81 | 82 | def decreaseExpires(self): 83 | for cardid in self.cards: 84 | card = self.cards[cardid] 85 | if not card.isExpired: 86 | card.expires = max(0, card.expires - 1) 87 | if card.expires == 0: 88 | card.isExpired = True 89 | cardval = self.tree.item(card.cardid) 90 | # Expires is the last column in the tree view 91 | cardval['values'][4] = card.expires 92 | self.cards[cardid] = card 93 | self.tree.item(card.cardid, text=cardval['text'], values=cardval['values'], tags=cardval['tags']) 94 | 95 | self.tree.after(1000, self.decreaseExpires) 96 | 97 | class Card(): 98 | 99 | def __init__(self, item): 100 | self.cardid = item['id'] 101 | self.resourceId = item['resourceId'] 102 | self.tradeId = item['tradeId'] 103 | self.buyNowPrice = item['buyNowPrice'] if item['buyNowPrice'] is not None else item['lastSalePrice'] 104 | self.startingBid = item['startingBid'] if item['startingBid'] is not None else "BIN" 105 | self.currentBid = item['currentBid'] if item['currentBid'] is not None else item['lastSalePrice'] 106 | self.expires = item['expires'] if item['expires'] is not None else 0 107 | self.isExpired = False 108 | 109 | class PlayerCard(Card): 110 | 111 | def __init__(self, item, name): 112 | Card.__init__(self, item) 113 | self.cardname = name -------------------------------------------------------------------------------- /dmg/settings.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | import biplist 5 | import os.path 6 | 7 | # 8 | # Example settings file for dmgbuild 9 | # 10 | 11 | # Use like this: dmgbuild -s settings.py "Test Volume" test.dmg 12 | 13 | # You can actually use this file for your own application (not just TextEdit) 14 | # by doing e.g. 15 | # 16 | # dmgbuild -s settings.py -D app=/path/to/My.app "My Application" MyApp.dmg 17 | 18 | # .. Useful stuff .............................................................. 19 | 20 | application = defines.get('app', 'dist/FIFA 17 Auto Buyer.app') 21 | appname = os.path.basename(application) 22 | 23 | def icon_from_app(app_path): 24 | plist_path = os.path.join(app_path, 'Contents', 'Info.plist') 25 | plist = biplist.readPlist(plist_path) 26 | icon_name = plist['CFBundleIconFile'] 27 | icon_root,icon_ext = os.path.splitext(icon_name) 28 | if not icon_ext: 29 | icon_ext = '.icns' 30 | icon_name = icon_root + icon_ext 31 | return os.path.join(app_path, 'Contents', 'Resources', icon_name) 32 | 33 | # .. Basics .................................................................... 34 | 35 | # Uncomment to override the output filename 36 | # filename = 'test.dmg' 37 | 38 | # Uncomment to override the output volume name 39 | # volume_name = 'Test' 40 | 41 | #Â Volume format (see hdiutil create -help) 42 | format = defines.get('format', 'UDBZ') 43 | 44 | # Volume size (must be large enough for your files) 45 | size = defines.get('size', '100M') 46 | 47 | # Files to include 48 | files = [ application ] 49 | 50 | # Symlinks to create 51 | symlinks = { 'Applications': '/Applications' } 52 | 53 | # Volume icon 54 | # 55 | # You can either define icon, in which case that icon file will be copied to the 56 | # image, *or* you can define badge_icon, in which case the icon file you specify 57 | # will be used to badge the system's Removable Disk icon 58 | # 59 | #icon = '/path/to/icon.icns' 60 | badge_icon = icon_from_app(application) 61 | 62 | # Where to put the icons 63 | icon_locations = { 64 | appname: (140, 120), 65 | 'Applications': (500, 120) 66 | } 67 | 68 | # .. Window configuration ...................................................... 69 | 70 | # Background 71 | # 72 | # This is a STRING containing any of the following: 73 | # 74 | # #3344ff - web-style RGB color 75 | # #34f - web-style RGB color, short form (#34f == #3344ff) 76 | # rgb(1,0,0) - RGB color, each value is between 0 and 1 77 | # hsl(120,1,.5) - HSL (hue saturation lightness) color 78 | # hwb(300,0,0) - HWB (hue whiteness blackness) color 79 | # cmyk(0,1,0,0) - CMYK color 80 | # goldenrod - X11/SVG named color 81 | # builtin-arrow - A simple built-in background with a blue arrow 82 | # /foo/bar/baz.png - The path to an image file 83 | # 84 | # The hue component in hsl() and hwb() may include a unit; it defaults to 85 | # degrees ('deg'), but also supports radians ('rad') and gradians ('grad' 86 | # or 'gon'). 87 | # 88 | # Other color components may be expressed either in the range 0 to 1, or 89 | # as percentages (e.g. 60% is equivalent to 0.6). 90 | background = 'builtin-arrow' 91 | 92 | show_status_bar = False 93 | show_tab_view = False 94 | show_toolbar = False 95 | show_pathbar = False 96 | show_sidebar = False 97 | sidebar_width = 180 98 | 99 | # Window position in ((x, y), (w, h)) format 100 | window_rect = ((100, 100), (640, 280)) 101 | 102 | # Select the default view; must be one of 103 | # 104 | # 'icon-view' 105 | # 'list-view' 106 | # 'column-view' 107 | # 'coverflow' 108 | # 109 | default_view = 'icon-view' 110 | 111 | # General view configuration 112 | show_icon_preview = False 113 | 114 | # Set these to True to force inclusion of icon/list view settings (otherwise 115 | # we only include settings for the default view) 116 | include_icon_view_settings = 'auto' 117 | include_list_view_settings = 'auto' 118 | 119 | # .. Icon view configuration ................................................... 120 | 121 | arrange_by = None 122 | grid_offset = (0, 0) 123 | grid_spacing = 120 124 | scroll_position = (0, 0) 125 | label_pos = 'bottom' # or 'right' 126 | text_size = 16 127 | icon_size = 128 128 | 129 | # .. List view configuration ................................................... 130 | 131 | # Column names are as follows: 132 | # 133 | # name 134 | # date-modified 135 | # date-created 136 | # date-added 137 | # date-last-opened 138 | # size 139 | # kind 140 | # label 141 | # version 142 | # comments 143 | # 144 | list_icon_size = 16 145 | list_text_size = 12 146 | list_scroll_position = (0, 0) 147 | list_sort_by = 'name' 148 | list_use_relative_dates = True 149 | list_calculate_all_sizes = False, 150 | list_columns = ('name', 'date-modified', 'size', 'kind', 'date-added') 151 | list_column_widths = { 152 | 'name': 300, 153 | 'date-modified': 181, 154 | 'date-created': 181, 155 | 'date-added': 181, 156 | 'date-last-opened': 181, 157 | 'size': 97, 158 | 'kind': 115, 159 | 'label': 100, 160 | 'version': 75, 161 | 'comments': 300, 162 | } 163 | list_column_sort_directions = { 164 | 'name': 'ascending', 165 | 'date-modified': 'descending', 166 | 'date-created': 'descending', 167 | 'date-added': 'descending', 168 | 'date-last-opened': 'descending', 169 | 'size': 'descending', 170 | 'kind': 'ascending', 171 | 'label': 'ascending', 172 | 'version': 'ascending', 173 | 'comments': 'ascending', 174 | } 175 | -------------------------------------------------------------------------------- /frames/watch.py: -------------------------------------------------------------------------------- 1 | import tkinter as tk 2 | import multiprocessing as mp 3 | import queue 4 | 5 | from frames.base import Base 6 | from PIL import ImageTk 7 | from core.playercard import create 8 | from core.watch import watch 9 | 10 | 11 | class Watch(Base): 12 | def __init__(self, master, controller): 13 | Base.__init__(self, master, controller) 14 | 15 | self._watching = False 16 | self._errorCount = 0 17 | self.q = mp.Queue() 18 | self.p = None 19 | 20 | options = tk.Frame(self, bg='#1d93ab') 21 | options.grid(column=0, row=0, sticky='ns') 22 | 23 | stats = tk.Frame(self, bg='#1d93ab') 24 | stats.grid(column=1, row=0, pady=50, sticky='ns') 25 | 26 | self.grid_rowconfigure(0, weight=1) 27 | self.grid_columnconfigure(0, weight=0) 28 | self.grid_columnconfigure(1, weight=1) 29 | 30 | back = tk.Button(options, bg='#1d93ab', text='Back to Player Search', command=self.show_playersearch) 31 | back.grid(column=0, row=0, sticky='we') 32 | 33 | self.card = tk.Label(options, bg='#1d93ab') 34 | self.card.grid(column=0, row=1) 35 | 36 | descLbl = tk.Label(stats, text='We are watching all of the trades for the next 20 minutes, and reporting below:', bg='#1d93ab', fg='#ffeb7e') 37 | descLbl.grid(column=0, row=0, columnspan=2) 38 | watchLbl = tk.Label(stats, text='Watched Trades:', bg='#1d93ab', fg='#ffeb7e') 39 | watchLbl.grid(column=0, row=1, sticky='e') 40 | self.numwatch = tk.Label(stats, text='0', bg='#1d93ab', fg='#ffeb7e') 41 | self.numwatch.grid(column=1, row=1) 42 | activeLbl = tk.Label(stats, text='Active Trades:', bg='#1d93ab', fg='#ffeb7e') 43 | activeLbl.grid(column=0, row=2, sticky='e') 44 | self.numactive = tk.Label(stats, text='0', bg='#1d93ab', fg='#ffeb7e') 45 | self.numactive.grid(column=1, row=2) 46 | bidLbl = tk.Label(stats, text='Trades with a Bid:', bg='#1d93ab', fg='#ffeb7e') 47 | bidLbl.grid(column=0, row=3, sticky='e') 48 | self.numbid = tk.Label(stats, text='0', bg='#1d93ab', fg='#ffeb7e') 49 | self.numbid.grid(column=1, row=3) 50 | lowLbl = tk.Label(stats, text='Lowest Bid:', bg='#1d93ab', fg='#ffeb7e') 51 | lowLbl.grid(column=0, row=4, sticky='e') 52 | self.low = tk.Label(stats, text='0', bg='#1d93ab', fg='#ffeb7e') 53 | self.low.grid(column=1, row=4) 54 | midLbl = tk.Label(stats, text='Median Bid:', bg='#1d93ab', fg='#ffeb7e') 55 | midLbl.grid(column=0, row=5, sticky='e') 56 | self.mid = tk.Label(stats, text='0', bg='#1d93ab', fg='#ffeb7e') 57 | self.mid.grid(column=1, row=5) 58 | avgLbl = tk.Label(stats, text='Average Bid:', bg='#1d93ab', fg='#ffeb7e') 59 | avgLbl.grid(column=0, row=6, sticky='e') 60 | self.avg = tk.Label(stats, text='0', bg='#1d93ab', fg='#ffeb7e') 61 | self.avg.grid(column=1, row=6) 62 | lowUnsoldLbl = tk.Label(stats, text='Lowest UNSOLD List Price:', bg='#1d93ab', fg='#ffeb7e') 63 | lowUnsoldLbl.grid(column=0, row=7, sticky='e') 64 | self.lowUnsold = tk.Label(stats, text='0', bg='#1d93ab', fg='#ffeb7e') 65 | self.lowUnsold.grid(column=1, row=7) 66 | 67 | self.checkQueue() 68 | 69 | def watch(self): 70 | self.p = mp.Process(target=watch, args=( 71 | self.q, 72 | self.controller.api, 73 | int(self.args['player']['id']) 74 | )) 75 | self.p.start() 76 | 77 | def checkQueue(self): 78 | try: 79 | status = self.q.get(False) 80 | if isinstance(status, FutError): 81 | if isinstance(status, ExpiredSession): 82 | self._errorCount += 3 83 | else: 84 | self._errorCount += 1 85 | if self._errorCount >= 3: 86 | if self.p is not None: 87 | self.p.terminate() 88 | self.controller.status.set_status('Too many errors watching trades. Please re-login.') 89 | self.controller.show_frame(Login) 90 | else: 91 | self.numwatch.config(text=status['total']) 92 | self.numactive.config(text=status['active']) 93 | self.numbid.config(text=status['bidding']) 94 | self.low.config(text=status['lowest']) 95 | self.mid.config(text=status['median']) 96 | self.avg.config(text=status['mean']) 97 | self.lowUnsold.config(text=status['minUnsoldList']) 98 | self.update_idletasks() 99 | except queue.Empty: 100 | pass 101 | finally: 102 | self.after(100, self.checkQueue) 103 | 104 | def show_playersearch(self): 105 | if self.p is not None: 106 | self.p.terminate() 107 | self.controller.show_frame(PlayerSearch) 108 | 109 | def active(self): 110 | if self.controller.api is None: 111 | self.controller.show_frame(Login) 112 | 113 | Base.active(self) 114 | displayName = self.args['player']['commonName'] if self.args['player']['commonName'] is not '' else self.args['player']['lastName'] 115 | self.controller.status.set_status('Watching auctions for %s...' % displayName) 116 | img = ImageTk.PhotoImage(create(self.args['player'])) 117 | self.card.config(image=img) 118 | self.card.image = img 119 | self.numwatch.config(text='0') 120 | self.numactive.config(text='0') 121 | self.numbid.config(text='0') 122 | self.low.config(text='0') 123 | self.mid.config(text='0') 124 | self.avg.config(text='0') 125 | self.lowUnsold.config(text='0') 126 | self.update_idletasks() 127 | self.watch() 128 | 129 | from frames.login import Login 130 | from frames.playersearch import PlayerSearch 131 | from fut.exceptions import FutError, ExpiredSession 132 | -------------------------------------------------------------------------------- /frames/login.py: -------------------------------------------------------------------------------- 1 | import tkinter as tk 2 | import json 3 | import core.constants as constants 4 | 5 | from frames.base import Base 6 | from api.delayedcore import DelayedCore 7 | from os.path import expanduser 8 | 9 | 10 | class Login(Base): 11 | def __init__(self, master, controller): 12 | #init Base 13 | Base.__init__(self, master, controller) 14 | 15 | # Values 16 | self.controller = controller 17 | self.username = tk.StringVar() 18 | self.password = tk.StringVar() 19 | self.secret = tk.StringVar() 20 | self.code = tk.StringVar() 21 | self.platform = tk.StringVar() 22 | self.emulate = tk.StringVar() 23 | self.debug = tk.IntVar() 24 | self.data = [] 25 | self._keepalive = None 26 | 27 | # Search for settings 28 | try: 29 | with open(constants.LOGIN_FILE, 'r') as f: 30 | self.data = json.load(f) 31 | 32 | if not isinstance(self.data, list): 33 | self.data = [self.data] 34 | 35 | self.username.set(self.data[0]['username']) 36 | self.password.set(self.data[0]['password']) 37 | self.secret.set(self.data[0]['secret']) 38 | self.code.set(self.data[0]['code']) 39 | self.platform.set(self.data[0]['platform']) 40 | self.emulate.set(self.data[0]['emulate']) 41 | except: 42 | self.platform.set('xbox') 43 | self.emulate.set('pc') 44 | 45 | mainframe = tk.Frame(self, bg='#1d93ab') 46 | mainframe.pack(expand=True) 47 | 48 | self.loginlbl = tk.Label( 49 | mainframe, 50 | text='\nWe need to collect your login information in order to connect to the FIFA servers. This information will be saved on your computer for future use.', 51 | anchor='w', justify='left', wraplength=500, 52 | fg='#fff', bg='#1d93ab', font=('KnulBold', 16) 53 | ) 54 | # self.loginlbl.grid(column=0, row=0) 55 | self.loginlbl.pack() 56 | loginfr = tk.Frame(mainframe) 57 | # loginfr.grid(column=0, row=1, sticky='ns') 58 | loginfr.pack() 59 | 60 | # init user input 61 | userlbl = tk.Label(loginfr, text='Email:', font=('KnulBold', 16, 'bold')) 62 | userlbl.grid(column=0, row=1, sticky='e', padx=5, pady=5) 63 | userbox = tk.Entry(loginfr, textvariable=self.username) 64 | userbox.bind('', self.search) 65 | userbox.grid(column=1, row=1, sticky='w', padx=5, pady=5) 66 | passlbl = tk.Label(loginfr, text='Password:', font=('KnulBold', 16, 'bold')) 67 | passlbl.grid(column=0, row=2, sticky='e', padx=5, pady=5) 68 | passbox = tk.Entry(loginfr, textvariable=self.password, show='*') 69 | passbox.grid(column=1, row=2, sticky='w', padx=5, pady=5) 70 | secretlbl = tk.Label(loginfr, text='Secret Question:', font=('KnulBold', 16, 'bold')) 71 | secretlbl.grid(column=0, row=3, sticky='e', padx=5, pady=5) 72 | secretbox = tk.Entry(loginfr, textvariable=self.secret, show='*') 73 | secretbox.grid(column=1, row=3, sticky='w', padx=5, pady=5) 74 | codelbl = tk.Label(loginfr, text='Access Code:', font=('KnulBold', 16, 'bold')) 75 | codelbl.grid(column=0, row=4, sticky='e', padx=5, pady=5) 76 | codebox = tk.Entry(loginfr, textvariable=self.code) 77 | codebox.grid(column=1, row=4, sticky='w', padx=5, pady=5) 78 | platformlbl = tk.Label(loginfr, text='Platform:', font=('KnulBold', 16, 'bold')) 79 | platformlbl.grid(column=0, row=5, sticky='e', padx=5, pady=5) 80 | platformsel = tk.OptionMenu(loginfr, self.platform, 'pc', 'xbox', 'xbox360', 'ps3', 'ps4') 81 | platformsel.grid(column=1, row=5, sticky='w', padx=5, pady=5) 82 | emulatelbl = tk.Label(loginfr, text='Emulate:', font=('KnulBold', 16, 'bold')) 83 | emulatelbl.grid(column=0, row=6, sticky='e', padx=5, pady=5) 84 | emulatesel = tk.OptionMenu(loginfr, self.emulate, 'pc', 'android', 'iOS') 85 | emulatesel.grid(column=1, row=6, sticky='w', padx=5, pady=5) 86 | # debugLbl = tk.Label(loginfr, text='Enable Debug:', font=('KnulBold', 16, 'bold')) 87 | # debugLbl.grid(column=0, row=7, sticky='e') 88 | # debugCheckbox = tk.Checkbutton(loginfr, variable=self.debug) 89 | # debugCheckbox.grid(column=1, row=7, sticky='w') 90 | loginbtn = tk.Button(loginfr, text='Login', command=self.login) 91 | loginbtn.grid(column=0, row=7, columnspan=2, padx=5, pady=5) 92 | 93 | def search(self, event=None): 94 | i = self.find(self.data, 'username', self.username.get()) 95 | if i != -1: 96 | self.loginlbl.config(text='\nLoaded saved login info for %s' % (self.username.get())) 97 | self.password.set(self.data[i]['password']) 98 | self.secret.set(self.data[i]['secret']) 99 | self.code.set(self.data[i]['code']) 100 | self.platform.set(self.data[i]['platform']) 101 | self.emulate.set(self.data[i]['emulate']) 102 | 103 | def login(self, switchFrame=True): 104 | 105 | try: 106 | if self.username.get() and self.password.get() and self.secret.get() and self.platform.get() and self.emulate.get(): 107 | # Show loading frame 108 | if switchFrame: 109 | self.master.config(cursor='wait') 110 | self.master.update() 111 | self.controller.show_frame(Loading) 112 | 113 | # Save settings 114 | self.save_settings() 115 | 116 | # Convert emulate 117 | if self.emulate.get() == 'android': 118 | emulate = 'and' 119 | elif self.emulate.get() == 'iOS': 120 | emulate = 'ios' 121 | else: 122 | emulate = None 123 | 124 | # Start API and update credits 125 | cookies_file = constants.SETTINGS_DIR + self.username.get().split('@')[0]+'.txt' 126 | self.controller.api = DelayedCore(self.username.get(), self.password.get(), self.secret.get(), self.platform.get(), self.code.get(), emulate, False, cookies_file) 127 | self.controller.status.set_credits(str(self.controller.api.credits)) 128 | self.controller.user = self.username.get() 129 | self._keepalive = self.keepalive() 130 | 131 | if switchFrame: 132 | self.controller.status.set_status('Successfully Logged In!') 133 | self.master.config(cursor='') 134 | self.master.update() 135 | self.controller.show_frame(PlayerSearch) 136 | else: 137 | raise FutError('Invalid Login Information') 138 | 139 | except (FutError, RequestException) as e: 140 | self.controller.show_frame(Login) 141 | self.master.config(cursor='') 142 | self.loginlbl.config(text='\nError logging in: %s (%s)' % (e.reason, type(e).__name__)) 143 | self.controller.status.set_status('Error logging in') 144 | 145 | def logout(self, switchFrame=True): 146 | if switchFrame: 147 | self.controller.show_frame(Login) 148 | else: 149 | if self.controller.api is not None: 150 | self.controller.api.logout() 151 | self.controller.api = None 152 | if self._keepalive is not None: 153 | self.after_cancel(self._keepalive) 154 | self._keepalive = None 155 | 156 | def save_settings(self, *args): 157 | try: 158 | login = { 159 | 'username': self.username.get(), 160 | 'password': self.password.get(), 161 | 'secret': self.secret.get(), 162 | 'code': self.code.get(), 163 | 'platform': self.platform.get(), 164 | 'emulate': self.emulate.get() 165 | } 166 | i = self.find(self.data, 'username', self.username.get()) 167 | if i != -1: 168 | self.data[i] = login 169 | else: 170 | self.data.append(login) 171 | with open(constants.LOGIN_FILE, 'w') as f: 172 | json.dump(self.data, f) 173 | except: 174 | pass 175 | 176 | def find(self, lst, key, value): 177 | for i, dic in enumerate(lst): 178 | if dic[key] == value: 179 | return i 180 | return -1 181 | 182 | def keepalive(self): 183 | try: 184 | if self.controller.api is not None: 185 | self.controller.api.keepalive() 186 | self.after(480000, self.keepalive) 187 | except (FutError, RequestException): 188 | self.controller.show_frame(Login) 189 | 190 | def active(self): 191 | Base.active(self) 192 | self.logout(switchFrame=False) 193 | 194 | from fut.exceptions import FutError 195 | from requests.exceptions import RequestException 196 | from frames.loading import Loading 197 | from frames.playersearch import PlayerSearch 198 | -------------------------------------------------------------------------------- /core/editabletreeview.py: -------------------------------------------------------------------------------- 1 | # encoding: utf8 2 | # 3 | # Copyright 2012-2013 Alejandro Autalán 4 | # 5 | # This program is free software: you can redistribute it and/or modify it 6 | # under the terms of the GNU General Public License version 3, as published 7 | # by the Free Software Foundation. 8 | # 9 | # This program is distributed in the hope that it will be useful, but 10 | # WITHOUT ANY WARRANTY; without even the implied warranties of 11 | # MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR 12 | # PURPOSE. See the GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License along 15 | # with this program. If not, see . 16 | # 17 | # For further info, check https://github.com/alejandroautalan/pygubu 18 | 19 | from __future__ import unicode_literals 20 | import functools 21 | 22 | try: 23 | import tkinter as tk 24 | import tkinter.ttk as ttk 25 | except: 26 | import Tkinter as tk 27 | import ttk 28 | 29 | 30 | class EditableTreeview(ttk.Treeview): 31 | def __init__(self, master=None, **kw): 32 | ttk.Treeview.__init__(self, master, **kw) 33 | 34 | self._curfocus = None 35 | self._inplace_widgets = {} 36 | self._inplace_widgets_show = {} 37 | self._inplace_vars = {} 38 | 39 | self.bind('<>', self.__check_focus) 40 | # Wheel events? 41 | self.bind('<4>', lambda e: self.after_idle(self.__updateWnds)) 42 | self.bind('<5>', lambda e: self.after_idle(self.__updateWnds)) 43 | # self.bind('', self.__check_focus) 44 | self.bind('', self.__check_focus) 45 | self.bind('', functools.partial(self.__on_key_press, 'Home')) 46 | self.bind('', functools.partial(self.__on_key_press, 'End')) 47 | self.bind('', lambda e: self.after_idle(self.__updateWnds)) 48 | 49 | def __on_key_press(self, key, event): 50 | if key == 'Home': 51 | self.selection_set("") 52 | self.focus(self.get_children()[0]) 53 | if key == 'End': 54 | self.selection_set("") 55 | self.focus(self.get_children()[-1]) 56 | 57 | def delete(self, *items): 58 | self.after_idle(self.__updateWnds) 59 | ttk.Treeview.delete(self, *items) 60 | 61 | def yview(self, *args): 62 | """Update inplace widgets position when doing vertical scroll""" 63 | self.after_idle(self.__updateWnds) 64 | ttk.Treeview.yview(self, *args) 65 | 66 | def yview_scroll(self, number, what): 67 | self.after_idle(self.__updateWnds) 68 | ttk.Treeview.yview_scroll(self, number, what) 69 | 70 | def yview_moveto(self, fraction): 71 | self.after_idle(self.__updateWnds) 72 | ttk.Treeview.yview_moveto(self, fraction) 73 | 74 | def xview(self, *args): 75 | """Update inplace widgets position when doing horizontal scroll""" 76 | self.after_idle(self.__updateWnds) 77 | ttk.Treeview.xview(self, *args) 78 | 79 | def xview_scroll(self, number, what): 80 | self.after_idle(self.__updateWnds) 81 | ttk.Treeview.xview_scroll(self, number, what) 82 | 83 | def xview_moveto(self, fraction): 84 | self.after_idle(self.__updateWnds) 85 | ttk.Treeview.xview_moveto(self, fraction) 86 | 87 | def __check_focus(self, event): 88 | """Checks if the focus has changed""" 89 | # print('Event:', event.type, event.x, event.y) 90 | changed = False 91 | if not self._curfocus: 92 | changed = True 93 | elif self._curfocus != self.focus(): 94 | self.__clear_inplace_widgets() 95 | changed = True 96 | newfocus = self.focus() 97 | if changed: 98 | if newfocus: 99 | # print('Focus changed to:', newfocus) 100 | self._curfocus = newfocus 101 | self.__focus(newfocus) 102 | self.__updateWnds() 103 | 104 | def __focus(self, item): 105 | """Called when focus item has changed""" 106 | cols = self.__get_display_columns() 107 | for col in cols: 108 | self.__event_info = (col, item) 109 | self.event_generate('<>') 110 | if col in self._inplace_widgets: 111 | w = self._inplace_widgets[col] 112 | w.bind('', lambda e: w.tk_focusNext().focus_set()) 113 | w.bind('', lambda e: w.tk_focusPrev().focus_set()) 114 | 115 | def __updateWnds(self, event=None): 116 | if not self._curfocus: 117 | return 118 | item = self._curfocus 119 | cols = self.__get_display_columns() 120 | for col in cols: 121 | if col in self._inplace_widgets: 122 | wnd = self._inplace_widgets[col] 123 | bbox = '' 124 | if self.exists(item): 125 | bbox = self.bbox(item, column=col) 126 | if bbox == '': 127 | wnd.place_forget() 128 | elif col in self._inplace_widgets_show: 129 | wnd.place(x=bbox[0], y=bbox[1], 130 | width=bbox[2], height=bbox[3]) 131 | 132 | def __clear_inplace_widgets(self): 133 | """Remove all inplace edit widgets.""" 134 | cols = self.__get_display_columns() 135 | # print('Clear:', cols) 136 | for c in cols: 137 | if c in self._inplace_widgets: 138 | widget = self._inplace_widgets[c] 139 | widget.place_forget() 140 | self._inplace_widgets_show.pop(c, None) 141 | # widget.destroy() 142 | # del self._inplace_widgets[c] 143 | 144 | def __get_display_columns(self): 145 | cols = self.cget('displaycolumns') 146 | show = (str(s) for s in self.cget('show')) 147 | if '#all' in cols: 148 | cols = self.cget('columns') + ('#0',) 149 | elif 'tree' in show: 150 | cols = cols + ('#0',) 151 | return cols 152 | 153 | def get_event_info(self): 154 | return self.__event_info 155 | 156 | def __get_value(self, col, item): 157 | if col == '#0': 158 | return self.item(item, 'text') 159 | else: 160 | return self.set(item, col) 161 | 162 | def __set_value(self, col, item, value): 163 | if col == '#0': 164 | self.item(item, text=value) 165 | else: 166 | self.set(item, col, value) 167 | self.__event_info = (col, item) 168 | self.event_generate('<>') 169 | 170 | def __update_value(self, col, item): 171 | if not self.exists(item): 172 | return 173 | value = self.__get_value(col, item) 174 | newvalue = self._inplace_vars[col].get() 175 | if value != newvalue: 176 | self.__set_value(col, item, newvalue) 177 | 178 | def inplace_entry(self, col, item): 179 | if col not in self._inplace_vars: 180 | self._inplace_vars[col] = tk.StringVar() 181 | svar = self._inplace_vars[col] 182 | svar.set(self.__get_value(col, item)) 183 | if col not in self._inplace_widgets: 184 | self._inplace_widgets[col] = ttk.Entry(self, textvariable=svar) 185 | entry = self._inplace_widgets[col] 186 | entry.bind('', lambda e: self.__update_value(col, item)) 187 | entry.bind('', lambda e: self.__update_value(col, item)) 188 | self._inplace_widgets_show[col] = True 189 | 190 | def inplace_checkbutton(self, col, item, onvalue='True', offvalue='False'): 191 | if col not in self._inplace_vars: 192 | self._inplace_vars[col] = tk.StringVar() 193 | svar = self._inplace_vars[col] 194 | svar.set(self.__get_value(col, item)) 195 | if col not in self._inplace_widgets: 196 | self._inplace_widgets[col] = ttk.Checkbutton(self, 197 | textvariable=svar, variable=svar, onvalue=onvalue, offvalue=offvalue) 198 | cb = self._inplace_widgets[col] 199 | cb.bind('', lambda e: self.__update_value(col, item)) 200 | cb.bind('', lambda e: self.__update_value(col, item)) 201 | self._inplace_widgets_show[col] = True 202 | 203 | def inplace_combobox(self, col, item, values, readonly=True): 204 | state = 'readonly' if readonly else 'normal' 205 | if col not in self._inplace_vars: 206 | self._inplace_vars[col] = tk.StringVar() 207 | svar = self._inplace_vars[col] 208 | svar.set(self.__get_value(col, item)) 209 | if col not in self._inplace_widgets: 210 | self._inplace_widgets[col] = ttk.Combobox(self, 211 | textvariable=svar, values=values, state=state) 212 | cb = self._inplace_widgets[col] 213 | cb.bind('', lambda e: self.__update_value(col, item)) 214 | cb.bind('', lambda e: self.__update_value(col, item)) 215 | self._inplace_widgets_show[col] = True 216 | 217 | def inplace_spinbox(self, col, item, min, max, step): 218 | if col not in self._inplace_vars: 219 | self._inplace_vars[col] = tk.StringVar() 220 | svar = self._inplace_vars[col] 221 | svar.set(self.__get_value(col, item)) 222 | if col not in self._inplace_widgets: 223 | self._inplace_widgets[col] = tk.Spinbox(self, 224 | textvariable=svar, from_=min, to=max, increment=step) 225 | sb = self._inplace_widgets[col] 226 | sb.bind('', lambda e: self.__update_value(col, item)) 227 | sb.bind('', lambda e: self.__update_value(col, item)) 228 | self._inplace_widgets_show[col] = True 229 | 230 | def inplace_custom(self, col, item, widget): 231 | if col not in self._inplace_vars: 232 | self._inplace_vars[col] = tk.StringVar() 233 | svar = self._inplace_vars[col] 234 | svar.set(self.__get_value(col, item)) 235 | self._inplace_widgets[col] = widget 236 | widget.bind('', lambda e: self.__update_value(col, item)) 237 | widget.bind('', lambda e: self.__update_value(col, item)) 238 | self._inplace_widgets_show[col] = True 239 | -------------------------------------------------------------------------------- /frames/playersearch.py: -------------------------------------------------------------------------------- 1 | import tkinter as tk 2 | import json 3 | import requests 4 | import multiprocessing as mp 5 | import core.constants as constants 6 | 7 | from core.editabletreeview import EditableTreeview 8 | from os.path import expanduser 9 | from frames.base import Base 10 | from PIL import Image, ImageTk 11 | from core.playercard import create 12 | 13 | 14 | class PlayerSearch(Base): 15 | def __init__(self, master, controller): 16 | Base.__init__(self, master, controller) 17 | self.master = master 18 | self.url = 'https://www.easports.com/fifa/ultimate-team/api/fut/item' 19 | self._job = None 20 | self.player = tk.StringVar() 21 | self._playerName = '' 22 | search = tk.Entry(self, textvariable=self.player) 23 | search.bind('', self.search) 24 | search.bind('', self.lookup) 25 | search.grid(column=0, row=0, columnspan=2, sticky='we') 26 | 27 | # preload cards and info 28 | self.cards = { 29 | 'group0': Image.open('images/cards/group0.png'), 30 | 'group1': Image.open('images/cards/group1.png'), 31 | 'group2': Image.open('images/cards/group2.png') 32 | } 33 | with open('images/cards/cards_big.json', 'r') as f: 34 | self.cardinfo = json.load(f) 35 | 36 | self.cardLabels = None 37 | 38 | # create scrolling frame 39 | # create a canvas object and a vertical scrollbar for scrolling it 40 | hscrollbar = tk.Scrollbar(self, orient=tk.HORIZONTAL) 41 | hscrollbar.grid(column=0, row=2, columnspan=2, sticky='we') 42 | canvas = tk.Canvas(self, bd=0, highlightthickness=0, bg='#1d93ab', xscrollcommand=hscrollbar.set) 43 | canvas.grid(column=0, row=1, columnspan=2, sticky='news') 44 | hscrollbar.config(command=canvas.xview) 45 | 46 | # reset the view 47 | canvas.xview_moveto(0) 48 | canvas.yview_moveto(0) 49 | 50 | # create a frame inside the canvas which will be scrolled with it 51 | self.interior = interior = tk.Frame(canvas, bg='#1d93ab') 52 | interior_id = canvas.create_window(0, 0, window=interior, 53 | anchor=tk.NW) 54 | 55 | # track changes to the canvas and frame width and sync them, 56 | # also updating the scrollbar 57 | def _configure_interior(event): 58 | # update the scrollbars to match the size of the inner frame 59 | size = (interior.winfo_reqwidth(), interior.winfo_reqheight()) 60 | canvas.config(scrollregion="0 0 %s %s" % size) 61 | if interior.winfo_reqheight() != canvas.winfo_height(): 62 | # update the canvas's width to fit the inner frame 63 | canvas.config(height=interior.winfo_reqheight()) 64 | interior.bind('', _configure_interior) 65 | 66 | def _configure_canvas(event): 67 | if interior.winfo_reqheight() != canvas.winfo_height(): 68 | # update the inner frame's width to fill the canvas 69 | canvas.itemconfigure(interior_id, height=canvas.winfo_height()) 70 | canvas.bind('', _configure_canvas) 71 | 72 | # Add a treeview to display selected players 73 | self.tree = EditableTreeview(self, columns=('position', 'rating', 'buy', 'sell', 'bin', 'actions'), selectmode='browse', height=8) 74 | self.tree.heading('#0', text='Name', anchor='w') 75 | self.tree.column('position', width=100, anchor='center') 76 | self.tree.heading('position', text='Position') 77 | self.tree.column('rating', width=100, anchor='center') 78 | self.tree.heading('rating', text='Rating') 79 | self.tree.column('buy', width=100, anchor='center') 80 | self.tree.heading('buy', text='Purchase For') 81 | self.tree.column('sell', width=100, anchor='center') 82 | self.tree.heading('sell', text='Sell For') 83 | self.tree.column('bin', width=100, anchor='center') 84 | self.tree.heading('bin', text='Sell For BIN') 85 | self.tree.column('actions', width=20, anchor='center') 86 | self.tree.bind('<>', self._on_inplace_edit) 87 | self.tree.bind('<>', self._on_cell_edited) 88 | self.tree.grid(column=0, row=3, columnspan=2, sticky='we') 89 | 90 | watchbtn = tk.Button(self, text='Watch Player', command=self.show_watch) 91 | watchbtn.grid(column=0, row=4, sticky='we') 92 | 93 | bidbtn = tk.Button(self, text='Start Bidding', command=self.show_bid) 94 | bidbtn.grid(column=1, row=4, sticky='we') 95 | 96 | self._del_btn = tk.Button(self.tree, text='-', command=self._on_del_clicked) 97 | 98 | # Search for existing list 99 | self._playerFile = {} 100 | self._playerList = [] 101 | try: 102 | with open(constants.PLAYERS_FILE, 'r') as f: 103 | self._playerFile = json.load(f) 104 | self._playerList = [] 105 | except: 106 | pass 107 | 108 | self.grid_columnconfigure(0, weight=1) 109 | self.grid_columnconfigure(1, weight=1) 110 | self.grid_rowconfigure(0, weight=0) 111 | self.grid_rowconfigure(1, weight=1) 112 | self.grid_rowconfigure(2, weight=0) 113 | self.grid_rowconfigure(3, weight=0) 114 | self.grid_rowconfigure(4, weight=0) 115 | 116 | def search(self, event=None): 117 | self.kill_job() 118 | 119 | # make sure it's a different name 120 | if self._playerName != self.player.get(): 121 | self._playerName = self.player.get() 122 | self._job = self.after(500, self.lookup) 123 | 124 | def lookup(self, event=None): 125 | self.kill_job() 126 | payload = {'jsonParamObject': json.dumps({'name': self._playerName})} 127 | response = requests.get(self.url, params=payload).json() 128 | self.controller.status.set_status('Found %d matches for "%s"' % (response['totalResults'], self._playerName)) 129 | for child in self.interior.winfo_children(): 130 | child.destroy() 131 | p = mp.Pool(processes=mp.cpu_count()) 132 | results = [p.apply_async(create, (player,)) for player in response['items']] 133 | self.master.config(cursor='wait') 134 | self.master.update() 135 | i = 0 136 | for r in results: 137 | self.load_player(r.get(), response['items'][i]) 138 | i += 1 139 | self.master.config(cursor='') 140 | self.master.update() 141 | 142 | def load_player(self, result, player): 143 | # make card 144 | img = ImageTk.PhotoImage(result) 145 | lbl = tk.Label(self.interior, bg='#1d93ab', image=img) 146 | lbl.pack(side='left') 147 | lbl.config(cursor='pencil') 148 | lbl.image = img 149 | self.update_idletasks() 150 | lbl.bind("", lambda e, player=player: self.add_player({ 151 | 'player': player, 152 | 'buy': 0, 153 | 'sell': 0, 154 | 'bin': 0 155 | })) 156 | 157 | def add_player(self, item, write=True): 158 | player = item['player'] 159 | displayName = player['commonName'] if player['commonName'] is not '' else player['lastName'] 160 | try: 161 | self.tree.insert('', 'end', player['id'], text=displayName, values=(player['position'], player['rating'], item['buy'], item['sell'], item['bin'])) 162 | if write: 163 | self._playerList.append(item) 164 | self.save_list() 165 | except: 166 | pass 167 | 168 | def save_list(self): 169 | self._playerFile[self.controller.user] = self._playerList 170 | with open(constants.PLAYERS_FILE, 'w') as f: 171 | json.dump(self._playerFile, f) 172 | 173 | def _on_inplace_edit(self, event): 174 | col, item = self.tree.get_event_info() 175 | if col in ('buy', 'sell', 'bin'): 176 | self.tree.inplace_entry(col, item) 177 | elif col in ('actions',): 178 | self.tree.inplace_custom(col, item, self._del_btn) 179 | 180 | def _on_del_clicked(self): 181 | sel = self.tree.selection() 182 | if sel: 183 | item = sel[0] 184 | self.tree.delete(item) 185 | del self._playerList[next(i for (i, d) in enumerate(self._playerList) if d['player']['id'] == item)] 186 | self.save_list() 187 | 188 | def _on_cell_edited(self, event): 189 | col, item = self.tree.get_event_info() 190 | values = self.tree.item(item, 'values') 191 | for player in self._playerList: 192 | if player['player']['id'] == item: 193 | player['buy'] = int(values[2]) 194 | player['sell'] = int(values[3]) 195 | player['bin'] = int(values[4]) 196 | break 197 | self.save_list() 198 | 199 | def show_bid(self): 200 | if len(self._playerList) > 0: 201 | self.controller.show_frame(Bid, playerFile=self._playerFile, playerList=self._playerList) 202 | 203 | def show_watch(self): 204 | sel = self.tree.selection() 205 | if sel: 206 | item = sel[0] 207 | item = self._playerList[next(i for (i, d) in enumerate(self._playerList) if d['player']['id'] == item)] 208 | self.controller.show_frame(Watch, player=item['player']) 209 | 210 | def kill_job(self): 211 | if self._job is not None: 212 | self.after_cancel(self._job) 213 | self._job = None 214 | 215 | def active(self): 216 | Base.active(self) 217 | if self.controller.api is None: 218 | self.controller.show_frame(Login) 219 | 220 | # Backwards compatability 221 | if isinstance(self._playerFile, list): 222 | self._playerList = self._playerFile 223 | self._playerFile = { 224 | self.controller.user: self._playerList 225 | } 226 | self.save_list() 227 | 228 | # Check if we have a list for this user 229 | if self.controller.user not in self._playerFile: 230 | self._playerFile[self.controller.user] = [] 231 | 232 | self._playerList = self._playerFile[self.controller.user] 233 | for item in self._playerList: 234 | self.add_player(item, write=False) 235 | 236 | 237 | from frames.login import Login 238 | from frames.watch import Watch 239 | from frames.bid import Bid 240 | -------------------------------------------------------------------------------- /core/bid.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | from operator import itemgetter 4 | from frames.misc.auctions import Card, PlayerCard, EventType 5 | 6 | 7 | def increment(bid): 8 | if bid < 1000: 9 | return 50 10 | elif bid < 10000: 11 | return 100 12 | elif bid < 50000: 13 | return 250 14 | elif bid < 100000: 15 | return 500 16 | else: 17 | return 1000 18 | 19 | def decrement(bid): 20 | if bid <= 1000: 21 | return 50 22 | elif bid <= 10000: 23 | return 100 24 | elif bid <= 50000: 25 | return 250 26 | elif bid <= 100000: 27 | return 500 28 | else: 29 | return 1000 30 | 31 | def roundBid(bid): 32 | return int(increment(bid) * round(float(bid)/increment(bid))) 33 | 34 | 35 | def bid(q, api, playerList, settings): 36 | pileFull = False 37 | auctionsWon = 0 38 | bidDetails = {} 39 | trades = {} 40 | 41 | api.resetSession() 42 | 43 | for item in playerList: 44 | bidDetails[item['player']['id']] = { 45 | 'maxBid': item['buy'], 46 | 'sell': item['sell'], 47 | 'binPrice': item['bin'] 48 | } 49 | 50 | for item in api.watchlist(): 51 | trades[item['tradeId']] = item['resourceId'] 52 | 53 | # Grab all items from tradepile 54 | tradepile = api.tradepile() 55 | 56 | # Log selling players 57 | for trade in tradepile: 58 | asset = api.cardInfo(trade['resourceId']) 59 | if str(asset['Item']['ItemType']).startswith('Player'): 60 | displayName = asset['Item']['CommonName'] if asset['Item']['CommonName'] else asset['Item']['LastName'] 61 | else: 62 | displayName = asset['Item']['Desc'] 63 | card = PlayerCard(trade, displayName) 64 | q.put((card, EventType.SELLING, api.credits)) 65 | 66 | for defId in bidDetails.keys(): 67 | 68 | if bidDetails[defId]['maxBid'] < 100: 69 | continue 70 | 71 | try: 72 | 73 | # How many of this item do we already have listed? 74 | listed = sum([str(api.baseId(item['resourceId'])) == defId for item in tradepile]) 75 | 76 | # Only bid if we don't already have a full trade pile and don't own too many of this player 77 | binWon = False 78 | if not pileFull and api.credits > settings['minCredits'] and listed < settings['maxPlayer']: 79 | 80 | # Look for any BIN less than the BIN price 81 | for item in api.searchAuctions('player', defId=defId, max_buy=bidDetails[defId]['maxBid'], start=0, page_size=50): 82 | # player safety checks for every possible bid 83 | if listed >= settings['maxPlayer'] or api.credits < settings['minCredits']: 84 | break 85 | 86 | # No Dups 87 | if item['tradeId'] in trades: 88 | continue 89 | 90 | # Must have contract 91 | if item['contract'] < 1: 92 | continue 93 | 94 | # Buy!!! 95 | if api.bid(item['tradeId'], item['buyNowPrice']): 96 | asset = api.cardInfo(item['resourceId']) 97 | displayName = asset['Item']['CommonName'] if asset['Item']['CommonName'] else asset['Item']['LastName'] 98 | card = PlayerCard(item, displayName) 99 | 100 | q.put((card, EventType.BIN, api.credits)) 101 | q.put('%s Card Purchased: BIN %d on %s %s\n' % (time.strftime('%Y-%m-%d %H:%M:%S'), item['buyNowPrice'], asset['Item']['FirstName'], asset['Item']['LastName'])) 102 | trades[item['tradeId']] = item['resourceId'] 103 | binWon = True 104 | listed += 1 105 | else: 106 | q.put('%s Bid Error: You are not allowed to bid on this trade\n' % (time.strftime('%Y-%m-%d %H:%M:%S'))) 107 | 108 | # Search first 50 items in my price range to bid on within 5 minutes 109 | if not settings['snipeOnly']: 110 | bidon = 0 111 | subtract = decrement(bidDetails[defId]['maxBid']) 112 | for item in api.searchAuctions('player', defId=defId, max_price=bidDetails[defId]['maxBid']-subtract, start=0, page_size=50): 113 | # player safety checks for every possible bid 114 | # Let's look at last 5 minutes for now and bid on 5 players max 115 | if item['expires'] > 300 or bidon >= 5 or listed >= settings['maxPlayer'] or api.credits < settings['minCredits']: 116 | break 117 | 118 | # No Dups 119 | if item['tradeId'] in trades: 120 | continue 121 | 122 | # Must have contract 123 | if item['contract'] < 1: 124 | continue 125 | 126 | # Set my initial bid 127 | if item['currentBid']: 128 | bid = item['currentBid'] + increment(item['currentBid']) 129 | else: 130 | bid = item['startingBid'] 131 | 132 | # Bid!!! 133 | if api.bid(item['tradeId'], bid): 134 | asset = api.cardInfo(item['resourceId']) 135 | displayName = asset['Item']['CommonName'] if asset['Item']['CommonName'] else asset['Item']['LastName'] 136 | card = PlayerCard(item, displayName) 137 | 138 | card.currentBid = bid 139 | q.put((card, EventType.NEWBID, api.credits)) 140 | q.put('%s New Bid: %d on %s %s\n' % (time.strftime('%Y-%m-%d %H:%M:%S'), bid, asset['Item']['FirstName'], asset['Item']['LastName'])) 141 | trades[item['tradeId']] = item['resourceId'] 142 | bidon += 1 143 | else: 144 | q.put('%s Bid Error: You are not allowed to bid on this trade\n' % (time.strftime('%Y-%m-%d %H:%M:%S'))) 145 | 146 | if not settings['snipeOnly'] and trades: 147 | # Update watched items 148 | q.put('%s Updating watched items...\n' % (time.strftime('%Y-%m-%d %H:%M:%S'))) 149 | for item in api.tradeStatus([tradeId for tradeId in trades]): 150 | item['resourceId'] = trades[item['tradeId']] 151 | baseId = str(api.baseId(item['resourceId'])) 152 | if baseId not in bidDetails: 153 | q.put('%s Trade not found - baseId: %s\n' % (time.strftime('%Y-%m-%d %H:%M:%S'), baseId)) 154 | continue 155 | maxBid = bidDetails[baseId]['maxBid'] 156 | sell = bidDetails[baseId]['sell'] 157 | binPrice = bidDetails[baseId]['binPrice'] 158 | # How many of this item do we already have listed? 159 | listed = sum([str(api.baseId(trade['resourceId'])) == baseId for trade in tradepile]) 160 | 161 | tradeId = item['tradeId'] 162 | if tradeId not in trades: 163 | q.put('%s Trade not found - tradeId: %s\n' % (time.strftime('%Y-%m-%d %H:%M:%S'), tradeId)) 164 | continue 165 | 166 | asset = api.cardInfo(trades[tradeId]) 167 | displayName = asset['Item']['CommonName'] if asset['Item']['CommonName'] else asset['Item']['LastName'] 168 | card = PlayerCard(item, displayName) 169 | 170 | # Update the card, regardless what will happen 171 | q.put((card, EventType.UPDATE, api.credits)) 172 | 173 | # Handle Expired Items 174 | if item['expires'] == -1: 175 | if (item['bidState'] == 'highest' or (item['tradeState'] == 'closed' and item['bidState'] == 'buyNow')): 176 | 177 | # We won! Send to Pile! 178 | q.put((card, EventType.BIDWON, api.credits)) 179 | q.put('%s Auction Won: %d on %s %s\n' % (time.strftime('%Y-%m-%d %H:%M:%S'), item['currentBid'], asset['Item']['FirstName'], asset['Item']['LastName'])) 180 | if api.sendToTradepile(tradeId, item['id'], safe=True): 181 | # List on market 182 | if api.sell(item['id'], sell, binPrice): 183 | auctionsWon += 1 184 | listed += 1 185 | # No need to keep track of expired bids 186 | del trades[tradeId] 187 | q.put('%s Item Listed: %s %s for %d (%d BIN)\n' % (time.strftime('%Y-%m-%d %H:%M:%S'), asset['Item']['FirstName'], asset['Item']['LastName'], sell, binPrice)) 188 | pileFull = False 189 | 190 | else: 191 | q.put('%s Error: %s %s could not be placed in the tradepile...\n' % (time.strftime('%Y-%m-%d %H:%M:%S'), asset['Item']['FirstName'], asset['Item']['LastName'])) 192 | pileFull = True 193 | 194 | else: 195 | 196 | if api.watchlistDelete(tradeId): 197 | 198 | if item['currentBid'] < maxBid: 199 | q.put((card, EventType.LOST, api.credits)) 200 | q.put('%s TOO SLOW: %s %s went for %d\n' % (time.strftime('%Y-%m-%d %H:%M:%S'), asset['Item']['FirstName'], asset['Item']['LastName'], item['currentBid'])) 201 | else: 202 | q.put((card, EventType.LOST, api.credits)) 203 | q.put('%s Auction Lost: %s %s went for %d\n' % (time.strftime('%Y-%m-%d %H:%M:%S'), asset['Item']['FirstName'], asset['Item']['LastName'], item['currentBid'])) 204 | 205 | # No need to keep track of expired bids 206 | del trades[tradeId] 207 | 208 | elif item['bidState'] != 'highest': 209 | # Continue if we already have too many listed or we don't have enough credits 210 | if listed >= settings['maxPlayer'] or api.credits < settings['minCredits']: 211 | continue 212 | 213 | # We were outbid 214 | newBid = item['currentBid'] + increment(item['currentBid']) 215 | if newBid > maxBid: 216 | if api.watchlistDelete(tradeId): 217 | q.put((card, EventType.OUTBID, api.credits)) 218 | q.put('%s Outbid: Won\'t pay %d for %s %s\n' % (time.strftime('%Y-%m-%d %H:%M:%S'), newBid, asset['Item']['FirstName'], asset['Item']['LastName'])) 219 | del trades[tradeId] 220 | 221 | else: 222 | if api.bid(tradeId, newBid): 223 | card.currentBid = newBid 224 | q.put((card, EventType.BIDWAR, api.credits)) 225 | q.put('%s Bidding War: %d on %s %s\n' % (time.strftime('%Y-%m-%d %H:%M:%S'), newBid, asset['Item']['FirstName'], asset['Item']['LastName'])) 226 | else: 227 | q.put('%s Bid Error: You are not allowed to bid on this trade\n' % (time.strftime('%Y-%m-%d %H:%M:%S'))) 228 | 229 | # buy now goes directly to unassigned now 230 | if binWon: 231 | for item in api.unassigned(): 232 | baseId = str(api.baseId(item['resourceId'])) 233 | if baseId not in bidDetails: 234 | q.put('%s Trade not found for BIN - baseId: %s\n' % (time.strftime('%Y-%m-%d %H:%M:%S'), baseId)) 235 | continue 236 | maxBid = bidDetails[baseId]['maxBid'] 237 | sell = bidDetails[baseId]['sell'] 238 | binPrice = bidDetails[baseId]['binPrice'] 239 | 240 | tradeId = item['tradeId'] if item['tradeId'] is not None else -1 241 | asset = api.cardInfo(item['resourceId']) 242 | displayName = asset['Item']['CommonName'] if asset['Item']['CommonName'] else asset['Item']['LastName'] 243 | card = PlayerCard(item, displayName) 244 | 245 | # We won! Send to Pile! 246 | q.put((card, EventType.BIDWON, api.credits)) 247 | q.put('%s Auction Won: %d on %s %s\n' % (time.strftime('%Y-%m-%d %H:%M:%S'), item['lastSalePrice'], asset['Item']['FirstName'], asset['Item']['LastName'])) 248 | if api.sendToTradepile(tradeId, item['id'], safe=True): 249 | # List on market 250 | if api.sell(item['id'], sell, binPrice): 251 | auctionsWon += 1 252 | # No need to keep track of expired bids 253 | if tradeId > 0: 254 | del trades[tradeId] 255 | q.put('%s Item Listed: %s %s for %d (%d BIN)\n' % (time.strftime('%Y-%m-%d %H:%M:%S'), asset['Item']['FirstName'], asset['Item']['LastName'], sell, binPrice)) 256 | pileFull = False 257 | 258 | else: 259 | q.put('%s Error: %s %s could not be placed in the tradepile...\n' % (time.strftime('%Y-%m-%d %H:%M:%S'), asset['Item']['FirstName'], asset['Item']['LastName'])) 260 | pileFull = True 261 | 262 | # relist items 263 | expired = sum([i['tradeState'] == 'expired' for i in tradepile]) 264 | if expired > 0: 265 | 266 | relistFailed = False 267 | if settings['relistAll']: 268 | try: 269 | api.relist() 270 | except InternalServerError: 271 | relistFailed = True 272 | pass 273 | 274 | if not settings['relistAll'] or relistFailed: 275 | q.put('%s Manually re-listing %d players.\n' % (time.strftime('%Y-%m-%d %H:%M:%S'), expired)) 276 | for i in tradepile: 277 | baseId = str(api.baseId(i['resourceId'])) 278 | if baseId in bidDetails: 279 | sell = i['startingBid'] if settings['relistAll'] else bidDetails[baseId]['sell'] 280 | binPrice = i['buyNowPrice'] if settings['relistAll'] else bidDetails[baseId]['binPrice'] 281 | if i['tradeState'] == 'expired' and sell and binPrice: 282 | api.sell(i['id'], sell, binPrice) 283 | elif i['tradeState'] == 'expired': 284 | # If we don't follow this player, then just relist it with the same price 285 | asset = api.cardInfo(i['resourceId']) 286 | displayName = asset['Item']['CommonName'] if asset['Item']['CommonName'] else asset['Item']['LastName'] 287 | q.put('%s Re-listing %s at the same price. (Player not in target list)\n' % (time.strftime('%Y-%m-%d %H:%M:%S'), displayName)) 288 | api.sell(i['id'], i['startingBid'], i['buyNowPrice']) 289 | 290 | # Log sold items 291 | sold = sum([i['tradeState'] == 'closed' for i in tradepile]) 292 | if sold > 0: 293 | for i in tradepile: 294 | if i['tradeState'] == 'closed': 295 | asset = api.cardInfo(i['resourceId']) 296 | displayName = asset['Item']['CommonName'] if asset['Item']['CommonName'] else asset['Item']['LastName'] 297 | card = PlayerCard(i, displayName) 298 | q.put((card, EventType.SOLD, api.credits)) 299 | q.put('%s Item Sold: %s %s for %d\n' % (time.strftime('%Y-%m-%d %H:%M:%S'), asset['Item']['FirstName'], asset['Item']['LastName'], i['currentBid'])) 300 | api.tradepileDelete(i['tradeId']) 301 | pileFull = False 302 | 303 | # Sleep if we have no more space left 304 | if pileFull: 305 | 306 | # Update tradepile and verify that we are really full and it just wasn't an error 307 | tradepile = api.tradepile() 308 | if len(tradepile) >= api.tradepile_size: 309 | # No use in trying more until min trade is done 310 | selling = sorted(tradepile, key=itemgetter('expires'), reverse=True) 311 | q.put('%s Trade Pile Full! Resume bidding in %d seconds\n' % (time.strftime('%Y-%m-%d %H:%M:%S'), selling[0]['expires'])) 312 | time.sleep(selling[0]['expires']) 313 | 314 | q.put((auctionsWon, sold, api.credits)) 315 | 316 | # re-sync tradepile if we won something 317 | if auctionsWon or expired or sold: 318 | tradepile = api.tradepile() 319 | 320 | # Reset auctions won 321 | auctionsWon = 0 322 | 323 | except (FutError, RequestException) as e: 324 | q.put(e) 325 | 326 | # update our api 327 | q.put(api) 328 | 329 | from fut.exceptions import FutError, InternalServerError 330 | from requests.exceptions import RequestException 331 | -------------------------------------------------------------------------------- /frames/bid.py: -------------------------------------------------------------------------------- 1 | import tkinter as tk 2 | import tkinter.ttk as ttk 3 | import multiprocessing as mp 4 | import queue 5 | import time 6 | import json 7 | import requests 8 | import core.constants as constants 9 | 10 | from frames.base import Base 11 | from frames.misc.auctions import Auctions, Card, EventType 12 | from core.bid import bid, increment, roundBid 13 | from core.watch import lowestBin 14 | from api.delayedcore import DelayedCore 15 | from os.path import expanduser 16 | 17 | 18 | class Bid(Base): 19 | def __init__(self, master, controller): 20 | Base.__init__(self, master, controller) 21 | 22 | self._bidding = False 23 | self._bidCycle = 0 24 | self._errorCount = 0 25 | self._banWait = 0 26 | self._startTime = 0 27 | self._lastUpdate = 0 28 | self._updatedItems = [] 29 | self.auctionsWon = 0 30 | self.sold = 0 31 | 32 | self.q = mp.Queue() 33 | self.p = None 34 | 35 | self.rpm = tk.StringVar() 36 | self.minCredits = tk.StringVar() 37 | self.maxPlayer = tk.StringVar() 38 | self.autoUpdate = tk.IntVar() 39 | self.buy = tk.StringVar() 40 | self.sell = tk.StringVar() 41 | self.bin = tk.StringVar() 42 | self.snipeOnly = tk.IntVar() 43 | self.relistAll = tk.IntVar() 44 | 45 | self.settings = { 46 | 'rpm': 20, 47 | 'minCredits': 1000, 48 | 'maxPlayer': 20, 49 | 'autoUpdate': 0, 50 | 'buy': 0.9, 51 | 'sell': 1, 52 | 'bin': 1.25, 53 | 'snipeOnly': 0, 54 | 'relistAll': 1 55 | } 56 | 57 | # Search for settings 58 | try: 59 | with open(constants.SETTINGS_FILE, 'r') as f: 60 | self.settings.update(json.load(f)) 61 | except: 62 | pass 63 | 64 | # Set initial values 65 | self.rpm.set(self.settings['rpm']) 66 | self.minCredits.set(self.settings['minCredits']) 67 | self.maxPlayer.set(self.settings['maxPlayer']) 68 | self.autoUpdate.set(self.settings['autoUpdate']) 69 | self.buy.set(int(self.settings['buy']*100)) 70 | self.sell.set(int(self.settings['sell']*100)) 71 | self.bin.set(int(self.settings['bin']*100)) 72 | self.snipeOnly.set(self.settings['snipeOnly']) 73 | self.relistAll.set(self.settings['relistAll']) 74 | 75 | # Setup traces 76 | self.rpm.trace('w', self.save_settings) 77 | self.minCredits.trace('w', self.save_settings) 78 | self.maxPlayer.trace('w', self.save_settings) 79 | self.autoUpdate.trace('w', self.save_settings) 80 | self.buy.trace('w', self.save_settings) 81 | self.sell.trace('w', self.save_settings) 82 | self.bin.trace('w', self.save_settings) 83 | self.snipeOnly.trace('w', self.save_settings) 84 | self.relistAll.trace('w', self.save_settings) 85 | 86 | # Setup GUI 87 | options = tk.Frame(self) 88 | options.grid(column=0, row=0, sticky='ns') 89 | 90 | auctions = tk.Frame(self) 91 | 92 | self.auctionStatus = Auctions(auctions) 93 | self.auctionStatus.get_view().grid(column=0, row=0, sticky='nsew') 94 | 95 | self.logView = tk.Text(auctions, bg='#1d93ab', fg='#ffeb7e', bd=0) 96 | self.logView.grid(column=0, row=1, sticky='nsew') 97 | 98 | auctions.grid(column=1, row=0, sticky='nsew') 99 | auctions.grid_rowconfigure(0, weight=3) 100 | auctions.grid_rowconfigure(1, weight=1) 101 | auctions.grid_columnconfigure(0, weight=1) 102 | 103 | self.grid_rowconfigure(0, weight=1) 104 | self.grid_columnconfigure(0, weight=0) 105 | self.grid_columnconfigure(1, weight=1) 106 | 107 | back = tk.Button(options, bg='#1d93ab', text='Back to Player Search', command=self.playersearch) 108 | back.grid(column=0, row=0, sticky='we') 109 | 110 | self.tree = ttk.Treeview(options, columns=('buy', 'sell', 'bin', 'won'), selectmode='browse') 111 | self.tree.heading('#0', text='Name', anchor='w') 112 | self.tree.column('buy', width=50, anchor='center') 113 | self.tree.heading('buy', text='Max Bid') 114 | self.tree.column('sell', width=50, anchor='center') 115 | self.tree.heading('sell', text='Sell') 116 | self.tree.column('bin', width=50, anchor='center') 117 | self.tree.heading('bin', text='BIN') 118 | self.tree.column('won', width=50, anchor='center') 119 | self.tree.heading('won', text='# Won') 120 | self.tree.grid(column=0, row=1, sticky='ns') 121 | 122 | form = tk.Frame(options, padx=15, pady=15) 123 | form.grid(column=0, row=2) 124 | 125 | options.grid_columnconfigure(0, weight=1) 126 | options.grid_rowconfigure(0, weight=0) 127 | options.grid_rowconfigure(1, weight=1) 128 | options.grid_rowconfigure(2, weight=0) 129 | 130 | settingsLbl = tk.Label(form, text='Settings', font=('KnulBold', 16)) 131 | settingsLbl.grid(column=0, row=0, columnspan=2) 132 | 133 | rpmLbl = tk.Label(form, text='RPM:') 134 | rpmLbl.grid(column=0, row=1, sticky='e') 135 | rpmEntry = tk.Entry(form, width=8, textvariable=self.rpm) 136 | rpmEntry.grid(column=1, row=1, sticky='w') 137 | 138 | minCreditsLbl = tk.Label(form, text='Min $:') 139 | minCreditsLbl.grid(column=0, row=2, sticky='e') 140 | minCreditsEntry = tk.Entry(form, width=8, textvariable=self.minCredits) 141 | minCreditsEntry.grid(column=1, row=2, sticky='w') 142 | 143 | maxPlayerLbl = tk.Label(form, text='Max Cards:') 144 | maxPlayerLbl.grid(column=0, row=3, sticky='e') 145 | maxPlayerEntry = tk.Entry(form, width=8, textvariable=self.maxPlayer) 146 | maxPlayerEntry.grid(column=1, row=3, sticky='w') 147 | 148 | snipeOnlyLbl = tk.Label(form, text='BIN Snipe:') 149 | snipeOnlyLbl.grid(column=0, row=4, sticky='e') 150 | snipeOnlyCheckbox = tk.Checkbutton(form, variable=self.snipeOnly) 151 | snipeOnlyCheckbox.grid(column=1, row=4, sticky='w') 152 | 153 | pricingLbl = tk.Label(form, text='Pricing', font=('KnulBold', 16)) 154 | pricingLbl.grid(column=2, row=0, columnspan=2) 155 | 156 | autoUpdateLbl = tk.Label(form, text='Auto Update:') 157 | autoUpdateLbl.grid(column=2, row=1, sticky='e') 158 | autoUpdateCheckbox = tk.Checkbutton(form, variable=self.autoUpdate) 159 | autoUpdateCheckbox.grid(column=3, row=1, sticky='w') 160 | 161 | autoBuyLbl = tk.Label(form, text='Auto Bid %:') 162 | autoBuyLbl.grid(column=2, row=2, sticky='e') 163 | autoBuyEntry = tk.Entry(form, width=4, textvariable=self.buy) 164 | autoBuyEntry.grid(column=3, row=2, sticky='w') 165 | 166 | autoSellLbl = tk.Label(form, text='Auto Sell %:') 167 | autoSellLbl.grid(column=2, row=3, sticky='e') 168 | autoSellEntry = tk.Entry(form, width=4, textvariable=self.sell) 169 | autoSellEntry.grid(column=3, row=3, sticky='w') 170 | 171 | autoBINLbl = tk.Label(form, text='Auto BIN %:') 172 | autoBINLbl.grid(column=2, row=4, sticky='e') 173 | autoBINEntry = tk.Entry(form, width=4, textvariable=self.bin) 174 | autoBINEntry.grid(column=3, row=4, sticky='w') 175 | 176 | relistAllLbl = tk.Label(form, text='Same Relist $:') 177 | relistAllLbl.grid(column=2, row=5, sticky='e') 178 | relistAllCheckbox = tk.Checkbutton(form, variable=self.relistAll) 179 | relistAllCheckbox.grid(column=3, row=5, sticky='w') 180 | 181 | self.bidbtn = tk.Button(form, text='Start Bidding', command=self.start) 182 | self.bidbtn.grid(column=0, row=6, columnspan=4, padx=5, pady=5) 183 | 184 | self.checkQueue() 185 | self.clearErrors() 186 | 187 | def bid(self): 188 | if not self._bidding: 189 | return 190 | if self.p is not None and self.p.is_alive(): 191 | self.after(1000, self.bid) 192 | return 193 | # Update RPM 194 | if self.settings['rpm'] > 0: 195 | self.controller.api.setRequestDelay(60/self.settings['rpm']) 196 | # Check if we need to update pricing 197 | if self.settings['autoUpdate'] and time.time() - self._lastUpdate > 3600: 198 | self.updatePrice() 199 | return 200 | try: 201 | self._bidCycle += 1 202 | self.p = mp.Process(target=bid, args=( 203 | self.q, 204 | self.controller.api, 205 | self.args['playerList'], 206 | self.settings 207 | )) 208 | self.p.start() 209 | self.controller.status.set_credits(self.controller.api.credits) 210 | self.after(5000, self.bid) 211 | except ExpiredSession: 212 | self.stop() 213 | self.controller.show_frame(Login) 214 | except (FutError, RequestException, ConnectionError) as e: 215 | self.updateLog('%s %s: %s (%s)\n' % (time.strftime('%Y-%m-%d %H:%M:%S'), type(e).__name__, e.reason, e.code)) 216 | self._errorCount += 1 217 | if self._errorCount >= 3: 218 | self._banWait = self._banWait + 1 219 | self.updateLog('%s Too many errors. Will resume in %d minutes...\n' % (time.strftime('%Y-%m-%d %H:%M:%S'), self._banWait*5)) 220 | self.stop() 221 | login = self.controller.get_frame(Login) 222 | login.logout(switchFrame=False) 223 | self.after(self._banWait*5*60000, self.relogin, (login)) 224 | else: 225 | self.after(2000, self.bid) 226 | pass 227 | 228 | def start(self): 229 | if not self._bidding: 230 | if self.controller.api is None: 231 | login = self.controller.get_frame(Login) 232 | self.relogin(login) 233 | self._bidding = True 234 | self._bidCycle = 0 235 | self._errorCount = 0 236 | self._startTime = time.time() 237 | self.bidbtn.config(text='STOP Bidding', command=self.stop) 238 | self.update_idletasks() 239 | self.updateLog('%s Started bidding...\n' % (time.strftime('%Y-%m-%d %H:%M:%S'))) 240 | self.bid() 241 | 242 | def stop(self): 243 | if self._bidding: 244 | if self.p is not None and self.p.is_alive(): 245 | self.p.terminate() 246 | self._bidding = False 247 | self._bidCycle = 0 248 | self._errorCount = 0 249 | self.controller.status.set_status('Set Bid Options...') 250 | self.bidbtn.config(text='Start Bidding', command=self.start) 251 | self.update_idletasks() 252 | self.updateLog('%s Stopped bidding...\n' % (time.strftime('%Y-%m-%d %H:%M:%S'))) 253 | 254 | def updatePrice(self): 255 | self.updateLog('%s Updating Prices for Player List...\n' % (time.strftime('%Y-%m-%d %H:%M:%S'))) 256 | self._updatedItems = [] 257 | # it takes around 3 searches per player, based on RPM 258 | wait = (60/self.settings['rpm']) * 3 * len(self.args['playerList']) 259 | self.updateLog('%s This is going to take around %.1f minute(s)...\n' % (time.strftime('%Y-%m-%d %H:%M:%S'), wait/60)) 260 | self.p = mp.Process(target=lowestBin, args=( 261 | self.q, 262 | self.controller.api, 263 | [item['player']['id'] for item in self.args['playerList']] 264 | )) 265 | self.p.start() 266 | self._lastUpdate = time.time() 267 | self.after(int(wait*1000), self.bid) 268 | 269 | def setPrice(self, item, sell): 270 | item['buy'] = roundBid(sell*self.settings['buy']) 271 | item['sell'] = roundBid(sell*self.settings['sell']) 272 | item['bin'] = roundBid(sell*self.settings['bin']) 273 | self.tree.set(item['player']['id'], 'buy', item['buy']) 274 | self.tree.set(item['player']['id'], 'sell', item['sell']) 275 | self.tree.set(item['player']['id'], 'bin', item['bin']) 276 | playersearch = self.controller.get_frame(PlayerSearch) 277 | playersearch.tree.set(item['player']['id'], 'buy', item['buy']) 278 | playersearch.tree.set(item['player']['id'], 'sell', item['sell']) 279 | playersearch.tree.set(item['player']['id'], 'bin', item['bin']) 280 | self.save_list() 281 | displayName = item['player']['commonName'] if item['player']['commonName'] is not '' else item['player']['lastName'] 282 | self.updateLog('%s Setting %s to %d/%d/%d (based on %d)...\n' % (time.strftime('%Y-%m-%d %H:%M:%S'), displayName, item['buy'], item['sell'], item['bin'], sell)) 283 | return item 284 | 285 | def lookup_bin(self, player): 286 | # lookup BIN 287 | r = {'xbox': 0, 'ps4': 0} 288 | displayName = player['commonName'] if player['commonName'] is not '' else player['lastName'] 289 | response = requests.get('http://www.futbin.com/pages/16/players/filter_processing.php', params={ 290 | 'start': 0, 291 | 'length': 30, 292 | 'search[value]': displayName 293 | }).json() 294 | for p in response['data']: 295 | if p[len(p)-2] == player['id']: 296 | if not p[6]: p[6] = 0 297 | if not p[8]: p[8] = 0 298 | r = {'xbox': int(p[8]), 'ps4': int(p[6])} 299 | return r 300 | 301 | def checkQueue(self): 302 | try: 303 | msg = self.q.get(False) 304 | if isinstance(msg, FutError): 305 | # Exception 306 | self.updateLog('%s %s: %s (%s)\n' % (time.strftime('%Y-%m-%d %H:%M:%S'), type(msg).__name__, msg.reason, msg.code)) 307 | if isinstance(msg, ExpiredSession): 308 | self._errorCount += 3 309 | else: 310 | self._errorCount += 1 311 | if self._errorCount >= 3: 312 | self._banWait = self._banWait + 1 313 | self.updateLog('%s Too many errors. Will resume in %d minutes...\n' % (time.strftime('%Y-%m-%d %H:%M:%S'), self._banWait*5)) 314 | self.stop() 315 | login = self.controller.get_frame(Login) 316 | login.logout(switchFrame=False) 317 | self.after(self._banWait*5*60000, self.relogin, (login)) 318 | elif isinstance(msg, DelayedCore): 319 | self.controller.api = msg 320 | elif isinstance(msg, tuple): 321 | if isinstance(msg[0], Card) and isinstance(msg[1], EventType): 322 | if msg[1] == EventType.BIDWAR: 323 | self.auctionStatus.update_status(msg[0], time.strftime('%Y-%m-%d %H:%M:%S'), msg[0].currentBid, tag='war') 324 | elif msg[1] == EventType.NEWBID: 325 | self.auctionStatus.update_status(msg[0], time.strftime('%Y-%m-%d %H:%M:%S'), msg[0].currentBid, tag='bid') 326 | elif (msg[1] == EventType.LOST or msg[1] == EventType.OUTBID): 327 | self.auctionStatus.update_status(msg[0], time.strftime('%Y-%m-%d %H:%M:%S'), msg[0].currentBid, tag='lost') 328 | elif (msg[1] == EventType.BIDWON or msg[1] == EventType.BIN): 329 | self.auctionStatus.update_status(msg[0], time.strftime('%Y-%m-%d %H:%M:%S'), msg[0].currentBid, tag='won') 330 | defId = str(self.controller.api.baseId(msg[0].resourceId)) 331 | self.tree.set(defId, 'won', int(self.tree.set(defId, 'won'))+1) 332 | elif msg[1] == EventType.SELLING: 333 | self.auctionStatus.update_status(msg[0], time.strftime('%Y-%m-%d %H:%M:%S'), msg[0].currentBid, tag='selling') 334 | elif msg[1] == EventType.SOLD: 335 | self.auctionStatus.update_status(msg[0], time.strftime('%Y-%m-%d %H:%M:%S'), msg[0].currentBid, tag='sold') 336 | elif msg[1] == EventType.UPDATE: 337 | self.auctionStatus.update_status(msg[0], time.strftime('%Y-%m-%d %H:%M:%S'), msg[0].currentBid, highlight=False) 338 | self.controller.status.set_credits(msg[2]) 339 | else: 340 | # Auction Results 341 | self.auctionsWon += msg[0] 342 | self.sold += msg[1] 343 | self.controller.status.set_credits(msg[2]) 344 | self.controller.status.set_stats((self.auctionsWon, self.sold)) 345 | self.controller.status.set_status('Bidding Cycle: %d' % (self._bidCycle)) 346 | if time.time() - self._startTime > 18000: 347 | self.updateLog('%s Pausing to prevent ban... Will resume in 1 hour...\n' % (time.strftime('%Y-%m-%d %H:%M:%S'))) 348 | self.stop() 349 | login = self.controller.get_frame(Login) 350 | login.logout(switchFrame=False) 351 | self.after(60*60000, self.relogin, (login)) 352 | elif isinstance(msg, dict): 353 | # Update Pricing 354 | self._lastUpdate = time.time() 355 | for item in self.args['playerList']: 356 | # Skip those that are finished 357 | if item['player']['id'] in self._updatedItems: 358 | continue 359 | if item['player']['id'] == msg['defId']: 360 | displayName = item['player']['commonName'] if item['player']['commonName'] is not '' else item['player']['lastName'] 361 | if msg['num'] > 10: 362 | bid = msg['lowestBIN'] - increment(msg['lowestBIN']) 363 | self.updateLog('%s %d %s listed... Lowering Bid to %d\n' % (time.strftime('%Y-%m-%d %H:%M:%S'), msg['num'], displayName, bid)) 364 | else: 365 | bid = msg['lowestBIN'] 366 | self.updateLog('%s %d %s listed... Setting Bid to %d\n' % (time.strftime('%Y-%m-%d %H:%M:%S'), msg['num'], displayName, bid)) 367 | item = self.setPrice(item, bid) 368 | break 369 | else: 370 | # Normal Message 371 | self._banWait = 0 372 | self.updateLog(msg) 373 | except queue.Empty: 374 | pass 375 | finally: 376 | self.after(100, self.checkQueue) 377 | 378 | def relogin(self, login): 379 | login.login(switchFrame=False) 380 | self.start() 381 | 382 | def clearErrors(self): 383 | if self._bidding and self._errorCount > 0: 384 | self._errorCount = self._errorCount - 1 385 | self.after(900000, self.clearErrors) 386 | 387 | def updateLog(self, msg): 388 | self.logView.insert('end', msg) 389 | self.logView.see(tk.END) 390 | self.update_idletasks() 391 | 392 | def save_list(self): 393 | self.args['playerFile'][self.controller.user] = self.args['playerList'] 394 | with open(constants.PLAYERS_FILE, 'w') as f: 395 | json.dump(self.args['playerFile'], f) 396 | 397 | def save_settings(self, *args): 398 | try: 399 | self.settings = { 400 | 'rpm': int(self.rpm.get()) if self.rpm.get() else 0, 401 | 'minCredits': int(self.minCredits.get()) if self.minCredits.get() else 0, 402 | 'maxPlayer': int(self.maxPlayer.get()) if self.maxPlayer.get() else 0, 403 | 'autoUpdate': self.autoUpdate.get(), 404 | 'buy': int(self.buy.get())/100 if self.buy.get() else 0, 405 | 'sell': int(self.sell.get())/100 if self.sell.get() else 0, 406 | 'bin': int(self.bin.get())/100 if self.bin.get() else 0, 407 | 'snipeOnly': self.snipeOnly.get(), 408 | 'relistAll': self.relistAll.get() 409 | } 410 | with open(constants.SETTINGS_FILE, 'w') as f: 411 | json.dump(self.settings, f) 412 | except: 413 | pass 414 | 415 | def playersearch(self): 416 | self.stop() 417 | self.controller.show_frame(PlayerSearch) 418 | 419 | def active(self): 420 | if self.controller.api is None: 421 | self.controller.show_frame(Login) 422 | 423 | Base.active(self) 424 | self.logView.delete(1.0, tk.END) 425 | self.updateLog('%s Set Bid Options...\n' % (time.strftime('%Y-%m-%d %H:%M:%S'))) 426 | self.controller.status.set_status('Set Bid Options...') 427 | self.tree.delete(*self.tree.get_children()) 428 | for item in self.args['playerList']: 429 | displayName = item['player']['commonName'] if item['player']['commonName'] is not '' else item['player']['lastName'] 430 | try: 431 | self.tree.insert('', 'end', item['player']['id'], text=displayName, values=(item['buy'], item['sell'], item['bin'], 0)) 432 | except: 433 | pass 434 | 435 | self._lastUpdate = 0 436 | self._updatedItems = [] 437 | self.auctionsWon = 0 438 | self.sold = 0 439 | 440 | from frames.login import Login 441 | from frames.playersearch import PlayerSearch 442 | from fut.exceptions import FutError, ExpiredSession 443 | from requests.exceptions import RequestException, ConnectionError 444 | --------------------------------------------------------------------------------