-
Notifications
You must be signed in to change notification settings - Fork 2
/
SmartFrame.py
488 lines (405 loc) · 18 KB
/
SmartFrame.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
#!/usr/bin/python3
# RaddedMC's SmartFrame v2 -- SmartFrame.py
# This program is the primary wrapper for SmartFrame v2!
# Options
# -h / --help = Runs help() and quits
# -c = Asks user to confirm and if yes, clears configfile
# -j = Just get data
# Title() -- Prints the epic title screen
# ClearConfig() -- Asks for confirmation and then clears the user's config file.
# Config() -- Displays the config menu
# ReadConfig() -- Reads in the config file
# ConfigFileError(error) -- Displays a screen asking the user if they want to correct an error in their config file.
# GetCardData() -- Returns a Card array
# printS() -- just a simple output formatter
# Required deps: console-menu, termcolor
# KEY FILES: config.cfg
# Plugins/
builddate = "2021 - 08 - 31"
moduleName = "SmartFrame Main"
# Imports
import os, time, sys
from consolemenu import *
from consolemenu.items import *
import termcolor
from Card import Card
from GenerateImage import GenerateImage
from datetime import datetime
from datetime import timedelta
from ErrorLogger import logError
import multiprocessing
def printS(string, color = "white"):
from termcolor import colored
print(moduleName + " | " + colored(str(string), color))
refreshtime = -1
# Config object
class Output:
# Xres
xres = 0
# Yres
yres = 0
# scalefactor
scale = 0
# outputlocation
outputlocation = ""
# Introcommand
introcommand = ""
# Outrocommand
outrocommand = ""
# delete boolean
todelete = False
def __init__(self, xres, yres, scale, outputlocation = "", outrocommand = "", introcommand = "", todelete = False):
self.xres = xres
self.yres = yres
self.scale = scale
self.outputlocation = outputlocation
self.outrocommand = outrocommand
self.introcommand = introcommand
self.todelete = todelete
outputstr = "New Output Config | will output a " + str(self.xres) + "x" + str(self.yres) + "s" + str(self.scale) +" image to " + self.outputlocation + " "
if self.introcommand != "":
outputstr += "after running " + self.introcommand + " "
if self.outrocommand != "":
outputstr += "before running " + self.outrocommand + ". "
if self.todelete:
outputstr += "The output will be deleted."
print(outputstr)
def __str__(self):
outputstr = "Output config object: will output a " + str(self.xres) + "x" + str(self.yres) + "s" + str(self.scale) + " image to " + self.outputlocation + " "
if self.introcommand != "":
outputstr += "after running " + self.introcommand
if self.outrocommand != "":
outputstr += "before running " + self.outrocommand + "."
if self.todelete:
outputstr += "The output will be deleted."
return outputstr
def ConfigFileError(error = "Unknown error in config file!"):
openmenu = SelectionMenu("")
fixerrorchoice = openmenu.get_selection(["Reset config and exit"], "Error!", error+"\nWould you like to correct this yourself or delete the config file and start from scratch?")
if fixerrorchoice == 0:
ClearConfig()
exit()
else:
exit()
# Fancy title screen
def Title():
try:
widthlimit = os.get_terminal_size().columns
except OSError:
printS("Unable to get terminal width. We are not running in a console.")
return None
print("\nSmartFrame | Options [-h / -c / -j / -t]\n")
print(("{0:<"+str(widthlimit)+"}").format("███████╗███╗ ███╗ █████╗ ██████╗ ████████╗ "+termcolor.colored("║█═══╗", "red")))
print(("{0:<"+str(widthlimit)+"}").format("██╔════╝████╗ ████║██╔══██╗██╔══██╗╚══██╔══╝ "+termcolor.colored("║████╚══╗", "red")))
print(("{0:<"+str(widthlimit)+"}").format("███████╗██╔████╔██║███████║██████╔╝ ██║ "+termcolor.colored("║███████║", "red")))
print(("{0:<"+str(widthlimit)+"}").format("╚════██║██║╚██╔╝██║██╔══██║██╔══██╗ ██║ "+termcolor.colored("║███████║", "red")))
print(("{0:<"+str(widthlimit)+"}").format("███████║██║ ╚═╝ ██║██║ ██║██║ ██║ ██║ "+termcolor.colored("║████╔══╝", "red")))
print(("{0:<"+str(widthlimit)+"}").format("╚══════╝╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═╝ ╚═╝ "+termcolor.colored("║█═══╝", "red")))
print(("{0:<"+str(widthlimit)+"}").format(" "))
print(("{0:<"+str(widthlimit)+"}").format(termcolor.colored(" ╔═══█║", "blue")+" ███████╗██████╗ █████╗ ███╗ ███╗███████╗ "))
print(("{0:<"+str(widthlimit)+"}").format(termcolor.colored("╔══╝████║", "blue")+" ██╔════╝██╔══██╗██╔══██╗████╗ ████║██╔════╝ "))
print(("{0:<"+str(widthlimit)+"}").format(termcolor.colored("║███████║", "blue")+" █████╗ ██████╔╝███████║██╔████╔██║█████╗ "))
print(("{0:<"+str(widthlimit)+"}").format(termcolor.colored("║███████║", "blue")+" ██╔══╝ ██╔══██╗██╔══██║██║╚██╔╝██║██╔══╝ "))
print(("{0:<"+str(widthlimit)+"}").format(termcolor.colored("╚══╗████║", "blue")+" ██║ ██║ ██║██║ ██║██║ ╚═╝ ██║███████╗ "))
print(("{0:<"+str(widthlimit)+"}").format(termcolor.colored(" ╚═══█║", "blue")+" ╚═╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═╝╚══════╝ "))
print(("{0:>"+str(widthlimit)+"}").format("ascii art from https://manytools.org/hacker-tools/ascii-banner/ with some slight changes"))
print(("{0:<"+str(widthlimit)+"}").format("\_______________________________________________________________/"))
time.sleep(0.5)
print("By RaddedMC https://github.com/RaddedMC | https://youtube.com/RaddedMC | https://reddit.com/u/RaddedMC")
print(" with help from BOX OF COOL PEEPS (https://discord.gg/9Ms4bFw)")
time.sleep(0.5)
print(("{0:>"+str(widthlimit)+"}").format("@Raminh05 -- Plugin development and Linux testing"))
time.sleep(0.1)
print(("{0:>"+str(widthlimit)+"}").format("@kelvinhall05 -- Literature writing, General cringe, and Linux testing"))
time.sleep(0.1)
print(("{0:>"+str(widthlimit)+"}").format("@Friendly Fire Entertainment -- Advice for RaddedMC's SmartFrame hardware"))
time.sleep(0.1)
print(builddate+"\n")
time.sleep(1.5)
cleared = False
# Asks for confirmation and clears the user's config file
def ClearConfig():
# Set up menu
print("Opening ClearConfig menu...")
menu = ConsoleMenu("Clear Config File?", "Press 1 to delete your config file. This can't be restored later!")
menu.append_item(FunctionItem("Delete config.cfg", DeleteConfig, should_exit=True))
# Show menu
menu.show()
if not cleared:
exit(0)
def DeleteConfig():
try:
os.remove('config.cfg')
except FileNotFoundError:
print("You don't have a config file! Let's make one...")
cleared = True
def ReadConfig():
try:
printS("Reading config file...", "blue")
configfile = open("config.cfg", 'r')
configlines = configfile.readlines()
for index, line in enumerate(configlines):
configlines[index] = line.strip()
printS("Config file opened!", "green")
printS("Parsing configfile...", "blue")
printS("Getting refresh time...", "blue")
# Get refresh time
try:
global refreshtime
refreshtime = int(configlines[1])
except ValueError:
configfile.close()
ConfigFileError("It looks like your refreshtime is an invalid value. You have set it to: " + configlines[1] + ". This needs to be a number in seconds.")
except KeyboardInterrupt:
print("Keyboard interrupt detected! Exiting...")
exit(0)
printS("Refresh time is " + str(refreshtime) + " seconds!", "green")
printS("Setting up output objects...", "blue")
startdex = 16
outputObjects = []
while True:
printS("Starting at line "+ str(startdex) + " of config file...", "yellow")
currentXres = 0
currentYres = 0
currentScale = 0
currentOutputLocation = ""
currentCommands = []
currentToDelete = False
# If the startdex does not exist or is empty, break loop!
try:
if configlines[startdex] == "" or not configlines[startdex]:
break
except IndexError:
break
except KeyboardInterrupt:
print("Keyboard interrupt detected! Exiting...")
exit(0)
# Otherwise, first line of startdex is resolution
splitdex = configlines[startdex].find("x")
scaledex = configlines[startdex].find("s")
currentXres = configlines[startdex][:splitdex]
currentYres = configlines[startdex][splitdex+1:scaledex]
currentScale = configlines[startdex][scaledex+1:]
# Second line is output location
currentOutputLocation = configlines[startdex+1]
# If any of these lines aren't 'delete' or '#':
finaldex = 2
for line in configlines[startdex+2:startdex+6]:
if line == "#":
finaldex+=1
break
if line == "delete":
currentToDelete = True
else:
currentCommands.append(line)
finaldex += 1
try:
outputObjects.append(Output(currentXres, currentYres, currentScale, currentOutputLocation, currentCommands[0], currentCommands[1], currentToDelete))
except IndexError:
try:
outputObjects.append(Output(currentXres, currentYres, currentScale, currentOutputLocation, currentCommands[0], "", currentToDelete))
except IndexError:
outputObjects.append(Output(currentXres, currentYres, currentScale, currentOutputLocation, "", "", currentToDelete))
except KeyboardInterrupt:
print("Keyboard interrupt detected! Exiting...")
exit(0)
except KeyboardInterrupt:
print("Keyboard interrupt detected! Exiting...")
exit(0)
startdex+=finaldex
printS("Finished reading config file!", "green")
return outputObjects
except KeyboardInterrupt:
print("Keyboard interrupt detected! Exiting...")
exit(0)
except:
ConfigFileError("There was an error loading your config file! Double check your file or report an issue on the github!")
import traceback
traceback.print_exc()
exit()
# Creates a new config file
def Config():
config = ""
print("Opening Config File Menu...")
# Opening menu
openmenu = SelectionMenu("")
menchoice = openmenu.get_selection(["Let's go!"], "Welcome to SmartFrame!", "You're about to have a good time. Hopefully. SmartFrame starts out with a config file. Make one now?")
if menchoice == 0:
# Refresh time
menchoice = openmenu.get_selection(["Every minute (recommended)", "Every 5 minutes (recommended for users with slow internet or a lot of plugins)", "Every 30 seconds (recommended on fast controllers)", "As fast as possible! (not recommended, will spam APIs)"],
"Refresh time",
"How often should SmartFrame refresh data and generate a new image?")
options = {
0: "60\n",
1: "360\n",
2: "30\n",
3: "0\n",
4: "exit"
}
if not options.get(menchoice) == "exit":
config += "# Refresh time (seconds): Higher is better to prevent API spam, lower gives more up-to-date info. NOTE: No matter your Refresh Time, SmartFrame cannot run faster than your plugins can generate information. This value simply affects the sleep timer at the end of generating a photo.\n"
config += options.get(menchoice)
config += "# Use this area to set up output types. Start with the output resolution in the form 1920x1080s4, where 4 is the scale factor..\n"
config += "# On a new line, write the full file output path.\n"
config += "# On another line, write a command to run after generating the photo.\n"
config += "# On another line, write a command to run before generating the photo.\n"
config += "# If you only add one command, it will run after generating the photo.\n"
config += "# Finally, write 'delete' on another line at the end of your entry if you want the file to be deleted after the 'after' command is ran.\n"
config += "# Use a hashtag on a newline to seperate output entries.\n"
config += "# Ex:\n"
config += "# 1920x1080s4\n"
config += "# /home/RaddedMC/SmartFrame1080.png\n"
config += "# bash /home/RaddedMC/Scripts/SmartFrameOut.sh\n"
config += "# bash /home/RaddedMC/Scripts/SmartFrameIn.sh\n"
config += "# delete\n"
config += "# #\n"
file = open("config.cfg", "w")
file.write(config)
file.close()
print("You can now open your favourite text editor to edit " + os.getcwd().replace('\\', '/')+"/config.cfg"+" and finish configuration.")
# Output resolutions / companion outputters
print("Run SmartFrame again to get started.")
def runPlugin(file, send_end):
# Get and run modules
import importlib.util
from io import StringIO
spec = importlib.util.spec_from_file_location("module.name", "Plugins/" + file)
plugin = importlib.util.module_from_spec(spec)
printS("Starting plugin " + file, "cyan")
# Capture stdout to keep plugin output organized
sys.stdout = stdoutStream = StringIO()
# Check for compile errors
try:
spec.loader.exec_module(plugin)
# Run plugin
try:
plugincards = plugin.GetCard()
except:
import traceback
logError("Runtime error with plugin " + file + "!", traceback.format_exc(), moduleName)
plugincards = None
except:
import traceback
logError("There's a compiler error with plugin " + file + "! Contact the developer for this plugin or fix the error yourself and make a pull request." + file + "!", traceback.format_exc(), moduleName)
plugincards=None
finally:
# Regardless of whether everything works, return what you can, print what was outputted (including errors), and give back stdout
printS("Plugin " + file + " finished!", "green")
sys.stdout = sys.__stdout__
print(stdoutStream.getvalue())
send_end.send(plugincards)
# Main
def main():
# Argument parsing
import argparse
parser = argparse.ArgumentParser(description="See https://github.com/RaddedMC/SmartFrame for more help.")
parser.add_argument('-c', action="store_true", help = "Asks for confirmation and clears the config file")
parser.add_argument('-j', action="store_true", help = "Just parses data from plugins, useful for testing")
parser.add_argument('-t', action="store_true", help = "Skips the title screen :(")
args = parser.parse_args()
runonce = False
# Show title screen
if not args.t:
Title()
# Config file clearer
if args.c:
ClearConfig()
# Runs SmartFrame only once
if args.j:
printS("SmartFrame will run once and exit.", "magenta")
runonce = True
# If the config file doesn't exist, make one!
if not os.path.isfile('config.cfg'):
Config()
exit()
configs = ReadConfig()
printS("Starting main loop...", "cyan")
### MAIN LOOP ###
while True:
# Get current time
startGenTime = datetime.now()
# Gather cards from plugins
printS("Gathering cards...", "blue")
cards = []
files = os.listdir("Plugins/")
# Get list of plugins in plugin folder and create processes for them
tasks = []
pipe_list = []
if files:
for file in files:
if file.endswith(".py"):
printS("Assembling plugin " + file, "yellow")
recv_end, send_end = multiprocessing.Pipe(False)
p = multiprocessing.Process(target=runPlugin, args=(file, send_end))
tasks.append(p)
pipe_list.append(recv_end)
p.start()
else:
printS("Error: There are no plugins! Please add some plugins before using SmartFrame.", "red")
exit(0)
returnvalues = [x.recv() for x in pipe_list]
for job in tasks:
job.join()
printS("All plugins finished! Assembling output...", "green")
# Sort output from tasks
for value in returnvalues:
if isinstance(value, list):
for card in value:
cards.append(card) # Append all new cards to variable cards
elif isinstance(value, Card):
cards.append(value)
else:
continue
printS("Cards assembled!", "green")
# Output images
printS("Preparing outputs...", "blue")
for idx, config in enumerate(configs):
try:
# Run before scripts
if (config.outrocommand):
printS("Running " + config.introcommand + " ...", "blue")
printS("Exit code: " + str(os.system(config.introcommand)), "green")
# Generate images
printS("Generating image #" + str(idx) + "...", "blue")
GenerateImage(cards, int(config.xres), int(config.yres), int(config.scale)).save(config.outputlocation.replace('\\','/'))
# Run after scripts
if (config.outrocommand):
printS("Running " + config.outrocommand + " ...", "blue")
printS("Exit code: " + str(os.system(config.outrocommand)), "green")
# Clear cards
if (config.todelete):
printS("Deleting " + config.outputlocation + "...", "blue")
os.remove(config.outputlocation)
printS("Deleted " + config.outputlocation + "!", "green")
printS("Image generated! Poggers!!!", "cyan")
except KeyboardInterrupt:
print("Keyboard interrupt detected! Exiting...")
exit(0)
except:
import traceback
logError("There was an error generating image #" + str(idx) + "! Double check your config file or report an issue on the github!", traceback.format_exc(), moduleName)
continue
# Delete card output images
printS("Clearing cards...")
for card in cards:
os.remove(card.fileLocation)
printS("Cleared card " + card.fileLocation)
# Calculate sleep time
#print(startGenTime)
nowTime = datetime.now()
#print(nowTime)
#print(refreshtime)
difference = timedelta(0,refreshtime)
#print(difference)
endTime = startGenTime + difference
#print(endTime)
sleepduration = (endTime - nowTime).total_seconds()
#print(sleepduration)
if runonce:
printS("Finished! Exiting...", "green")
exit(0)
printS("Sleeping for " + str(sleepduration) + " seconds...", "yellow")
if sleepduration > 0:
time.sleep(sleepduration)
if __name__ == "__main__":
main()