anonymous_you

file_roulette

Aug 11th, 2022
99
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Python 10.17 KB | None | 0 0
  1. import tkinter as tk
  2. from tkinter.filedialog import askdirectory
  3. import os
  4. import sys
  5. import pathlib
  6. import json
  7. import threading
  8. import shutil
  9. import random
  10. import re
  11.  
  12. # Copies a random file from a specified directory tree to current dir.
  13.  
  14. class Consts:
  15.     SAVE_CFG_ON_EXIT = True
  16.     SHOW_TOOLTIPS = True
  17.  
  18.     # UI
  19.     LABEL_SIZE = 8
  20.     DEFAULT_PAD = 4
  21.  
  22. ##### Config #####
  23.  
  24. def generate_index(root_dirs, extensions, ignore, regex_ignore, callback):
  25.     global file_index
  26.     callback("Indexing... ")
  27.     try:
  28.         index = []
  29.  
  30.         sanitized_root_dirs = [val for val in root_dirs if len(val) > 0]
  31.         sanitized_extensions = [val for val in extensions if len(val) > 0]
  32.         sanitized_ignore = [val for val in ignore if len(val) > 0]
  33.  
  34.         for root_dir in sanitized_root_dirs:
  35.             index += [os.path.join(dp, f).replace('\\', '/') for dp, dn, filenames in os.walk(root_dir) for f in filenames if os.path.splitext(f)[1] in sanitized_extensions]
  36.  
  37.         if regex_ignore:
  38.             index = [path for path in index if (not any([re.findall(part, path) for part in sanitized_ignore]))]
  39.         else:
  40.             index = [path for path in index if (not any([part in path for part in sanitized_ignore]))]
  41.  
  42.         file_index = index
  43.         callback("[Indexed "+str(len(index))+" files] Ready ")
  44.     except:
  45.         callback("Something went wrong ")
  46.  
  47. def yoink_file(callback):
  48.     global file_index
  49.     try:
  50.         file = random.choice(file_index).strip()
  51.         callback("Yoinked "+file+" ")
  52.         shutil.copy2(file, ".")
  53.     except:
  54.         callback("Something went wrong ")
  55.  
  56. ##### Config #####
  57.  
  58. def get_datadir():
  59.     """
  60.     Returns a parent directory path
  61.     where persistent application data can be stored.
  62.  
  63.     # linux: ~/.local/share
  64.     # macOS: ~/Library/Application Support
  65.     # windows: C:/Users/<USER>/AppData/Roaming
  66.     """
  67.     home = pathlib.Path.home()
  68.  
  69.     if sys.platform == "win32":
  70.         return home / "AppData/Roaming"
  71.     elif sys.platform.startswith("linux"):
  72.         return home / ".local/share"
  73.     elif sys.platform == "darwin":
  74.         return home / "Library/Application Support"
  75.  
  76. def save_config():
  77.     print("Saving current config")
  78.     cfg_dir = get_datadir()
  79.     pathlib.Path(cfg_dir).mkdir(parents=True, exist_ok=True)
  80.     cfg_file = cfg_dir / "fileroulette_cfg.json"
  81.  
  82.     cfg = {}
  83.     cfg["src_dirs"] = [var.get() for var in vars_src_dir]
  84.     cfg["exts"] = var_exts.get()
  85.     cfg["excludes"] = var_excludes.get()
  86.     cfg["regex_excludes"] = var_regex_excludes.get()
  87.  
  88.     json_str = json.dumps(cfg, indent=4)
  89.     with open(cfg_file, "w") as f:
  90.         f.write(json_str)
  91.  
  92. def load_config():
  93.     print("Checking if config exists and loading")
  94.     cfg_dir = get_datadir()
  95.     cfg_file = cfg_dir / "fileroulette_cfg.json"
  96.     if cfg_file.exists():
  97.         lines = []
  98.         with open(cfg_file, "r") as f:
  99.             lines = f.readlines()
  100.         json_str = "\n".join(lines)
  101.         cfg = json.loads(json_str)
  102.  
  103.         if "exts" in cfg:
  104.             var_exts.set(cfg["exts"])
  105.         if "excludes" in cfg:
  106.             var_excludes.set(cfg["excludes"])
  107.         if "regex_excludes" in cfg:
  108.             var_regex_excludes.set(cfg["regex_excludes"])
  109.        
  110.         if "src_dirs" in cfg:
  111.             return cfg["src_dirs"]
  112.         else:
  113.             return [""]
  114.  
  115. ##### UI #####
  116.  
  117. class CreateToolTip(object):
  118.     def __init__(self, widget, text='widget info'):
  119.         if Consts.SHOW_TOOLTIPS:
  120.             self.waittime = 500     #miliseconds
  121.             self.wraplength = 180   #pixels
  122.             self.widget = widget
  123.             self.text = text
  124.             self.widget.bind("<Enter>", self.enter)
  125.             self.widget.bind("<Leave>", self.leave)
  126.             self.widget.bind("<ButtonPress>", self.leave)
  127.             self.id = None
  128.             self.tw = None
  129.  
  130.     def enter(self, event=None):
  131.         self.schedule()
  132.  
  133.     def leave(self, event=None):
  134.         self.unschedule()
  135.         self.hidetip()
  136.  
  137.     def schedule(self):
  138.         self.unschedule()
  139.         self.id = self.widget.after(self.waittime, self.showtip)
  140.  
  141.     def unschedule(self):
  142.         id = self.id
  143.         self.id = None
  144.         if id:
  145.             self.widget.after_cancel(id)
  146.  
  147.     def showtip(self, event=None):
  148.         x = y = 0
  149.         x, y, cx, cy = self.widget.bbox("insert")
  150.         x += self.widget.winfo_rootx() + 25
  151.         y += self.widget.winfo_rooty() + 20
  152.         # creates a toplevel window
  153.         self.tw = tk.Toplevel(self.widget)
  154.         # Leaves only the label and removes the app window
  155.         self.tw.wm_overrideredirect(True)
  156.         self.tw.wm_geometry("+%d+%d" % (x, y))
  157.         label = tk.Label(self.tw, text=self.text, justify='left', background="#ffffff", relief='solid', borderwidth=1, wraplength = self.wraplength)
  158.         label.pack(ipadx=1)
  159.  
  160.     def hidetip(self):
  161.         tw = self.tw
  162.         self.tw= None
  163.         if tw:
  164.             tw.destroy()
  165.  
  166. def LabeledInput(parent, title, textvar, hint=None):
  167.     container = tk.Frame(parent)
  168.    
  169.     label = tk.Label(container, text=title, width=18, anchor=tk.W, justify=tk.LEFT, font=(None, Consts.LABEL_SIZE))
  170.     entry = tk.Entry(container, textvariable=textvar)
  171.  
  172.     if hint != None:
  173.         CreateToolTip(entry, hint)
  174.  
  175.     label.pack(side=tk.TOP, fill=tk.X, expand=True)
  176.     entry.pack(side=tk.BOTTOM, fill=tk.X, expand=True, padx=2)
  177.  
  178.     return container
  179.  
  180. def LabeledInputWithActions(parent, title, textvar, action_text, action, hint=None, entry_state=tk.NORMAL, action2_text=None, action2=None):
  181.     container = tk.Frame(parent)
  182.  
  183.     entry = tk.Entry(container, textvariable=textvar, state=entry_state)
  184.     button = tk.Button(container, text=action_text, command=action)
  185.  
  186.     if hint != None:
  187.         CreateToolTip(entry, hint)
  188.  
  189.     if title != None:
  190.         label = tk.Label(container, text=title, width=18, anchor=tk.W, justify=tk.LEFT, font=(None, Consts.LABEL_SIZE))
  191.         label.pack(side=tk.TOP, fill=tk.X, expand=True)
  192.     entry.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=2)
  193.     if action2_text != None:
  194.         button2 = tk.Button(container, text=action2_text, command=action2)
  195.         button2.pack(side=tk.RIGHT, expand=False, padx=2)
  196.     button.pack(side=tk.RIGHT, expand=False, padx=2)
  197.    
  198.     return container
  199.  
  200. def LabeledInputWithCheckbox(parent, title, textvar, check_text, check_var, hint=None):
  201.     container = tk.Frame(parent)
  202.    
  203.     label = tk.Label(container, text=title, width=18, anchor=tk.W, justify=tk.LEFT, font=(None, Consts.LABEL_SIZE))
  204.     entry = tk.Entry(container, textvariable=textvar)
  205.     check = tk.Checkbutton(container, text=check_text, variable=check_var)
  206.  
  207.     if hint != None:
  208.         CreateToolTip(entry, hint)
  209.  
  210.     label.pack(side=tk.TOP, fill=tk.X, expand=True)
  211.     entry.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=2)
  212.     check.pack(side=tk.RIGHT, expand=False, padx=2)
  213.    
  214.     return container
  215.  
  216. def on_closing():
  217.     save_config()
  218.     window.destroy()
  219.  
  220. def apply_settings():
  221.     global vars_src_dir
  222.     dirs = [var.get() for var in vars_src_dir]
  223.     exts = var_exts.get().split(';')
  224.     ignore = var_excludes.get().replace('\\', '\\\\').split(';')
  225.     threading.Thread(target=lambda: generate_index(dirs, exts, ignore, var_regex_excludes.get(), lambda status: var_statusbar_text.set(status))).start()
  226.  
  227. def get_next_file():
  228.     print(file_index)
  229.     threading.Thread(target=lambda: yoink_file(lambda status: var_statusbar_text.set(status))).start()
  230.  
  231. def rm_src_dir(widget, var):
  232.     global widgets_src_dirs, vars_src_dir
  233.     vars_src_dir.remove(var)
  234.     widgets_src_dirs.remove(widget)
  235.     widget.destroy()
  236.  
  237. def add_src_dir(value=""):
  238.     global widgets_src_dirs, vars_src_dir
  239.     idx = len(vars_src_dir)
  240.     new_var = tk.StringVar()
  241.     new_var.set(value)
  242.     vars_src_dir.append(new_var)
  243.     new_input = LabeledInputWithActions(parent=window, title="Source dirs" if idx == 0 else None, textvar=new_var, action_text="Select", action=lambda: new_var.set(askdirectory()), action2_text=" − ", action2=lambda: rm_src_dir(new_input, new_var), hint="The root source directory from which to pull files.\nFiles will also be searched for recursively in all sub-directories.")
  244.     new_input.pack(side=tk.TOP, fill=tk.X, padx=Consts.DEFAULT_PAD, pady=Consts.DEFAULT_PAD)
  245.     widgets_src_dirs.append(new_input)
  246.  
  247. ##### Main #####
  248.  
  249. window = tk.Tk()
  250. window.title("FileRoulette")
  251.  
  252. file_index = []
  253.  
  254. widgets_src_dirs = []
  255. vars_src_dir = []
  256. var_exts = tk.StringVar()
  257. var_excludes = tk.StringVar()
  258. var_regex_excludes = tk.StringVar()
  259. var_statusbar_text = tk.StringVar()
  260.  
  261. add_dir = LabeledInputWithActions(parent=window, title=None, textvar=None, action_text=" + ", action=add_src_dir, hint="Add another source dir", entry_state=tk.DISABLED)
  262. ext = LabeledInput(parent=window, title="File types", textvar=var_exts, hint="List of extensions specifying which file types to search for.\nNeeds to be ';' separated.\nE.g. .mp4;.webm")
  263. excludes = LabeledInputWithCheckbox(parent=window, title="Excludes", textvar=var_excludes, check_text="Regex", check_var=var_regex_excludes, hint="List of exclusions specifying which files to ignore.\nNeeds to be ';' separated.\nIf any part of the file path/name matches, the file will be ignored.\nOptionally can parse each exclude as a regex instead of directly matching.\nAn easy way to exclude entire directories is by doing /dirname1/;/dirname2/")
  264. apply_resync = tk.Button(window, text="Apply/Resync", command=apply_settings)
  265. run = tk.Button(window, text="GET", height=3, width=70, command=get_next_file)
  266.  
  267. # statusbar inside its own container so that it can have a fixed size and prevent window resizing when status is too long
  268. container_statusbar = tk.Frame(window)
  269. statusbar = tk.Label(container_statusbar, bd=1, relief=tk.SUNKEN, anchor=tk.E, bg="lightgray", textvariable=var_statusbar_text)
  270.  
  271. container_statusbar.pack(side=tk.BOTTOM, fill=tk.X)
  272. statusbar.pack(side=tk.BOTTOM, fill=tk.X, ipadx=2, ipady=2)
  273. run.pack(side=tk.BOTTOM, fill=tk.X, padx=Consts.DEFAULT_PAD, pady=Consts.DEFAULT_PAD)
  274. apply_resync.pack(side=tk.BOTTOM, fill=tk.X, padx=Consts.DEFAULT_PAD, pady=Consts.DEFAULT_PAD)
  275. excludes.pack(side=tk.BOTTOM, fill=tk.X, padx=Consts.DEFAULT_PAD, pady=Consts.DEFAULT_PAD)
  276. ext.pack(side=tk.BOTTOM, fill=tk.X, padx=Consts.DEFAULT_PAD, pady=Consts.DEFAULT_PAD)
  277. add_dir.pack(side=tk.BOTTOM, fill=tk.X, padx=Consts.DEFAULT_PAD, pady=Consts.DEFAULT_PAD)
  278.  
  279. if Consts.SAVE_CFG_ON_EXIT:
  280.     window.protocol("WM_DELETE_WINDOW", on_closing)
  281. src_dirs = load_config()
  282.  
  283. add_src_dir(value=src_dirs[0])
  284.  
  285. apply_settings()
  286.  
  287. window.update()
  288. h = window.winfo_height()
  289. w = window.winfo_width()
  290. window.minsize(width=w, height=h)
  291.  
  292. # prevent statusbar from resizing windown when text is long after the first update
  293. container_statusbar.pack_propagate(False)
  294.  
  295. # min size should not count additional src dir fields so removing doesn't leave a weird gap if multiple values were initially loaded
  296. for src_dir in src_dirs[1:]:
  297.     add_src_dir(value=src_dir)
  298.  
  299. window.mainloop()
  300.  
Advertisement
Add Comment
Please, Sign In to add comment