anonymous_you

awemepy-1.0

Aug 9th, 2022 (edited)
535
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Python 24.94 KB | None | 0 0
  1. import tkinter as tk
  2. from tkinter.filedialog import askopenfilename, askdirectory
  3. from tkinter.messagebox import showerror
  4. from enum import Enum, auto
  5. import subprocess
  6. import os
  7. import sys
  8. import pathlib
  9. import json
  10. import threading
  11. import re
  12. from tempfile import TemporaryDirectory
  13.  
  14. # Changelog
  15. # 1.0 - further tweaked filename parsing so hevc files with no description don't get left behind when convert and delete is enabled
  16. # beta9 - added sessionid to cookie flag; minor improvement to cleanup (long filenames shouldn't leave behind hevc files when convert and delete is enabled)
  17. # beta8 - downloading multiple ids now uses a temp file instead of pipes
  18. # beta7 - redownloading with rename sd option now deletes the new file instead of throwing an error when failing to rename because of file already existing
  19. # beta6 - rename sd option added
  20. # beta5 - date in filename option added
  21. # beta4 - adjusted how ids are added to the command (user stories may not have been working)
  22. # beta3 - user agent field added
  23. # beta2 - any errors while running videos-node will now dump stderr to an alert dialog; max file length has been renamed to be more indicative of what it does
  24. # beta1 - init
  25.  
  26. class Consts:
  27.     SAVE_CFG_ON_EXIT = True
  28.     SAVE_IDS_FIELD_ON_SAVE_CFG = False
  29.     SHOW_TOOLTIPS = True
  30.    
  31.     # AwemerWrapper
  32.     IGNORE_ALREADY_DOWNLOADED = True
  33.     IGNORE_EMPTY_DOWNLOADS = True
  34.     HEVC_SFX = "~hevc"
  35.     CONV_SFX = "~h264"
  36.     SD_SFX = "~sd"
  37.     MAX_RETRIES = 10
  38.     MAX_CONVERTS = 10
  39.     MAX_DOWNLOADS = 1
  40.  
  41.     # UI
  42.     LABEL_SIZE = 8
  43.     DEFAULT_PAD = 4
  44.  
  45.  
  46. class AwemerWrapper:
  47.  
  48.     def __init__(self):
  49.         self._ids = None
  50.         self._hq = None
  51.         self._add_date = None
  52.         self._verify = None
  53.         self._convert = None
  54.         self._delete = None
  55.         self._rm_sd_suffix = None
  56.         self._sid = None
  57.         self._uid = None
  58.         self._usr_agent = None
  59.         self._rand_usr_agent = None
  60.         self._max_results = None
  61.         self._max_fname_len = None
  62.         self._save_dir = None
  63.         self._awemer_loc = None
  64.         self._additional_flags = None
  65.         self._t_param = None
  66.         self._tmp_ids_dir = None
  67.  
  68.     def with_ids(self, ids):
  69.         self._ids = ids.strip().replace('\r', '').replace('\n', ' ')
  70.         return self
  71.  
  72.     def with_hq(self, hq):
  73.         self._hq = hq
  74.         return self
  75.  
  76.     def with_add_date(self, add_date):
  77.         self._add_date = add_date
  78.         return self
  79.  
  80.     def with_verify(self, verify):
  81.         self._verify = verify
  82.         return self
  83.  
  84.     def with_convert(self, convert):
  85.         self._convert = convert
  86.         return self
  87.  
  88.     def with_delete(self, delete):
  89.         self._delete = delete
  90.         return self
  91.  
  92.     def with_rm_sd_suffix(self, rm_sd_suffix):
  93.         self._rm_sd_suffix = rm_sd_suffix
  94.         return self
  95.  
  96.     def with_sid(self, sid):
  97.         self._sid = sid.strip()
  98.         return self
  99.  
  100.     def with_uid(self, uid):
  101.         self._uid = uid.strip()
  102.         return self
  103.  
  104.     def with_usr_agent(self, usr_agent):
  105.         self._usr_agent = usr_agent.strip()
  106.         return self
  107.  
  108.     def with_random_usr_agent(self, rand_usr_agent):
  109.         self._rand_usr_agent = rand_usr_agent
  110.         return self
  111.  
  112.     def with_max_results(self, max_results):
  113.         self._max_results = max_results.strip()
  114.         return self
  115.  
  116.     def with_max_filename_len(self, max_fname_len):
  117.         self._max_fname_len = max_fname_len.strip()
  118.         return self
  119.  
  120.     def with_save_dir(self, save_dir):
  121.         self._save_dir = save_dir.strip()
  122.         return self
  123.  
  124.     def with_awemer(self, awemer_loc):
  125.         self._awemer_loc = awemer_loc.strip()
  126.         return self
  127.  
  128.     def _file_ends_with(self, ext, f):
  129.         try:
  130.             return f.split(".")[-2].endswith(ext)
  131.         except:
  132.             return False
  133.  
  134.     def _get_id(self, fname):
  135.         return fname.split(".")[0].split("~")[0].split(" ")[0]
  136.  
  137.     def _cleanup_specific(self, suffix, filenames):
  138.         for f in filter(lambda f: self._file_ends_with(suffix, f), filenames):
  139.             try:
  140.                 parts = f.split(".")
  141.                 parts[-2] = parts[-2].removesuffix(suffix).rstrip()
  142.                 renamed = ".".join(parts)
  143.                 os.rename(self._save_dir + "/" + f, self._save_dir + "/" + renamed)
  144.                 print(f + " -> " + renamed)
  145.             except FileExistsError:
  146.                 os.remove(self._save_dir + "/" + f)
  147.                 print("File exists, removing " + f)
  148.  
  149.     def _cleanup(self):
  150.         if self._convert and self._delete:
  151.             print("Cleaning tp HEVC")
  152.             filenames = next(os.walk(self._save_dir), (None, None, []))[2]
  153.             ids = [self._get_id(f) for f in filenames if self._file_ends_with(Consts.CONV_SFX, f)]
  154.             for f in filter(lambda f: self._file_ends_with(Consts.HEVC_SFX, f), filenames):
  155.                 if self._get_id(f) in ids:
  156.                     print("Converted h264 exists, removing HEVC " + f)
  157.                     os.remove(self._save_dir + "/" + f)
  158.                 else:
  159.                     print("MISSING HEVC CONVERSION FOR " + f)
  160.  
  161.             print("Cleaning up H264 suffixes")
  162.             filenames = next(os.walk(self._save_dir), (None, None, []))[2]
  163.             self._cleanup_specific(Consts.CONV_SFX, filenames)
  164.  
  165.         if self._rm_sd_suffix:
  166.             print("Cleaning up SD filenames")
  167.             filenames = next(os.walk(self._save_dir), (None, None, []))[2]
  168.             self._cleanup_specific(Consts.SD_SFX, filenames)
  169.  
  170.         if self._tmp_ids_dir != None:
  171.             print("Cleaning up temp files")
  172.             self._tmp_ids_dir.cleanup()
  173.  
  174.     def _exec(self, callback, err_callback):
  175.         print("Assembling awemer command and executing")
  176.         cmd = "node \"" + str(self._awemer_loc) + "\""
  177.  
  178.         if self._ids != None and self._ids != "":
  179.             if re.findall("\\s", self._ids):
  180.                 # Temp files can be a little tricky on different systems -
  181.                 # may not be able to have multiple instances open at the same time and file disappears as soon as it is closed (node needs a filename to open, not a python file).
  182.                 # Using instead temp dir which can be opened here and deleted along with all its contents in _cleanup().
  183.                 self._tmp_ids_dir = TemporaryDirectory()
  184.                 fname = self._tmp_ids_dir.name + "/ids.txt"
  185.                 with open(fname, "w") as f:
  186.                     f.write(self._ids)
  187.                 cmd += " i=\"" + str(fname) + "\""
  188.  
  189.                 # Alternatively this was the piping jankery without using temp files, but it might not work correcty for multiple ids at once.
  190.                 # # pipe jankery to get videos-node to read whitespace separated ids from stdin (requires shell=True)
  191.                 # cmd = "echo " + str(self._ids.replace('\r', '').replace('\n', ' ')) + " | " + cmd + " i="
  192.             else:
  193.                 # single ids sometimes don't like getting piped, e.g. when scraping user stories (that needs a literal i=<id> param)
  194.                 cmd += " i=" + str(self._ids)
  195.         elif self._uid != None:
  196.             cmd += " i=" + str(self._uid)
  197.  
  198.         if self._hq:
  199.             cmd += " q=2"
  200.         else:
  201.             cmd += " q=0"
  202.  
  203.         if self._add_date:
  204.             cmd += " c"
  205.  
  206.         if self._verify:
  207.             cmd += " verify_files"
  208.  
  209.         if self._convert:
  210.             cmd += " convert_hevc"
  211.             cmd += " rename_hevc"
  212.  
  213.         if self._sid != None and self._sid != "":
  214.             cmd += " j=sid_tt=" + str(self._sid) + ";sessionid=" + str(self._sid)
  215.             cmd += " cookie=" + str(self._sid)
  216.  
  217.         if self._rand_usr_agent:
  218.             cmd += " ur"
  219.         elif self._usr_agent != None and self._usr_agent != "":
  220.             cmd += " u=\"" + str(self._usr_agent)+"\""
  221.  
  222.         if self._max_results != None and self._max_results != "":
  223.             cmd += " l=" + str(self._max_results)
  224.  
  225.         if self._max_fname_len != None and self._max_fname_len != "":
  226.             cmd += " d=" + str(self._max_fname_len)
  227.  
  228.         if self._save_dir != None and self._save_dir != "":
  229.             cmd += " f=\"" + str(self._save_dir)+"\""
  230.  
  231.         if self._additional_flags != None and self._additional_flags != "":
  232.             cmd += " " + str(self._additional_flags)
  233.  
  234.         if self._t_param != None and self._t_param != "":
  235.             cmd += " t=" + str(self._t_param)
  236.  
  237.         cmd += " n=" + str(Consts.MAX_RETRIES) + " max_resumes=" + str(Consts.MAX_RETRIES) + " max_retries=" + str(Consts.MAX_RETRIES)
  238.         cmd += " max_converts=" + str(Consts.MAX_CONVERTS) + " max_downloads=" + str(Consts.MAX_DOWNLOADS)
  239.        
  240.         if Consts.IGNORE_ALREADY_DOWNLOADED:
  241.             cmd += " ignore_ids"
  242.  
  243.         if Consts.IGNORE_EMPTY_DOWNLOADS:
  244.             cmd += " skip_empty"
  245.  
  246.         print("Executing:")
  247.         print(cmd)
  248.         error_lines = []
  249.         # shell=False opens a terminal window while node runs, =True does not
  250.         with subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, bufsize=0, shell=True) as p:
  251.             for line in iter(p.stdout.readline, b''):
  252.                 callback(line.decode("utf-8").strip())
  253.             for line in iter(p.stderr.readline, b''):
  254.                 error_lines.append(line.decode("utf-8").strip())
  255.         self._cleanup()
  256.  
  257.         if len(error_lines) > 0:
  258.             err_callback("\n".join(error_lines))
  259.  
  260.         callback("Done.")
  261.         print("Done")
  262.  
  263.     # start downloading a specific type, the stdout will be returned in real time in the callback param, err_callback will be called at the end with all of stderr if there were errors
  264.     def execute(self, download_type, callback, err_callback):
  265.         match download_type:
  266.             case DownloadType.VIDEOS:
  267.                 callback("Scraping videos...")
  268.                 self._t_param = "v"
  269.             case DownloadType.STORIES:
  270.                 callback("Scraping stories...")
  271.                 self._t_param = "us"
  272.             case DownloadType.IMAGES:
  273.                 callback("Scraping images...")
  274.                 self._t_param = "v"
  275.                 self._additional_flags = "images"
  276.             case DownloadType.LIKES:
  277.                 callback("Scraping likes...")
  278.                 self._t_param = "ul"
  279.  
  280.         self._exec(callback, err_callback)
  281.  
  282. class DownloadType(Enum):
  283.     VIDEOS = auto()
  284.     STORIES = auto()
  285.     IMAGES = auto()
  286.     LIKES = auto()
  287.  
  288. ##### Config #####
  289.  
  290. def get_datadir():
  291.     """
  292.     Returns a parent directory path
  293.     where persistent application data can be stored.
  294.  
  295.     # linux: ~/.local/share
  296.     # macOS: ~/Library/Application Support
  297.     # windows: C:/Users/<USER>/AppData/Roaming
  298.     """
  299.     home = pathlib.Path.home()
  300.  
  301.     if sys.platform == "win32":
  302.         return home / "AppData/Roaming"
  303.     elif sys.platform.startswith("linux"):
  304.         return home / ".local/share"
  305.     elif sys.platform == "darwin":
  306.         return home / "Library/Application Support"
  307.  
  308. def save_config():
  309.     print("Saving current config")
  310.     cfg_dir = get_datadir()
  311.     pathlib.Path(cfg_dir).mkdir(parents=True, exist_ok=True)
  312.     cfg_file = cfg_dir / "awemepy_cfg.json"
  313.  
  314.     cfg = {}
  315.     if Consts.SAVE_IDS_FIELD_ON_SAVE_CFG:
  316.         cfg["ids"] = var_ids.get()
  317.     cfg["max_vids"] = var_max_vids.get()
  318.     cfg["max_file_len"] = var_max_file_len.get()
  319.     cfg["sid"] = var_sid.get()
  320.     cfg["uid"] = var_uid.get()
  321.     cfg["usr_agent"] = var_usr_agent.get()
  322.     cfg["rand_usr_agent"] = var_rand_usr_agent.get()
  323.     cfg["save_dir"] = var_save_dir.get()
  324.     cfg["awemer"] = var_awemer.get()
  325.     cfg["hq"] = var_hq.get()
  326.     cfg["add_date"] = var_add_date.get()
  327.     cfg["verify"] = var_verify.get()
  328.     cfg["convert"] = var_convert.get()
  329.     cfg["delete"] = var_delete.get()
  330.     cfg["rm_sd_suffix"] = var_rm_sd_suffix.get()
  331.  
  332.     json_str = json.dumps(cfg, indent=4)
  333.     with open(cfg_file, "w") as f:
  334.         f.write(json_str)
  335.  
  336. def load_config():
  337.     print("Checking if config exists and loading")
  338.     cfg_dir = get_datadir()
  339.     cfg_file = cfg_dir / "awemepy_cfg.json"
  340.     if cfg_file.exists():
  341.         lines = []
  342.         with open(cfg_file, "r") as f:
  343.             lines = f.readlines()
  344.         json_str = "\n".join(lines)
  345.         cfg = json.loads(json_str)
  346.  
  347.         if "ids" in cfg:
  348.             var_ids.set(cfg["ids"])
  349.         if "max_vids" in cfg:
  350.             var_max_vids.set(cfg["max_vids"])
  351.         if "max_file_len" in cfg:
  352.             var_max_file_len.set(cfg["max_file_len"])
  353.         if "sid" in cfg:
  354.             var_sid.set(cfg["sid"])
  355.         if "uid" in cfg:
  356.             var_uid.set(cfg["uid"])
  357.         if "usr_agent" in cfg:
  358.             var_usr_agent.set(cfg["usr_agent"])
  359.         if "rand_usr_agent" in cfg:
  360.             var_rand_usr_agent.set(cfg["rand_usr_agent"])
  361.         if "save_dir" in cfg:
  362.             var_save_dir.set(cfg["save_dir"])
  363.         if "awemer" in cfg:
  364.             var_awemer.set(cfg["awemer"])
  365.         if "hq" in cfg:
  366.             var_hq.set(cfg["hq"])
  367.         if "add_date" in cfg:
  368.             var_add_date.set(cfg["add_date"])
  369.         if "verify" in cfg:
  370.             var_verify.set(cfg["verify"])
  371.         if "convert" in cfg:
  372.             var_convert.set(cfg["convert"])
  373.         if "delete" in cfg:
  374.             var_delete.set(cfg["delete"])
  375.         if "rm_sd_suffix" in cfg:
  376.             var_rm_sd_suffix.set(cfg["rm_sd_suffix"])
  377.  
  378. ##### Main window parts #####
  379.  
  380. def MainInputAndDownloadBtns(parent, var_ids, action_dl_vids, action_dl_stories, action_dl_imgs, action_dl_likes):
  381.     container = tk.Frame(parent)
  382.  
  383.     ids = IdsInput(container, var_ids)
  384.     btns = DownloadButtons(container, action_dl_vids, action_dl_stories, action_dl_imgs, action_dl_likes)
  385.    
  386.     ids.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=Consts.DEFAULT_PAD, pady=Consts.DEFAULT_PAD)
  387.     btns.pack(side=tk.RIGHT, expand=False, fill=tk.Y, padx=Consts.DEFAULT_PAD, pady=Consts.DEFAULT_PAD)
  388.    
  389.     return container
  390.  
  391. def CheckboxesAndLimitInputs(parent, var_hq, var_add_date, var_verify, var_convert, var_delete, var_rm_sd_suffix, var_max_vids, var_max_file_len):
  392.     container = tk.Frame(parent)
  393.  
  394.     checkboxes = Checkboxes(container, var_hq, var_add_date, var_verify, var_convert, var_delete, var_rm_sd_suffix)
  395.     limits = LimitInputs(container, var_max_vids, var_max_file_len)
  396.  
  397.     checkboxes.pack(side=tk.LEFT, expand=False, fill=tk.X, padx=Consts.DEFAULT_PAD)
  398.     limits.pack(side=tk.RIGHT, expand=True, fill=tk.X, padx=Consts.DEFAULT_PAD)
  399.  
  400.     return container
  401.  
  402. def ConfigInputs(parent, var_sid, var_uid, var_save_dir, var_awemer, var_usr_agent, var_rand_usr_agent):
  403.     container = tk.Frame(parent)
  404.  
  405.     input_sid = LabeledInput(container, title="sid_tt", textvar=var_sid, hint="Your private sid_tt cookie from \".tiktok.com\"")
  406.     input_uid = LabeledInput(container, title="(You)r uid", textvar=var_uid, hint="Your user ID")
  407.     input_usr_agent = LabeledInputWithCheckbox(container, title="User-Agent", textvar=var_usr_agent, hint="Set a custom User-Agent or use a random one. Leave blank for default.", check_text="Randomize", check_var=var_rand_usr_agent)
  408.     input_save_dir = LabeledInputWithAction(container, title="Save to", textvar=var_save_dir, hint="Save downloads to this location", action_text="Select", action=lambda: var_save_dir.set(askdirectory()))
  409.     input_awemer_dir = LabeledInputWithAction(container, title="videos-node.js", textvar=var_awemer, hint="Location of the videos-node.js file", action_text="Select", action=lambda: var_awemer.set(askopenfilename(filetypes=[("videos-node.js", "*.js")])))
  410.  
  411.     input_sid.pack(side=tk.TOP, expand=True, fill=tk.X, padx=Consts.DEFAULT_PAD, pady=Consts.DEFAULT_PAD)
  412.     input_uid.pack(side=tk.TOP, expand=True, fill=tk.X, padx=Consts.DEFAULT_PAD, pady=Consts.DEFAULT_PAD)
  413.     input_usr_agent.pack(side=tk.TOP, expand=True, fill=tk.X, padx=Consts.DEFAULT_PAD, pady=Consts.DEFAULT_PAD)
  414.     input_save_dir.pack(side=tk.TOP, expand=True, fill=tk.X, padx=Consts.DEFAULT_PAD, pady=Consts.DEFAULT_PAD)
  415.     input_awemer_dir.pack(side=tk.TOP, expand=True, fill=tk.X, padx=Consts.DEFAULT_PAD, pady=Consts.DEFAULT_PAD)
  416.    
  417.     return container
  418.  
  419. ##### Sub-window parts #####
  420.  
  421. def IdsInput(parent, var_ids):
  422.     container = tk.Frame(parent)
  423.  
  424.     label = tk.Label(container, text="IDs", width=18, anchor=tk.W, justify=tk.LEFT, font=(None, Consts.LABEL_SIZE))
  425.     id_field = tk.Text(container, width=28, height=10)
  426.    
  427.     id_field.bind('<KeyRelease>', lambda *args: var_ids.set(id_field.get("1.0", tk.END)[:-1])) # get seems to \n terminate the string additionally
  428.     var_ids.trace_add("write", lambda *args: set_text_widget(id_field, var_ids.get()))
  429.  
  430.     CreateToolTip(id_field, "IDs or @name of a user, IDs of a music, IDs or #tag of a hashtag, IDs of a video.\nIf this is blank (You)r UID will be used for the user parameter instead.")
  431.    
  432.     label.pack(side=tk.TOP, fill=tk.X, expand=False)
  433.     id_field.pack(side=tk.TOP, fill=tk.BOTH, expand=True)
  434.  
  435.     return container
  436.  
  437. def DownloadButtons(parent, action_dl_vids, action_dl_stories, action_dl_imgs, action_dl_likes):
  438.     container = tk.Frame(parent)
  439.  
  440.     label = tk.Label(container, text="Download", width=18, anchor=tk.W, justify=tk.LEFT, font=(None, Consts.LABEL_SIZE))
  441.     dl_videos = tk.Button(container, text="Videos", height=2, command=action_dl_vids)
  442.     dl_stories = tk.Button(container, text="Stories", height=2, command=action_dl_stories)
  443.     dl_images = tk.Button(container, text="Images", height=2, command=action_dl_imgs)
  444.     dl_likes = tk.Button(container, text="Likes", height=2, command=action_dl_likes)
  445.  
  446.     label.pack(side=tk.TOP, fill=tk.X, expand=False)
  447.     dl_videos.pack(side=tk.TOP, fill=tk.X, expand=False, pady=Consts.DEFAULT_PAD)
  448.     dl_stories.pack(side=tk.TOP, fill=tk.X, expand=False, pady=Consts.DEFAULT_PAD)
  449.     dl_images.pack(side=tk.TOP, fill=tk.X, expand=False, pady=Consts.DEFAULT_PAD)
  450.     dl_likes.pack(side=tk.TOP, fill=tk.X, expand=False, pady=Consts.DEFAULT_PAD)
  451.    
  452.     return container
  453.  
  454. def Checkboxes(parent, var_hq, var_add_date, var_verify, var_convert, var_delete, var_rm_sd_suffix):
  455.     container = tk.Frame(parent)
  456.  
  457.     opt_hq = tk.Checkbutton(container, text='HD', variable=var_hq)
  458.     opt_add_date = tk.Checkbutton(container, text='Date in filename', variable=var_add_date)
  459.     opt_verify = tk.Checkbutton(container, text='Verify', variable=var_verify)
  460.     opt_convert = tk.Checkbutton(container, text='Convert HEVC', variable=var_convert)
  461.     opt_delete = tk.Checkbutton(container, text='Delete HEVC', variable=var_delete)
  462.     opt_rm_sd_suffix = tk.Checkbutton(container, text='Rename SD', variable=var_rm_sd_suffix)
  463.  
  464.     CreateToolTip(opt_hq, "Download higher quality videos (downloads standard quality if HD fails)")
  465.     CreateToolTip(opt_add_date, "Add the timestamp to the saved video filename")
  466.     CreateToolTip(opt_verify, "[Requires ffprobe]\nDownload videos with a ~verify suffix, pass them through ffprobe after downloading to check their integrity, and rename with an ~error suffix if it fails")
  467.     CreateToolTip(opt_convert, "[Requires ffprobe & ffmpeg]\nIf the downloaded video is HEVC then convert it to a H264 version with a ~h264 suffix")
  468.     CreateToolTip(opt_delete, "Clean up downloads after converting - deletes HEVC files and renames H264 files to have no suffix")
  469.     CreateToolTip(opt_rm_sd_suffix, "Clean up downloads - renames SD files to not have the '~sd' suffix")
  470.  
  471.     toggle_enabled(opt_delete, var_convert.get(), var_delete)
  472.     var_convert.trace("w", lambda *args: toggle_enabled(opt_delete, var_convert.get(), var_delete))
  473.  
  474.     opt_hq.pack(side=tk.TOP, expand=False, anchor=tk.W)
  475.     opt_add_date.pack(side=tk.TOP, expand=False, anchor=tk.W)
  476.     opt_verify.pack(side=tk.TOP, expand=False, anchor=tk.W)
  477.     opt_convert.pack(side=tk.TOP, expand=False, anchor=tk.W)
  478.     opt_delete.pack(side=tk.TOP, expand=False, anchor=tk.W)
  479.     opt_rm_sd_suffix.pack(side=tk.TOP, expand=False, anchor=tk.W)
  480.  
  481.     return container
  482.  
  483. def LimitInputs(parent, var_max_vids, var_max_file_len):
  484.     container = tk.Frame(parent)
  485.  
  486.     input_max_vids = LabeledInput(container, title="Max videos", textvar=var_max_vids, hint="Maximum number of items to download.\nAlternatively can be a date how far back to fetch (formatted as yyyy-MM-dd).")
  487.     input_filename_len = LabeledInput(container, title="Description length", textvar=var_max_file_len, hint="Append the video description to the filename.\nLeave empty for no description, 0 for unlimited length or a number to limit the length of the description.")
  488.  
  489.     input_max_vids.pack(side=tk.TOP, expand=True, fill=tk.X)
  490.     input_filename_len.pack(side=tk.TOP, expand=True, fill=tk.X)
  491.  
  492.     return container
  493.  
  494. ##### UI Utility #####
  495.  
  496. class CreateToolTip(object):
  497.     def __init__(self, widget, text='widget info'):
  498.         if Consts.SHOW_TOOLTIPS:
  499.             self.waittime = 500     #miliseconds
  500.             self.wraplength = 180   #pixels
  501.             self.widget = widget
  502.             self.text = text
  503.             self.widget.bind("<Enter>", self.enter)
  504.             self.widget.bind("<Leave>", self.leave)
  505.             self.widget.bind("<ButtonPress>", self.leave)
  506.             self.id = None
  507.             self.tw = None
  508.  
  509.     def enter(self, event=None):
  510.         self.schedule()
  511.  
  512.     def leave(self, event=None):
  513.         self.unschedule()
  514.         self.hidetip()
  515.  
  516.     def schedule(self):
  517.         self.unschedule()
  518.         self.id = self.widget.after(self.waittime, self.showtip)
  519.  
  520.     def unschedule(self):
  521.         id = self.id
  522.         self.id = None
  523.         if id:
  524.             self.widget.after_cancel(id)
  525.  
  526.     def showtip(self, event=None):
  527.         x = y = 0
  528.         x, y, cx, cy = self.widget.bbox("insert")
  529.         x += self.widget.winfo_rootx() + 25
  530.         y += self.widget.winfo_rooty() + 20
  531.         # creates a toplevel window
  532.         self.tw = tk.Toplevel(self.widget)
  533.         # Leaves only the label and removes the app window
  534.         self.tw.wm_overrideredirect(True)
  535.         self.tw.wm_geometry("+%d+%d" % (x, y))
  536.         label = tk.Label(self.tw, text=self.text, justify='left', background="#ffffff", relief='solid', borderwidth=1, wraplength = self.wraplength)
  537.         label.pack(ipadx=1)
  538.  
  539.     def hidetip(self):
  540.         tw = self.tw
  541.         self.tw= None
  542.         if tw:
  543.             tw.destroy()
  544.  
  545. def LabeledInput(parent, title, textvar, hint=None):
  546.     container = tk.Frame(parent)
  547.    
  548.     label = tk.Label(container, text=title, width=18, anchor=tk.W, justify=tk.LEFT, font=(None, Consts.LABEL_SIZE))
  549.     entry = tk.Entry(container, textvariable=textvar)
  550.  
  551.     if hint != None:
  552.         CreateToolTip(entry, hint)
  553.  
  554.     label.pack(side=tk.TOP, fill=tk.X, expand=True)
  555.     entry.pack(side=tk.BOTTOM, fill=tk.X, expand=True, padx=2)
  556.  
  557.     return container
  558.  
  559. def LabeledInputWithAction(parent, title, textvar, action_text, action, hint=None):
  560.     container = tk.Frame(parent)
  561.    
  562.     label = tk.Label(container, text=title, width=18, anchor=tk.W, justify=tk.LEFT, font=(None, Consts.LABEL_SIZE))
  563.     entry = tk.Entry(container, textvariable=textvar)
  564.     button = tk.Button(container, text=action_text, command=action)
  565.  
  566.     if hint != None:
  567.         CreateToolTip(entry, hint)
  568.  
  569.     label.pack(side=tk.TOP, fill=tk.X, expand=True)
  570.     entry.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=2)
  571.     button.pack(side=tk.RIGHT, expand=False, padx=2)
  572.    
  573.     return container
  574.  
  575. def LabeledInputWithCheckbox(parent, title, textvar, check_text, check_var, hint=None):
  576.     container = tk.Frame(parent)
  577.    
  578.     label = tk.Label(container, text=title, width=18, anchor=tk.W, justify=tk.LEFT, font=(None, Consts.LABEL_SIZE))
  579.     entry = tk.Entry(container, textvariable=textvar)
  580.     check = tk.Checkbutton(container, text=check_text, variable=check_var)
  581.  
  582.     toggle_enabled(entry, not check_var.get(), None)
  583.     check_var.trace_add("write", lambda *args: toggle_enabled(entry, not check_var.get(), None))
  584.  
  585.     if hint != None:
  586.         CreateToolTip(entry, hint)
  587.  
  588.     label.pack(side=tk.TOP, fill=tk.X, expand=True)
  589.     entry.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=2)
  590.     check.pack(side=tk.RIGHT, expand=False, padx=2)
  591.    
  592.     return container
  593.  
  594. def AlertDialog(text):
  595.     showerror(title="Something went wrong", message=text)
  596.  
  597. def toggle_enabled(widget, enabled, var_value):
  598.     if enabled:
  599.         widget["state"] = tk.NORMAL
  600.     else:
  601.         if var_value != None:
  602.             var_value.set(False)
  603.         widget["state"] = tk.DISABLED
  604.  
  605. def set_text_widget(widget, text):
  606.     if widget.get("1.0", tk.END)[:-1] != text:
  607.         widget.delete("1.0", tk.END)
  608.         widget.insert("1.0", text)
  609.  
  610. ##### Main #####
  611.  
  612. def on_closing():
  613.     save_config()
  614.     window.destroy()
  615.  
  616. def run_awemer(dl_type):
  617.     executor = AwemerWrapper() \
  618.         .with_ids(var_ids.get()) \
  619.         .with_hq(var_hq.get()) \
  620.         .with_add_date(var_add_date.get()) \
  621.         .with_verify(var_verify.get()) \
  622.         .with_convert(var_convert.get()) \
  623.         .with_delete(var_delete.get()) \
  624.         .with_rm_sd_suffix(var_rm_sd_suffix.get()) \
  625.         .with_sid(var_sid.get()) \
  626.         .with_uid(var_uid.get()) \
  627.         .with_usr_agent(var_usr_agent.get()) \
  628.         .with_random_usr_agent(var_rand_usr_agent.get()) \
  629.         .with_max_results(var_max_vids.get()) \
  630.         .with_max_filename_len(var_max_file_len.get()) \
  631.         .with_save_dir(var_save_dir.get()) \
  632.         .with_awemer(var_awemer.get())
  633.     threading.Thread(target=lambda: executor.execute(dl_type, lambda out: var_statusbar_text.set(out), AlertDialog)).start()
  634.  
  635. window = tk.Tk()
  636. window.title("Awemepy")
  637.  
  638. var_ids = tk.StringVar()
  639. var_max_vids = tk.StringVar()
  640. var_max_file_len = tk.StringVar()
  641. var_sid = tk.StringVar()
  642. var_uid = tk.StringVar()
  643. var_usr_agent = tk.StringVar()
  644. var_rand_usr_agent = tk.BooleanVar()
  645. var_save_dir = tk.StringVar()
  646. var_awemer = tk.StringVar()
  647. var_hq = tk.BooleanVar()
  648. var_add_date = tk.BooleanVar()
  649. var_verify = tk.BooleanVar()
  650. var_convert = tk.BooleanVar()
  651. var_delete = tk.BooleanVar()
  652. var_rm_sd_suffix = tk.BooleanVar()
  653. var_statusbar_text = tk.StringVar()
  654.  
  655. action_dl_vids = lambda: run_awemer(DownloadType.VIDEOS)
  656. action_dl_stories = lambda: run_awemer(DownloadType.STORIES)
  657. action_dl_imgs = lambda: run_awemer(DownloadType.IMAGES)
  658. action_dl_likes = lambda: run_awemer(DownloadType.LIKES)
  659.  
  660. container_top = MainInputAndDownloadBtns(window, var_ids, action_dl_vids, action_dl_stories, action_dl_imgs, action_dl_likes)
  661. container_middle = CheckboxesAndLimitInputs(window, var_hq, var_add_date, var_verify, var_convert, var_delete, var_rm_sd_suffix, var_max_vids, var_max_file_len)
  662. container_input_fields = ConfigInputs(window, var_sid, var_uid, var_save_dir, var_awemer, var_usr_agent, var_rand_usr_agent)
  663. # statusbar inside its own container so that it can have a fixed size and prevent window resizing when status is too long
  664. container_statusbar = tk.Frame(window)
  665. statusbar = tk.Label(container_statusbar, bd=1, relief=tk.SUNKEN, anchor=tk.E, bg="lightgray", textvariable=var_statusbar_text)
  666.  
  667. container_top.pack(side=tk.TOP, expand=True, fill=tk.BOTH)
  668. container_middle.pack(side=tk.TOP, expand=False, fill=tk.X)
  669. container_input_fields.pack(side=tk.TOP, expand=False, fill=tk.X, pady=(0, Consts.DEFAULT_PAD))
  670. container_statusbar.pack(side=tk.BOTTOM, fill=tk.X)
  671. statusbar.pack(side=tk.BOTTOM, fill=tk.X, ipadx=2, ipady=2)
  672.  
  673. window.update()
  674. h = window.winfo_height()
  675. w = window.winfo_width()
  676. window.minsize(width=w, height=h)
  677.  
  678. # prevent statusbar from resizing windown when text is long after the first update
  679. container_statusbar.pack_propagate(False)
  680.  
  681. if Consts.SAVE_CFG_ON_EXIT:
  682.     window.protocol("WM_DELETE_WINDOW", on_closing)
  683. load_config()
  684.  
  685. window.mainloop()
  686.  
Advertisement
Add Comment
Please, Sign In to add comment