import os, glob, logging, traceback, time, datetime, multiprocessing from PIL import Image #from waveshare_epd import epd7in5_V2 # Comment if "screen" is used as DISPLAY_DEVICE, uncomment if "epd" is used # LOGGING LOGGING_LEVEL = logging.INFO LOGGING_FILE = "logs.log" # Log in to file: "filepath" LOGGING_CONSOLE = False # Log in to console: True of False LOGGING_FORMAT = "%(asctime)s [%(levelname)s] %(message)s" # DISPLAY DISPLAY_DEVICE = "screen" # "screen" or "epd". If you chose "epd", don't forget to import epd7in5_V2 from waveshare_epd (line 3) DISPLAY_TXT_AUTHORIZED_EXTENSIONLIST = [".jpg", ".jpeg",".png", ".gif", ".tga", ".tif", ".bmp"] DISPLAY_TXT_TIME_FORMAT = "%d/%m/%Y %H:%M:%S" DISPLAY_INPUT_TXT_PATH = "./display_pictures.txt" DISPLAY_OUTPUT_TXT_PATH = "./display_EDITpictures.txt" DISPLAY_MINIMUM_PICS = 5 REBUILD_TIMER = 30 # CONFIG FILES CONFIG_PATH = "./config.txt" CONFIG_DELIMITER = "=" TIME_PARAMETER = "time" SORT_PARAMETER = "sort" CLEARNOW_PARAMETER = "clearNow" PIC_DIRECTORY_PARAMETER = "pic_directory" PIC_OUTPUT_DIRECTORY_PARAMETER = "pic_output_directory" LOGGING_TO_FILE_PARAMETER = "logging_to_file" # BLACKLIST/NOW FILES BLACKLIST_PATH = "./blacklist.txt" NOW_PATH = "./now.txt" NOW_EXTENSIONLIST_SEARCHER = [".jpg", ".jpeg",".png", ".gif", ".tga", ".tif", ".bmp"] # If extension is missed for a file in now.txt # PIC CONVERSION COLOR_CONVERSION_PATH = "./__white__.png" # white or black WANTED_SIZE_X = 800 WANTED_SIZE_Y = 480 PICTURE_FORMAT = ".bmp" # (.jpg, .jpeg, .png, .gif, .tga, .tif, .bmp) WATCHER_TIMER = 10 # OTHERS ENCODING = "utf-8" README_PATH = "../README.txt" README_CONTENT = ["written by moleite", "https://fr.fiverr.com/moleite", "________________________", "###### file parameters", "\n", "# LOGGING", "LOGGING_LEVEL : Logging level that will be displayed (logging.DEBUG, logging.INFO, logging.WARNING, logging.ERROR, logging.CRITICAL)", "LOGGING_FILE : Logging file path (use \"\" if you don't want to log into a file)", "LOGGING_CONSOLE : Will display logging into the console (True or False)", "LOGGING_FORMAT : Format used for logging", "\n", "# DISPLAY", "DISPLAY_DEVICE : name of the used display: \"screen\" or \"epd\". (\"screen\" will work for Windows/Ubuntu, \"epd\" for e-paper display using Raspberry Pi). If you chose \"epd\", don't forget to import epd7in5_V2 from waveshare_epd (line 3)", "DISPLAY_TXT_AUTHORIZED_EXTENSIONLIST : list of extension authorized into the display.txt files. If a file without these extensions is found, it will not get added into the display.txt file", "DISPLAY_TXT_TIME_FORMAT : time format used into the display.txt files", "DISPLAY_INPUT_TXT_PATH : path for display_input.txt file", "DISPLAY_OUTPUT_TXT_PATH = path for display_output.txt file", "DISPLAY_MINIMUM_PICS : Minimum files that should contain 'display_EDITpictures.txt'. If it contains less, Display Thread will not display anything (see Workflow.docx)", "REBUILD_TIMER : Time, in seconds, before every rebuild from Rebuild Thread (see Workflow.docx)", "\n", "# CONFIG FILES", "CONFIG_PATH : config.txt file path", "CONFIG_DELIMITER : config file delimiter between parameter and value", "TIME_PARAMETER : name of time parameter", "SORT_PARAMETER : name of sort parameter", "CLEARNOW_PARAMETER : name of clearNow parameter", "PIC_DIRECTORY_PARAMETER : name of pic_directory parameter", "PIC_OUTPUT_DIRECTORY_PARAMETER : name of pic_output_directory parameter", "LOGGING_TO_FILE_PARAMETER : name of logging_to_file parameter", "\n", "# BLACKLIST/NOW FILES", "BLACKLIST_PATH : blacklist.txt file path", "NOW_PATH : now.txt file path", "NOW_EXTENSIONLIST_SEARCHER : list of extensions that now.txt will search for if you don't write a file with his extension.", "\n", "# PIC CONVERSION", "COLOR_CONVERSION_PATH : output color conversion file path (\"./__white__.png\" or \"./__black__.png\")", "WANTED_SIZE_X : output x size after conversion", "WANTED_SIZE_Y : output y size after conversion", "PICTURE_FORMAT : output format after conversion (authorized formats: .jpg, .jpeg, .png, .gif, .tga, .tif, .bmp)", "WATCHER_TIMER : Time, in seconds, before Watcher Thread will search for changes into 'pictures/' (see Workflow.docx)", "\n", "#OTHERS", "ENCODING : encoding format for input/output files (displays.txt, logs.log, config.txt etc...)", "README_PATH : README.txt file path", "README_CONTENT : actual file content in an array", "\n", "###### file modification while script is running", "\n", "- adding/deleting files to pictures/ directory", "While the script is running, you can add to-convert files in pictures/", "The script will search for changes, convert them into EDITpictures/, then update display.txt files before displaying them", "\n", "- You can modify now.txt, blacklist.txt and config.txt while the script is running", "Changes are checked everytime", "(except for config.txt[pic_directory] and config.txt[pic_output_directory])", "\n", "\n", "###### blacklist.txt", "\n", "#- Extension specification:", "You can write files with or without extensions.", "The script will automatically blacklist any existing extension on the filename or folder path.", "\n", "Example:", "__black__", "__white__.png", "2/", "3/2", "sample_jpg.bmp", "\n", "\"__black__.bmp\" file will get blacklisted", "\"__white__.bmp\" file will NOT get blacklisted (but if you add a \"__white__.png\" file into EDITpictures, it will get blacklisted)", "All files from \"2/\" folder will get blacklisted: \"2/1.bmp\" \"2/2.bmp\" \"2/3.bmp\"", "\"3/2.bmp\" file will get blacklisted", "\"sample_jpg.bmp\" file will get blacklisted", "\n", "Note:", "blacklist always blocks \"now.txt\" blacklisted files", "\n", "\n", "###### now.txt", "\n", "#- Extension specification & restriction:", "You can write files with or without extensions.", "The script will automatically fill the extension (from display.py[NOW_EXTENSIONLIST_SEACHER] parameter) if the file exists into the EDITpictudes folder.", "\n", "Example:", "sample_bmp", "sample_jpg.bmp", "2/1", "3/2", "1/", "\n", "\n", "For this example, now will bring these four first files, and all files in the folder \"1/\":", "sample_bmp.bmp (after finding the extension alone)", "sample_jpg.bmp", "2/1.bmp (after finding the extension alone)", "3/2.bmp", "1/1.bmp (after finding the extension alone)", "\n", "Note:", "in this example, \"1/\" folder contains only one file: \"1.bmp\"", "If you write your file with an extension into now.txt, and if it exists, the now.txt will display it.", "If the file exists but you didn't mention its extension, and if its extension isn't in the display.py[NOW_EXTENSIONLIST_SEACHER] parameter, the file will not be displayed", "\n", "\n", "###### clearNow", "\n", "- clearNow checking", "clearNow will be checked before every display. You can change this parameter: config.txt[time]", "\n", "- clearNow during NOW:", "If you set clearNow to 1 DURING NOW display, it will wait for NOW completion."] ### GENERAL FUNCTIONS ### # Time parameter converter (days:hours:minuts:seconds => seconds) def convert_sec(timer): d, h, m, s = timer.split(':') return int(d) * 86400 + int(h) * 3600 + int(m) * 60 + int(s) # Create a folder. Return true if the creation works, or if folder already exists. Return false if impossible def create_folder(folder_path, log_message = ""): try: if not os.path.isdir(folder_path): logging.info(log_message + " Creating directory: " + folder_path) os.makedirs(folder_path) return True except OSError as error: if error.args[0] == 17: logging.info(log_message + " Directory '" + folder_path + "' already exists.") return True else: print(log_message + 'ERROR: Creating directory. ' + folder_path) #DEBUG logging.info(log_message + 'ERROR: Creating directory. ' + folder_path + " @ " + traceback.format_exc()) return False # Read a file, return array of file_lines def read_file(file_path, log_message = "", encoding = ENCODING): file_lines = [] try: file = open(file_path, 'rb') # Looking for Lines for line in file: line = line.decode(encoding).strip() if not line: continue #ignore empty row lines file_lines.append(line) file.close() except OSError: print(log_message + "ERROR: File not found: ", file_path) #DEBUG logging.info(log_message + "ERROR: File not found: " + file_path + " @ " + traceback.format_exc()) return file_lines # Reseting file content def reset_file(file_path): open(file_path, 'w').close() # Delete a file def delete_file(file_path, log_message = ""): try: if os.path.isfile(file_path): os.remove(file_path) else: logging.info("ERROR: '" + file_path + "' not found. Can't delete the file") except OSError: print(log_message + "ERROR [1]: An error occured, please check your files input: " + file_path + " @ " + traceback.format_exc()) #DEBUG logging.info(log_message + "ERROR [1]: An error occured, please check your files input: " + file_path + " @ " + traceback.format_exc()) # Returning [sub_directories, filename, extension] from a file or directory path "subdirectories/filename.extension" or "filename.extension" or "subdirectories/" def parse_file_directory_path(file_directory_path): parsed_filepath = [] # Replacing "\" by "/" file_directory_path = file_directory_path.replace("\\", "/") # Getting subfolders temp_filepath = (file_directory_path).rsplit("/", 1) if len(temp_filepath) > 1: file_subfolders = temp_filepath[0] + "/" temp_picfile = temp_filepath[1].rsplit(".", 1) else: file_subfolders = "" temp_picfile = temp_filepath[0].rsplit(".", 1) # Splitting file by name and extension if len(temp_picfile) > 1: file_filename = temp_picfile[0] file_extension = "." + temp_picfile[1] else: file_filename = "" file_extension = "" parsed_filepath.extend((file_subfolders, file_filename, file_extension)) return parsed_filepath # Get files from directory and sub-directories in a list def get_files_subfiles(folder_path): filelist = [] for root, dirs, files in os.walk(folder_path): for file in files: filepath=os.path.join(root,file) filepath_without_folderpath = filepath.split(folder_path, 1)[1].replace("\\", "/") filelist.append(filepath_without_folderpath) return filelist # Get files from directory and sub-directories in a dictionary def get_files_subfiles_indict(folder_path): filedict = {} for root, dirs, files in os.walk(folder_path): for file in files: filepath=os.path.join(root,file) filepath_without_folderpath = filepath.split(folder_path, 1)[1].replace("\\", "/") filedict[filepath_without_folderpath] = None return filedict # Funnel list of filename.extension by extension list def funnel_files_extensions(filelist, extensionlist): filelist_funneled = [] for file in filelist: for extension in extensionlist: if extension.casefold() in file.casefold(): filelist_funneled.append(file) break return filelist_funneled # Sort doublearray by time [[x1, time], [x2, time]] def sort_doublearray_bytime(doublearray, reverse = False, display_time = DISPLAY_TXT_TIME_FORMAT): doublearray_sorted = sorted(doublearray, reverse=reverse, key=lambda x:time.strptime(x[1],display_time)) return doublearray_sorted # Non-caught errors handler def catch_new_error(log_message): print(log_message + "ERROR TO CATCH. @ " + traceback.format_exc()) logging.info(log_message + "ERROR TO CATCH. @ " + traceback.format_exc()) ### CONFIG FUNCTIONS ### # Config files initializer def init_config_files(log_message = ""): try: # Creating config files if they don't exist config_file = open(CONFIG_PATH,'a+') blacklist_file = open(BLACKLIST_PATH,'a+') now_file = open(NOW_PATH,'a+') config_file.close() blacklist_file.close() now_file.close() configlist = read_configs(first_read_config = True, log_message = log_message) time_c = configlist[TIME_PARAMETER] sort_c = configlist[SORT_PARAMETER] clearnow_c = configlist[CLEARNOW_PARAMETER] pic_directory_c = configlist[PIC_DIRECTORY_PARAMETER] pic_output_directory_c = configlist[PIC_OUTPUT_DIRECTORY_PARAMETER] logging_to_file_c = configlist[LOGGING_TO_FILE_PARAMETER] except KeyError: print(log_message + "ERROR: Please check that you have all of these parameters in your config.txt file:", TIME_PARAMETER, SORT_PARAMETER, CLEARNOW_PARAMETER, PIC_DIRECTORY_PARAMETER, PIC_OUTPUT_DIRECTORY_PARAMETER, LOGGING_TO_FILE_PARAMETER) logging.info(log_message + "ERROR: Please check that you have all of these parameters in your config.txt file: " + TIME_PARAMETER + " " + SORT_PARAMETER + " " + CLEARNOW_PARAMETER + " " + PIC_DIRECTORY_PARAMETER + " " + PIC_OUTPUT_DIRECTORY_PARAMETER + " " + LOGGING_TO_FILE_PARAMETER) exit() return configlist # README file initializer def init_readme_file(): if not os.path.isfile(README_PATH): logging.info("Creating README file: " + README_PATH) readme_file = open(README_PATH, "w") for readme_line in README_CONTENT: if readme_line != "\n": readme_file.write(readme_line + "\n") else: readme_file.write("\n") readme_file.close() # Config.txt reader def read_configs(first_read_config = False, config_path = CONFIG_PATH, delimiter = CONFIG_DELIMITER, log_message = ""): config_lines = read_file(config_path, log_message) configs = {} # Split + Strips the newline after "1" maxsplit for config in config_lines: temp = config.split(delimiter, 1) configs[temp[0].strip()] = temp[1].strip() if not first_read_config: logging.info(log_message + " CONFIGS: " + str(configs)) return configs # Return {"blacklist_files": [], "blacklist_folders": []} def get_blacklist(blacklist_path, log_message = ""): blacklist_lines = read_file(blacklist_path, log_message) blacklist = {"blacklist_files": [], "blacklist_folders": []} # iterating through blacklist lines for line in blacklist_lines: # if we found a directory if "/" in line: blacklist["blacklist_folders"].append(line) # if we found a filename else: blacklist["blacklist_files"].append(line) return blacklist # Return if file or directory input is blacklisted. def is_blacklisted(blacklist, file_or_directory_path, log_message = ""): logging.info(log_message + "[BLACKLIST] ________________ Checking blacklist.txt ____________________") parsed_file_or_directory = parse_file_directory_path(file_or_directory_path) file_subdirs = parsed_file_or_directory[0] file_name = parsed_file_or_directory[1] file_name_extension = parsed_file_or_directory[1] + parsed_file_or_directory[2] # Checking if dir/subdirs or dir/subdirs/file are blacklisted if file_subdirs != "": for bl_directory in blacklist["blacklist_folders"]: if file_subdirs == bl_directory: logging.info(log_message + "[BLACKLIST] Blacklisted: " + file_subdirs + file_name_extension) return True # Checking if filename or filename + extension are blacklisted if file_name != "": for bl_filename in blacklist["blacklist_files"]: if file_name == bl_filename: logging.info(log_message + "[BLACKLIST] Blacklisted: " + file_name_extension) return True if file_name_extension == bl_filename: logging.info(log_message + "[BLACKLIST] Blacklisted: " + file_name_extension) return True return False # Return list of files from now_path def get_now(pic_output_directory, now_path = NOW_PATH, extensionlist = NOW_EXTENSIONLIST_SEARCHER, log_message = ""): logging.info(log_message + "[NOW] ________________ Checking now.txt __________________________") now_lines = read_file(now_path, log_message) now = [] # iterating through now lines for line in now_lines: # if we found a directory if line.endswith("/"): complete_dir_path = pic_output_directory + line now_folder_files = get_files_subfiles(complete_dir_path) logging.info(log_message + "[NOW] Directory found: " + complete_dir_path) for file in now_folder_files: logging.info(log_message + "[NOW] Dir/File found: " + complete_dir_path + file) now.append(complete_dir_path + file) # if we found a filename (without extension) else: found = False for extension in extensionlist: complete_file_path = pic_output_directory + line + extension if os.path.isfile(complete_file_path): logging.info(log_message + "[NOW] File found: " + complete_file_path) now.append(complete_file_path) found = True if not found: logging.info(log_message + "[NOW] File not found: " + pic_output_directory + line) return now ### DISPAY FUNCTIONS ### # Display.txt maker. Use same display_path and original_folder if get_original_time = False def make_display(display_path, get_original_time = True, original_folder = "", write_file = True, display_txt_path = DISPLAY_OUTPUT_TXT_PATH, display_time = DISPLAY_TXT_TIME_FORMAT, extensionlist = DISPLAY_TXT_AUTHORIZED_EXTENSIONLIST, sort = None, encoding = ENCODING, log_message = ""): try: logging.info(log_message + " ________________ Creating display: " + display_path + " ________") displaylist = {"name_time": [], "extension": []} deleted_files_during_make_display = False # checking if we have to sort files by extensions if len(extensionlist) == 0: filelist = get_files_subfiles(display_path) else: filelist = funnel_files_extensions(get_files_subfiles(display_path), extensionlist) # creating displaylist with files paths and timestamps (option: get date from original folders) for x in range(len(filelist)): file_path = filelist[x] file_without_extension = os.path.splitext(file_path)[0] file_extension = "." + file_path.rsplit(".", 1)[1] if get_original_time: try: guessed_file = glob.glob(original_folder + file_without_extension + ".*")[0] last_modified = datetime.datetime.fromtimestamp(os.path.getmtime(guessed_file)).strftime(display_time) except IndexError : logging.info(log_message + "WARNING: '" + file_path + "' cannot be found in '" + original_folder + ". File will be deleted from " + display_path) delete_file(display_path + file_path, log_message) deleted_files_during_make_display = True continue else: last_modified = datetime.datetime.fromtimestamp(os.path.getmtime(display_path + file_path)).strftime(display_time) displaylist["name_time"].append([file_without_extension, last_modified]) displaylist["extension"].append([file_extension, last_modified]) # checking if files were deleted from display_path during the make_display. If yes: restart the whole make_display with the same parameters that used [MAIN] for both folders if deleted_files_during_make_display: logging.info(log_message + "Files were deleted during the display maker. Restarting both makers..") return make_display(display_path, original_folder = original_folder, sort = sort, display_txt_path = display_txt_path, log_message = "[MAIN]") # checking if we have to sort files by time if sort == "0": displaylist["name_time"] = sort_doublearray_bytime(displaylist["name_time"], reverse=True) displaylist["extension"] = sort_doublearray_bytime(displaylist["extension"], reverse=True) elif sort == "1": displaylist["name_time"] = sort_doublearray_bytime(displaylist["name_time"]) displaylist["extension"] = sort_doublearray_bytime(displaylist["extension"]) # writing display.txt file if write_file: logging.info(log_message + " ________________ Creating txt file: " + display_txt_path + " ________") display_file = open(display_txt_path, "wb") for x in range(len(displaylist["name_time"])): x_name = displaylist["name_time"][x][0] x_time = displaylist["name_time"][x][1] x_extension = displaylist["extension"][x][0] line_towrite = x_name + x_extension + " | " + x_time display_file.write((line_towrite + "\n").encode(encoding)) logging.info(log_message + " ADDING DISPLAY: " + line_towrite) display_file.close() # Raise error message if display_path is empty if len(displaylist["name_time"]) == 0: print(log_message + "Nothing to create display! Path is empty:", display_path) #DEBUG logging.info(log_message + "Nothing to create display! Path is empty: " + display_path) logging.info(log_message + " ________________ Display creation completed ____________________\n") return displaylist except: catch_new_error(log_message) # Returning content of display.txt def get_display(display_path = DISPLAY_INPUT_TXT_PATH, log_message = ""): display_lines = read_file(display_path, log_message) displaylist = {"name_time": [], "extension": []} # iterating through display lines for line in display_lines: temp_display = line.split(" | ") temp_file = temp_display[0].split(".") file_name = temp_file[0] file_extension = "." + temp_file[1] file_time = temp_display[1] displaylist["name_time"].append([file_name, file_time]) displaylist["extension"].append([file_extension, file_time]) return displaylist # Display a picture onscreen, with a timer, on different devices def open_display(pic_path, pic_directory_for_blacklist, log_message = "", display_device = DISPLAY_DEVICE, blacklist_path = BLACKLIST_PATH): # Don't display if the file or parent_folders are blacklisted blacklist = get_blacklist(blacklist_path, log_message) pic_path_without_parent = pic_path.split(pic_directory_for_blacklist, 1)[1] if is_blacklisted(blacklist, pic_path_without_parent, log_message): return ["blacklisted", None] try: Himage = Image.open(pic_path) logging.info(log_message + "--------------- displaying: " + pic_path) if display_device == "epd": epd = epd7in5_V2.EPD() epd.init() epd.Clear() epd.display(epd.getbuffer(Himage)) return [display_device, epd] elif display_device == "screen": Himage.show() #Take some times to execute return [display_device, None] else: print(log_message, display_device, "isn't a valid device, please restat with 'screen' or 'epd'.") #DEBUG logging.info(log_message + display_device + "isn't a valid device, please restat with 'screen' or 'epd'.") exit() except IOError: return ["ERR_FILE_NOT_FOUND", None] except KeyboardInterrupt: logging.info("[open_display()] ctrl + c:") if display_device == "epd": epd.Clear() epd7in5_V2.epdconfig.module_exit() exit() ### CONVERSION FUNCTIONS ### # Resize and/or convert a picture in (jpg, jpeg, png, gif, tga, tif, bpm) formats, with a wanted size, and a wanted color. It will always overwrite an existing file existing with the same name in output directory def resize_convert_pic(pic_path, pic_directory, pic_output_directory, color_conversion_path = COLOR_CONVERSION_PATH, wanted_size_x = WANTED_SIZE_X, wanted_size_y = WANTED_SIZE_Y, picture_format = PICTURE_FORMAT, log_message = ""): try: pic_parsed = parse_file_directory_path(pic_path) create_folder(pic_output_directory + pic_parsed[0], log_message) logging.info(log_message + " RESIZE/CONVERT: " + pic_output_directory + pic_path) bkg = Image.open(color_conversion_path) img = Image.open(pic_directory + pic_path) width, height = img.size diffrenceImg = height/width diffrenceR = wanted_size_y/wanted_size_x if diffrenceImg < diffrenceR: k = width/wanted_size_x img = img.resize((int(wanted_size_x),int(height/k))) elif diffrenceImg > diffrenceR: k = height/wanted_size_y img = img.resize((int(width/k),int(wanted_size_y))) elif diffrenceImg == diffrenceR: k = width/wanted_size_x img = img.resize((int(wanted_size_x),int(height/k))) img = img.convert("RGBA") bkg = bkg.convert("RGBA") width, height = img.size bkg.paste(img,(int((wanted_size_x-width)/2),int((wanted_size_y-height)/2))) bkg = bkg.convert("L") final_pic = pic_output_directory + pic_parsed[0] + pic_parsed[1] + picture_format bkg.save(final_pic) return final_pic except (FileNotFoundError, PermissionError): logging.info(log_message + "WARNING: File not found: " + pic_directory + pic_path) return "ERR_FILE_NOT_FOUND" # Convert files that are on display1 but not display2, and delete files that are on display2 but not display1 def resize_convert_display_differences(display_input, display_output, pic_directory, pic_output_directory, log_message = ""): logging.info(log_message + " ________________ Start resizing display differences ____________________") differences = {"added": [], "removed": []} # Convert added files for x in range(len(display_input["name_time"])): name_time = display_input["name_time"][x] extension = display_input["extension"][x][0] if name_time not in display_output["name_time"]: done_resize_convert_pic = resize_convert_pic(name_time[0] + extension, pic_directory, pic_output_directory, log_message = log_message) if done_resize_convert_pic != "ERR_FILE_NOT_FOUND": differences["added"].append(name_time) # Delete removed files for x in range(len(display_output["name_time"])): name_time = display_output["name_time"][x] extension = display_output["extension"][x][0] if name_time not in display_input["name_time"]: differences["removed"].append(name_time) logging.info(log_message + " DELETING: " + pic_output_directory + name_time[0] + extension) delete_file(pic_output_directory + name_time[0] + extension, log_message) logging.info(log_message + " ________________ Resizing display differences completed ____________________\n") return differences ### THREADS ### # Logging initializer for threads def init_logging(logging_to_file, logging_level = LOGGING_LEVEL, logging_format = LOGGING_FORMAT, logging_file = LOGGING_FILE, logging_console = LOGGING_CONSOLE, logging_encoding = ENCODING): logging_handlers = [] if logging_to_file == "1": try: logging_handlers.append(logging.FileHandler(logging_file, 'a', logging_encoding)) except: print("Logging file can't be reached:", logging_file, "Please set config.txt[logging_to_file] to '0' or change logging file path.") #DEBUG exit() if logging_console: logging_handlers.append(logging.StreamHandler()) logging.basicConfig( level = logging_level, format = logging_format, handlers = logging_handlers ) if logging_handlers == []: logger = logging.getLogger() logger.disabled = True # Display Thread class DisplayProc(multiprocessing.Process): def __init__(self, logging_to_file_c): multiprocessing.Process.__init__(self) self.logging_to_file_c = logging_to_file_c def run(self): init_logging(self.logging_to_file_c) logging.info("[DISPLAY] Starting display process...") #DEBUG display_head = 0 while True: try: # Get configs before every display configlist = read_configs(log_message = "[DISPLAY] ") timer_c = convert_sec(configlist[TIME_PARAMETER]) clearnow_c = configlist[CLEARNOW_PARAMETER] pic_output_directory_c = configlist[PIC_OUTPUT_DIRECTORY_PARAMETER] display_result = ["", None] # Initialize open_display output # If clearnow = 0, start display if clearnow_c == "0": # Displaying now.txt files now_files = get_now(pic_output_directory_c, log_message = "[DISPLAY] ") reset_file(NOW_PATH) nb_now_files = len(now_files) for x in range(nb_now_files): log_message = "[DISPLAY] [NOW] " + str(x+1) + "/" + str(nb_now_files) + " " display_result = open_display(now_files[x], pic_output_directory_c, log_message) if display_result[0] != "blacklisted": time.sleep(timer_c) display_result = ["", None] # In case the last now is blacklisted, reset display_result to wait timer if there is no pics in display.txt # Read the display.txt file displayEDIT = get_display(DISPLAY_OUTPUT_TXT_PATH, "[DISPLAY]") nb_pics = len(displayEDIT["name_time"]) # Restart display if head is over the number of pics if display_head >= nb_pics: logging.info("[DISPLAY] End of file, resetting head reader\n") display_head = 0 # Start display only if minimum pics in the display.txt is reached if nb_pics >= DISPLAY_MINIMUM_PICS: pic_path = pic_output_directory_c + displayEDIT["name_time"][display_head][0] + displayEDIT["extension"][display_head][0] log_message = "[DISPLAY] " + str(display_head+1) + "/" + str(nb_pics) + " " display_result = open_display(pic_path, pic_output_directory_c, log_message) # If file doesn't exist, show error and continue. if display_result[0] == "ERR_FILE_NOT_FOUND": logging.info("[DISPLAY] file not found: " + pic_path) display_head += 1 else: logging.info("[DISPLAY] Need more than " + str(DISPLAY_MINIMUM_PICS) + " files. Actual number of files: " + str(nb_pics)) # If clearnow = 1: Resetting display_head and stop the display elif clearnow_c == "1": logging.info("Clearnow is set to '1', waiting: " + str(timer_c) + " seconds...") display_head = 0 # If file is blacklisted, go on next file without sleep. if display_result[0] == "blacklisted": continue time.sleep(timer_c) except KeyboardInterrupt: logging.info("[DISPLAY] Closing display process...") break # Watcher_pictures Thread class WatcherProc(multiprocessing.Process): def __init__(self, path_to_watch, pic_directory_c, pic_output_directory_c, sort_c, logging_to_file_c, timer = WATCHER_TIMER, display_input_txt_path = DISPLAY_INPUT_TXT_PATH, display_output_txt_path = DISPLAY_OUTPUT_TXT_PATH): multiprocessing.Process.__init__(self) self.path_to_watch = path_to_watch self.pic_directory_c = pic_directory_c self.pic_output_directory_c = pic_output_directory_c self.sort_c = sort_c self.logging_to_file_c = logging_to_file_c self.timer = timer self.display_input_txt_path = display_input_txt_path self.display_output_txt_path = display_output_txt_path def run(self): init_logging(self.logging_to_file_c) logging.info("[TRIGGER] Starting Trigger_pictures process...") #DEBUG try: before = get_files_subfiles_indict(self.path_to_watch) while True: after = get_files_subfiles_indict(self.path_to_watch) added = [f for f in after if not f in before] removed = [f for f in before if not f in after] # If we find changes: if added or removed: # Make display_picture.txtTEMP logging.info("[TRIGGER] Triggered: Added files: " + ", ".join(added) + " - Removed files: " + ", ".join(removed)) temp_input_txt_path = self.display_input_txt_path + "TEMP" temp_display_input = make_display(self.pic_directory_c, False, sort = self.sort_c, display_txt_path = temp_input_txt_path, log_message = "[TRIGGER]") # Get display_EDITpictures.txt display_output = get_display(self.display_output_txt_path, "[TRIGGER]") # Starting rebuild display Thread rebuild_thread = RebuildProc(self.pic_directory_c, self.pic_output_directory_c, self.sort_c, self.logging_to_file_c, log_message = "[TRIGGER]") rebuild_thread.start() # Resize and convert differences resize_convert_display_differences(temp_display_input, display_output, self.pic_directory_c, self.pic_output_directory_c, log_message = "[TRIGGER]") # Closing rebuild display Thread before writing the final display_EDITpictures.txt logging.info("[TRIGGER][REBUILD] Rebuild_display process closed.") rebuild_thread.terminate() # Make display_EDITpictures.txtTEMP temp_output_txt_path = self.display_output_txt_path + "TEMP" make_display(self.pic_output_directory_c, original_folder = self.pic_directory_c, sort = self.sort_c, display_txt_path = temp_output_txt_path, log_message = "[TRIGGER]") # Replacing display.txt files with TEMP files logging.info("[TRIGGER] Replacing display.txt files by display.txtTEMP files") os.replace(temp_input_txt_path, self.display_input_txt_path) os.replace(temp_output_txt_path, self.display_output_txt_path) before = after time.sleep(self.timer) except KeyboardInterrupt: logging.info("[TRIGGER] Closing Trigger_pictures process...") except (OSError, PermissionError, IsADirectoryError, NotADirectoryError): print("[TRIGGER] ERROR[2]: An error occured, please check your files input. @ " + traceback.format_exc()) #DEBUG logging.info("[TRIGGER] ERROR[2]: An error occured, please check your files input. @ " + traceback.format_exc()) # Rebuild_display Thread class RebuildProc(multiprocessing.Process): def __init__(self, pic_directory_c, pic_output_directory_c, sort_c, logging_to_file_c, display_input_txt_path = DISPLAY_INPUT_TXT_PATH ,display_output_txt_path = DISPLAY_OUTPUT_TXT_PATH, timer = REBUILD_TIMER, log_message = ""): multiprocessing.Process.__init__(self) self.pic_directory_c = pic_directory_c self.pic_output_directory_c = pic_output_directory_c self.sort_c = sort_c self.logging_to_file_c = logging_to_file_c self.display_input_txt_path = display_input_txt_path self.display_output_txt_path = display_output_txt_path self.timer = timer self.log_message = log_message def run(self): init_logging(self.logging_to_file_c) logging.info(self.log_message + "[REBUILD] Starting rebuild_display process...") #DEBUG while True: try: time.sleep(self.timer) # Make display_EDITpictures.txtTEMP_REBUILD and display_pictures.txtTEMP_REBUILD temp_input_txt_path = self.display_input_txt_path + "TEMPR" make_display(self.pic_directory_c, False, sort = self.sort_c, display_txt_path = temp_input_txt_path, log_message = self.log_message + "[REBUILD]") temp_output_txt_path = self.display_output_txt_path + "TEMPR" make_display(self.pic_output_directory_c, original_folder = self.pic_directory_c, sort = self.sort_c, display_txt_path = temp_output_txt_path, log_message = self.log_message + "[REBUILD]") # Replacing display_EDITpictures.txt and display_pictures.txt with TEMP files logging.info(self.log_message + "[REBUILD] Replacing '" + self.display_output_txt_path + "' file by txtTEMP file.") os.replace(temp_output_txt_path, self.display_output_txt_path) logging.info(self.log_message + "[REBUILD] Replacing '" + self.display_input_txt_path + "' file by txtTEMP file.") os.replace(temp_input_txt_path, self.display_input_txt_path) except KeyboardInterrupt: logging.info(self.log_message + "[REBUILD] Closing rebuild_display process...") break except (OSError, PermissionError, IsADirectoryError, NotADirectoryError): print(self.log_message + "[REBUILD] ERROR[3]: An error occured, please check your files input. @ " + traceback.format_exc()) #DEBUG logging.info(self.log_message + "[REBUILD] ERROR[3]: An error occured, please check your files input. @ " + traceback.format_exc()) # Main program def main(): try: # Initialize the program with config files, then getting config.txt parameters for the first time configlist = init_config_files(log_message = "[MAIN]") sort_c = configlist[SORT_PARAMETER] pic_directory_c = configlist[PIC_DIRECTORY_PARAMETER] pic_output_directory_c = configlist[PIC_OUTPUT_DIRECTORY_PARAMETER] logging_to_file_c = configlist[LOGGING_TO_FILE_PARAMETER] init_logging(logging_to_file_c) logging.info("\n\n################################################################################################################################\n") init_readme_file() # Initial display_EDITpictures.txt maker output_display = make_display(pic_output_directory_c, True, pic_directory_c, sort = sort_c, log_message = "[MAIN]") # Display Thread (multiprocessing.Process) display_thread = DisplayProc(logging_to_file_c) display_thread.start() # Initial display_pictures.txt maker input_display = make_display(pic_directory_c, False, display_txt_path = DISPLAY_INPUT_TXT_PATH, sort = sort_c, log_message = "[MAIN]") # Initial temporary Rebuild Thread to help display Process using in real time converted files if there is differences to convert at the first launch rebuild_thread = RebuildProc(pic_directory_c, pic_output_directory_c, sort_c, logging_to_file_c, log_message = "[MAIN]") rebuild_thread.start() # Initial display differences conversion, and display_EDITpictures.txt creation if differences have been found differences = resize_convert_display_differences(input_display, output_display, pic_directory_c, pic_output_directory_c, log_message = "[MAIN]") logging.info("[MAIN][REBUILD] Rebuild_display process closed.") rebuild_thread.terminate() # Ending rebuild_thread before making the last output display if differences["added"] or differences["removed"]: temp_output_txt_path = DISPLAY_OUTPUT_TXT_PATH + "TEMP" make_display(pic_output_directory_c, original_folder = pic_directory_c, sort = sort_c, display_txt_path = temp_output_txt_path, log_message = "[MAIN]") logging.info("[MAIN] Replacing '" + pic_output_directory_c + "' file by txtTEMP file.") os.replace(temp_output_txt_path, DISPLAY_OUTPUT_TXT_PATH) #Remaking display input (in case that files were deleted from the input folder during the the output display maker.) temp_input_txt_path = DISPLAY_INPUT_TXT_PATH + "TEMP" make_display(pic_directory_c, False, display_txt_path = temp_input_txt_path, sort = sort_c, log_message = "[MAIN]") logging.info("[MAIN] Replacing '" + pic_directory_c + "' file by txtTEMP file.") os.replace(temp_input_txt_path, DISPLAY_INPUT_TXT_PATH) # Trigger_pictures Thread (multiprocessing.Process) watcher_thread = WatcherProc(pic_directory_c, pic_directory_c, pic_output_directory_c, sort_c, logging_to_file_c) watcher_thread.start() # Closing threads display_thread.join() watcher_thread.join() except KeyboardInterrupt: logging.info("[MAIN] Closing program... (Ctrl + C):") pass except (OSError, PermissionError, IsADirectoryError, NotADirectoryError): print("[MAIN] ERROR[4]: An error occured, please check your files input. @ " + traceback.format_exc()) #DEBUG logging.info("[MAIN] ERROR[4]: An error occured, please check your files input. @ " + traceback.format_exc()) if __name__ == '__main__': main() #Launch program