diff --git a/.idea/dbnavigator.xml b/.idea/dbnavigator.xml new file mode 100644 index 00000000..46c11abe --- /dev/null +++ b/.idea/dbnavigator.xml @@ -0,0 +1,454 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Python3_Projects/Organize_Media/Organize_Media.py b/Python3_Projects/Organize_Media/Organize_Media.py index b9dc4533..3ca47318 100644 --- a/Python3_Projects/Organize_Media/Organize_Media.py +++ b/Python3_Projects/Organize_Media/Organize_Media.py @@ -3,71 +3,18 @@ import os import json from tkinter import * -from tkinter.ttk import Progressbar, Style, Separator +from tkinter.ttk import Progressbar, Style from PIL import Image, ImageTk -from imdb import IMDb from copy import deepcopy import encodings.idna +import sys from settings.general_functions import tv_show_ep_from_file_name, tv_show_ep_from_folder_structure, \ initcap_file_name, rename_all_media_in_directory, get_media_title from settings.mo_functions import get_downloads_or_media_path, save_paths_to_json - - -class CreateToolTip(object): - """ - create a tooltip for a given widget - """ - def __init__(self, widget, text='widget info', bg='#ffffff', fg='#000000'): - self.waittime = 500 #miliseconds - self.wraplength = 180 #pixels - self.widget = widget - self.text = text - self.bg = bg - self.fg = fg - self.widget.bind("", self.enter) - self.widget.bind("", self.leave) - self.widget.bind("", self.leave) - self.id = None - self.tw = None - - def enter(self, event=None): - self.schedule() - - def leave(self, event=None): - self.unschedule() - self.hidetip() - - def schedule(self): - self.unschedule() - self.id = self.widget.after(self.waittime, self.showtip) - - def unschedule(self): - id = self.id - self.id = None - if id: - self.widget.after_cancel(id) - - def showtip(self, event=None): - x = y = 0 - x, y, cx, cy = self.widget.bbox("insert") - x += self.widget.winfo_rootx() + 25 - y += self.widget.winfo_rooty() + 20 - # creates a toplevel window - self.tw = Toplevel(self.widget) - # Leaves only the label and removes the app window - self.tw.wm_overrideredirect(True) - self.tw.wm_geometry("+%d+%d" % (x, y)) - label = Label(self.tw, text=self.text, justify='left', - background=self.bg, foreground=self.fg, relief='solid', borderwidth=1, - wraplength=self.wraplength) - label.pack(ipadx=1) - - def hidetip(self): - tw = self.tw - self.tw = None - if tw: - tw.destroy() +from components.checklist import CheckList +from components.tooltip import CreateToolTip +from components.menu_bar import MenuBar class Organize(Tk): @@ -75,7 +22,7 @@ class Organize(Tk): def __init__(self): Tk.__init__(self) self.title('Media Organizer') - self.iconbitmap('Images/organize_media.ico') + # self.iconbitmap('Images/organize_media.ico') self.colors = { 'main': '#%02x%02x%02x' % (20, 20, 20), @@ -88,13 +35,13 @@ def __init__(self): font_default = 'Constantia' self.fonts = { - 'xxsmall': (font_default, 11), - 'xsmall': (font_default, 12), - 'small': (font_default, 13), - 'medium': (font_default, 14), - 'large': (font_default, 18), - 'xlarge': (font_default, 22), - 'xxlarge': (font_default, 40), + 'xxsmall': (font_default, 11, 'normal'), + 'xsmall': (font_default, 12, 'normal'), + 'small': (font_default, 13, 'normal'), + 'medium': (font_default, 14, 'normal'), + 'large': (font_default, 18, 'normal'), + 'xlarge': (font_default, 22, 'normal'), + 'xxlarge': (font_default, 40, 'normal'), } self.config(bg=self.colors['main']) @@ -109,7 +56,6 @@ def __init__(self): self.configure(bg=self.colors['main']) ''' Filter media will contain a dictionary like: {path: {file_name: name, title: title, kind: kind, ...}}''' - self.filtered_media = {} self.all_media_info = {} self.imdb_info = {} self.disable_buttons = False @@ -119,27 +65,27 @@ def __init__(self): self.starting_height = self.winfo_height() # Frames - self.left_frame = Frame(self, bg=self.colors['main']) - self.status_bar = Canvas(self, bg=self.colors['sub'], bd=0, highlightthickness=0, + self.main_frame = Frame(self, bg=self.colors['main'], bd=1, relief=RIDGE, name='main_frame') + self.left_frame = Frame(self.main_frame, bg=self.colors['main'], bd=1, relief=RIDGE) + self.status_bar = Canvas(self.main_frame, bg=self.colors['main'], bd=0, highlightthickness=0, width=self.starting_width, height=0, relief=SUNKEN) self.status_bar.pack(side=BOTTOM, fill=X) self.left_frame.pack(side=LEFT, fill=Y, ipadx=14, ipady=14) - self.middle_canvas = Canvas(self, bg=self.colors['sub'], bd=2, highlightthickness=0, relief=SUNKEN) - self.canvas_frame = Frame(self.middle_canvas, bg=self.colors['sub']) + self.middle_canvas = Canvas(self.main_frame, bg=self.colors['main'], highlightthickness=0) + self.canvas_frame = Frame(self.middle_canvas, bg=self.colors['main']) self.bind('', self.mid_canvas_dim) self.middle_canvas.create_window((0, 0), window=self.canvas_frame, anchor=NW) - y_scroll_bar = Scrollbar(self, command=self.middle_canvas.yview, orient=VERTICAL) - x_scroll_bar = Scrollbar(self, command=self.middle_canvas.xview, orient=HORIZONTAL) + y_scroll_bar = Scrollbar(self.main_frame, command=self.middle_canvas.yview, orient=VERTICAL) + x_scroll_bar = Scrollbar(self.main_frame, command=self.middle_canvas.xview, orient=HORIZONTAL) self.middle_canvas.config(yscrollcommand=y_scroll_bar.set, xscrollcommand=x_scroll_bar.set) y_scroll_bar.pack(side=RIGHT, fill=Y) x_scroll_bar.pack(side=BOTTOM, fill=X) self.middle_canvas.pack(side=LEFT, fill=BOTH, expand=True, ipadx=4) self.bind_all("", self._on_mousewheel) - self.image_config = { # Used by the buttons on the left of the main app - 'Locate Media': {'image': 'dir.png', 'image_size': (64, 64)}, + 'Locate Media': {'image': 'dir.png', 'image_size': (36, 36)}, 'Select Media': {'image': 'filter.png', 'image_size': (64, 64)}, 'Organize': {'image': 'organize_media.png', 'image_size': (64, 64)}, # Used later by the filter window @@ -152,15 +98,23 @@ def __init__(self): image = img.resize(self.image_config[label_text]['image_size'], Image.ANTIALIAS) self.image_config[label_text]['image_obj'] = ImageTk.PhotoImage(image) + self.image_config = {k: v['image_obj'] for k, v in self.image_config.items()} + note_text = 'If you have a Movies/TV Shows folder with a name other than "Movies" or "TV Shows",' \ ' in your media folder, you should rename them. Casing matters!\n\n' \ 'If you do not have the folders, they will be created for you.' + # Menu + menu = MenuBar(self, font=self.fonts['small'], bg=self.colors['main'], fg=self.colors['font'], + activebackground=self.colors['alt'], activeforeground=self.colors['font']) + tab = menu.add_menu_tab(text='File') + tab.add_menu_item(text='Locate Media', image=self.image_config['Locate Media'], command=self.locate_media) + tab.add_separator() + tab.add_menu_item(text='Exit', command=self.quit) + self.main_frame.pack(side=BOTTOM, fill=BOTH, expand=True) + # Buttons self.buttons_config = { - 'Locate Media': {'command': self.locate_media, - 'tooltip': 'Select your downloads folder and media folder; Your media folder will contain' - ' your "Movies" and "TV Shows" folders'}, 'Select Media': {'command': self.on_press_select_media, 'tooltip': 'Gathers a list of media files from your downloads folder, and' ' allows you to filter which files to organize'}, @@ -173,9 +127,11 @@ def __init__(self): padding_frame = Frame(self.left_frame, bg=self.colors['main']) padding_frame.pack(side=TOP, padx=10, pady=10, fill=X) # Button - button = Button(padding_frame, image=self.image_config[label_text]['image_obj'], command=cfg['command'], - cursor="hand2", bg=self.colors['main'], relief=FLAT, anchor=CENTER) - button.pack(fill=X) + button = Button(padding_frame, image=self.image_config[label_text], command=cfg['command'], + activebackground=self.colors['sub'], activeforeground=self.colors['font'], + bg=self.colors['main'], cursor="hand2", highlightthickness=0, bd=0, relief=FLAT, + anchor=CENTER) + button.pack() self.buttons_config[label_text]['button'] = button # Button label label = Label(padding_frame, text=label_text, font=self.fonts['xsmall'], @@ -254,109 +210,112 @@ def mid_canvas_dim(self, event): def _on_mousewheel(self, event): x, y = self.winfo_pointerxy() - canvas = self.winfo_containing(x, y) - widget_path = str(canvas.master) - if widget_path == '.' or widget_path.startswith('.!canvas2') or widget_path.startswith('.filterwindow'): + widget = self.winfo_containing(x, y) + widget_path = str(widget) + if widget_path.startswith('.main_frame.!canvas2'): self.middle_canvas.yview_scroll(int(-1 * (event.delta / 120)), "units") def get_media_info_from_paths(self, paths): """ Gets all media information from media files in the given paths + Output sample: + '/USER/Downloads': { + 'TV Shows': {'House': {'Season 1': {'C:/USER/Downloads/house.s01e01.avi': House - S01E01'}, + 'Season 2': {'C:/USER/Downloads/house.s02e01.avi': House - S02E01'}}} + 'Movies': {'C:/USER/Downloads/thor.avi': Thor'} + } + :param paths: A list of paths to media files :return: A dictionary of information about each file """ - self.progress_bar_appear() - self.progress_bar['maximum'] = len([file for path in paths - for x, y, files in os.walk(path) - for file in files - if file.split('.')[-1] in self.media_extensions]) - self.progress_bar['value'] = 0 - self.s.configure(style=self.style, text='Getting media info...') - file_info = {} - # Get a list of files to gather info for, and extract info from the local file - for folder_path in paths: - file_info[folder_path] = {} - for path, folders, files in sorted(os.walk(folder_path)): - for file in sorted(files): - current_file_path = os.path.join(path, file) - extension = file.split('.')[-1] - # If its a media file - if extension in self.media_extensions and os.path.isfile(current_file_path): - renamed_file = initcap_file_name(file) - file_folder_path = os.path.dirname(current_file_path) - bottom_folder = os.path.basename(file_folder_path) - # Split the file's path by the folder_path to get the folders inside of the main folder - if folder_path != file_folder_path: - split_path = [x for x in path.split(folder_path) if x] - # This is just making sure the string doesn't start with a slash - top_folder = split_path[-1] if split_path[-1][0] not in ['\\', '/'] \ - else split_path[-1][1:] - top_folder = top_folder.split('\\')[0].split('/')[0] - top_folder_title = str(get_media_title(None, - initcap_file_name(top_folder.replace('.', ' '))) - ).title() - else: - bottom_folder = None - top_folder_title = None - - # Use the Season / Episode extracted based on the path, - # if it could not be extracted from the file name - tv_show_episode, season, episode = tv_show_ep_from_file_name(renamed_file) - s, e = tv_show_ep_from_folder_structure(current_file_path) - t = None - if not season and s: - season = s - t = top_folder_title - if not episode and e: - episode = e - t = top_folder_title - - # Set the kind and season based on the season/episode extracted - if tv_show_episode or (season or season == 0) or (episode or episode == 0): - kind = 'TV Show' - # Assume it is the first season if there is no season extracted, - # but an episode was extracted, and there is a top folder - if not (season or season == 0) and (episode or episode == 0) and t: - season = 1 - # If the folder containing the show has 'extras' in the name, - # then it is safe to assume that this is an extra episode - if 'extras' in str(bottom_folder).lower(): - season = -1 # Extras are identified as season -1 - # Assume it is a movie if it extracted only an episode; i.e. Star Wars Episode 1 - elif not (season or season == 0) and (episode or episode == 0) and not t: + + def extract_info(): + file_info = {} + for folder_path in paths: + file_info[folder_path] = {} + for path, folders, files in sorted(os.walk(folder_path)): + for file in sorted(files): + file_path = os.path.join(path, file) + extension = file.split('.')[-1] + # If its a media file + if extension in self.media_extensions and os.path.isfile(file_path): + renamed_file = initcap_file_name(file) + file_folder_path = os.path.dirname(file_path) + bottom_folder = os.path.basename(file_folder_path) + # Split the file's path by the folder_path to get the folders inside of the main folder + if folder_path != file_folder_path: + split_path = [x for x in path.split(folder_path) if x] + # This is just making sure the string doesn't start with a slash + top_folder = split_path[-1] if split_path[-1][0] not in ['\\', '/'] \ + else split_path[-1][1:] + top_folder = top_folder.split('\\')[0].split('/')[0] + top_folder_title = str(get_media_title(None, + initcap_file_name(top_folder.replace('.', ' '))) + ).title() + else: + bottom_folder = None + top_folder_title = None + + # Use the Season / Episode extracted based on the path, + # if it could not be extracted from the file name + tv_show_episode, season, episode = tv_show_ep_from_file_name(renamed_file) + s, e = tv_show_ep_from_folder_structure(file_path) + season = season or s + episode = episode or e + + # Set the kind and season based on the season/episode extracted + if (season or season == 0) or (episode or episode == 0): + kind = 'TV Show' + # Assume it is the first season if there is no season extracted, + # but an episode was extracted, and there is a top folder + if not (season or season == 0) and (episode or episode == 0) and top_folder_title: + season = 1 + # If the folder containing the show has 'extras' in the name, + # then it is safe to assume that this is an extra episode + if 'extras' in str(bottom_folder).lower(): + season = -1 # Extras are identified as season -1 + # Assume it is a movie if it extracted only an episode; i.e. Star Wars Episode 1 + elif not (season or season == 0) and (episode or episode == 0) and not top_folder_title: + kind = 'Movie' + else: kind = 'Movie' - else: - kind = 'Movie' - - title = get_media_title(tv_show_episode, renamed_file) - years = re.findall(r'20\d\d|19\d\d', file) - year = int(years[0]) if years else None - - # Set the title for the Show/Movie based the show/season/episode info extracted - if kind == 'TV Show': - if (season or season == 0) and season != -1 and (episode or episode == 0): - title = top_folder_title if top_folder_title else title - renamed_file_name = f'{title} - ' \ - f'S{ "0" + str(season) if season < 10 else season}' \ - f'E{ "0" + str(episode) if episode < 10 else episode}' + + title = get_media_title(tv_show_episode, renamed_file) + years = re.findall(r'20\d\d|19\d\d', file) + year = int(years[0]) if years else None + title_w_year = f'{title} ({year})' if year else title + + # Set the title for the Show/Movie based the show/season/episode info extracted + if kind == 'TV Show': + if (season or season == 0) and season != -1 and (episode or episode == 0): + e_str = '' + episode = episode if isinstance(episode, list) else [episode] + for e in episode: + e_str += f'E{"0" + str(e) if e < 10 else e}' + title = top_folder_title if not tv_show_episode else title + renamed_file_name = f'{title} - ' \ + f'S{"0" + str(season) if season < 10 else season}' + e_str + else: + renamed_file_name = renamed_file.split('.')[0] else: - renamed_file_name = renamed_file.split('.')[0] - else: - renamed_file_name = title - - file_info[folder_path][current_file_path] = { - 'title': title, 'renamed_file_name': renamed_file_name, 'file_name': file, - 'file_path': current_file_path, 'top_folder_title': top_folder_title, - 'kind': kind, 'year': year, 'season': season, 'episode': episode} - self.progress_bar['value'] += 1 - self.s.configure(style=self.style, - text=f'Got info for {file}\nIn the {folder_path} directory\n') - if not file_info[folder_path]: - del file_info[folder_path] - - top_folder_dict = {} - for folder_path, info in file_info.items(): - for file_path, i in info.items(): + renamed_file_name = title_w_year + + file_info[folder_path][file_path] = {'kind': kind, 'year': year, + 'title': title_w_year, 'season': season, + 'top_folder_title': top_folder_title, + 'renamed_file': f'{renamed_file_name}.{extension}'} + self.progress_bar['value'] += 1 + self.s.configure(style=self.style, + text=f'Got info for {file}\nIn the {folder_path} directory\n') + if not file_info[folder_path]: + del file_info[folder_path] + + update_dict(file_info[folder_path]) + return file_info + + def update_dict(dictionary): + top_folder_dict = {} + for file_path, i in dictionary.items(): if i['top_folder_title']: if i['top_folder_title'] not in top_folder_dict: top_folder_dict[i['top_folder_title']] = {'title_list': [i['title']], @@ -370,250 +329,123 @@ def get_media_info_from_paths(self, paths): if i['year']: top_folder_dict[i['top_folder_title']]['year'] = i['year'] - for top_folder, info in top_folder_dict.items(): - top_folder_dict[top_folder]['num_unique_titles'] = len(info['title_list']) - for folder_path, info in file_info.items(): - for file_path, i in info.items(): + for top_folder, info in top_folder_dict.items(): + top_folder_dict[top_folder]['num_unique_titles'] = len(info['title_list']) + for file_path, i in dictionary.items(): if i['top_folder_title']: - file_info[folder_path][file_path]['kind'] = top_folder_dict[i['top_folder_title']]['kind'] - file_info[folder_path][file_path]['year'] = top_folder_dict[i['top_folder_title']]['year'] - if not file_info[folder_path][file_path]['season'] \ - and file_info[folder_path][file_path]['kind'] == 'TV Show': - file_info[folder_path][file_path]['season'] = -1 - file_info[folder_path][file_path]['renamed_file_name'] = initcap_file_name(i['file_name'] - ).split('.')[0] + dictionary[file_path]['kind'] = top_folder_dict[i['top_folder_title']]['kind'] + dictionary[file_path]['year'] = top_folder_dict[i['top_folder_title']]['year'] + if not dictionary[file_path]['season'] \ + and dictionary[file_path]['kind'] == 'TV Show': + dictionary[file_path]['season'] = -1 + dictionary[file_path]['renamed_file_name'] = initcap_file_name(os.path.basename(file_path) + ).split('.')[0] if top_folder_dict[i['top_folder_title']]['num_unique_titles'] > 1: - file_info[folder_path][file_path]['title'] = i['top_folder_title'] - del top_folder_dict - self.progress_complete('Gathered Media!') - return file_info - - def media_files_info(self, folder_paths=[]): - """ Gets information about each media file in a path, from IMDb. + dictionary[file_path]['title'] = i['top_folder_title'] + del top_folder_dict + + def convert_dict(dictionary): + converted_dict = {} + for folder, file in dictionary.items(): + converted_dict[folder] = {'TV Shows': {}, 'Movies': {}} + for file_path, i in file.items(): + alias = i['renamed_file'] + title = i['title'] + season = 'Extras' if i['season'] == -1 else f"Season {i['season']}" + if i['kind'] == 'TV Show': + if title not in converted_dict[folder]['TV Shows']: + converted_dict[folder]['TV Shows'][title] = {season: {file_path: alias}} + elif season not in converted_dict[folder]['TV Shows'][title]: + converted_dict[folder]['TV Shows'][title][season] = {file_path: alias} + elif file_path not in converted_dict[folder]['TV Shows'][title][season]: + converted_dict[folder]['TV Shows'][title][season][file_path] = alias + if i['kind'] == 'Movie': + converted_dict[folder]['Movies'][file_path] = alias + return converted_dict + + def remove_empty_items(dictionary, new_dict): + for key, value in dictionary.items(): + if value and isinstance(value, dict): + new_dict[key] = {} + remove_empty_items(dictionary[key], new_dict[key]) + elif value: + new_dict[key] = value - Output sample: - 'C:/USER/Downloads': { - 'C:/USER/Downloads/house.s01e01.avi': { - 'title': 'House (2004)', - 'title': 'House', - 'renamed_file_name': 'House - S01E01 - Pilot', - 'kind': 'TV Show', - 'season': 1, - 'episode': 1, - 'genres': ['Drama', 'Comedy'], - 'file_name': 'house.s01e01.avi'}} - - :param folder_paths: The path to your media files - :return: A dictionary of information about each file - """ - self.all_media_info = self.get_media_info_from_paths(folder_paths) - - if not self.all_media_info: - self.progress_bar_appear() - self.progress_complete('\nNo Media Detected...\n') - return 'No Media' - - else: - self.filter_window(self.all_media_info) - - def filter_window(self, all_media): - """ The filter window that appears to filter the media files to be sorted. - - :param all_media: The dictionary of all the media to filter - :return: Filtered media - """ - self.filtered_media = deepcopy(all_media) + self.progress_bar_appear() + self.progress_bar['maximum'] = len([file for path in paths + for x, y, files in os.walk(path) + for file in files + if file.split('.')[-1] in self.media_extensions]) + self.progress_bar['value'] = 0 + self.s.configure(style=self.style, text='Getting media info...') + # Gets info from media files + file_info = extract_info() + # Converts the dictionary to be compatible with checklist + file_info = convert_dict(file_info) + file_info_new = {} + remove_empty_items(file_info, file_info_new) + self.progress_complete('Gathered Media!') + return file_info_new - def upon_select(widget): - d = widget['destination'] - fp = widget['file_path'] - # Add or remove files from list when they are toggled - if widget['button'].var.get(): - if fp not in self.filtered_media[d]: - self.filtered_media[d][fp] = all_media[d][fp] - widget['button']['bg'] = self.colors['sub'] - else: - if fp in self.filtered_media[d]: - del self.filtered_media[d][fp] - widget['button']['bg'] = self.colors['main'] + def recursively_organize_shows_and_movies(self, delete_folders=True): - def toggle_all(x): - button, kind, files_dict = x - deselect_image = self.image_config['deselect']['image_obj'] - select_image = self.image_config['select']['image_obj'] - # Toggle each item in the dictionary based on the toggle - toggle = True if str(button['image']) == str(select_image) else False - for file, widget in files_dict.items(): - widget['button'].var = BooleanVar(value=toggle) - widget['button']['variable'] = widget['button'].var - upon_select(widget) - if toggle: - button['image'] = deselect_image - if self.toggle_all_buttons[kind] == button: - for title, b in self.toggle_title_buttons[kind].items(): - b['image'] = deselect_image - else: - button['image'] = select_image - if self.toggle_all_buttons[kind] == button: - for title, b in self.toggle_title_buttons[kind].items(): - b['image'] = select_image - - if self.toggle_all_buttons[kind] != button: - off_title_buttons = [str(btn['image']) for t, btn in self.toggle_title_buttons[kind].items() - if str(btn['image']) == str(select_image)] - if off_title_buttons: - self.toggle_all_buttons[kind]['image'] = select_image + def inc_max_progress(d): + """ Helper callback function to increment the max progress bar value for each file """ + for k, v in d.items(): + if not isinstance(v, dict): + self.progress_bar['maximum'] += 1 else: - self.toggle_all_buttons[kind]['image'] = deselect_image - - def collapse_show(checklist_frame): - if checklist_frame.winfo_viewable(): - checklist_frame.pack_forget() - else: - checklist_frame.pack(fill=X) - - def create_checklist(frame, files): - kind_mapping = {'TV Show': 'TV Shows', 'Movie': 'Movies'} - self.toggle_all_buttons = {k: {} for k in kind_mapping.keys()} - self.toggle_title_buttons = {k: {} for k in kind_mapping.keys()} - for folder_path, info in files.items(): - destination_frame = Frame(frame, bg=self.colors['main'], bd=1, relief=RIDGE) - destination_frame.pack(side=LEFT, padx=4, pady=4, anchor=N) - destination_title = Label(destination_frame, text=folder_path, anchor=NW, justify=LEFT, - bg=self.colors['main'], fg=self.colors['font'], font=self.fonts['medium']) - destination_title.pack(side=TOP, fill=X, anchor=W) - Separator(destination_frame).pack(side=TOP, fill=X) - sections = sorted(list(set([i['kind'] for f, i in info.items()])), reverse=True) - for kind in sections: - kind_dict = dict() - if sections.index(kind) > 0: - # Add a separator between shows and movies - Separator(destination_frame).pack(fill=X, pady=8) - media_type_frame = Frame(destination_frame, bg=self.colors['main']) - media_type_frame.pack(side=TOP, fill=X, anchor=NW) - # Toggle everything for each 'Kind' - self.toggle_all_buttons[kind] = Button(media_type_frame, cursor='hand2', - image=self.image_config['deselect']['image_obj'], - relief=FLAT, bg=self.colors['main']) - self.toggle_all_buttons[kind].pack(side=LEFT, anchor=NW) - media_type_label = Label(media_type_frame, text=kind_mapping[kind], font=self.fonts['small'], anchor=NW, - width=50, bg=self.colors['main'], fg=self.colors['font'], justify=LEFT) - media_type_label.pack(side=LEFT, fill=X) - media_content_frame = Frame(destination_frame, bg=self.colors['main'], bd=2, relief=SUNKEN) - media_content_frame.pack(side=TOP, fill=X, anchor=NW) - if kind == 'Movie': - movie_frame = Frame(destination_frame, bg=self.colors['main'], bd=2, relief=SUNKEN) - movie_frame.pack(side=TOP, fill=X, anchor=NW) - for t in sorted(list(set([v['title'] for k, v in info.items() if v['kind'] == kind]))): - dictionary = dict() - if kind == 'TV Show': - show_frame = Frame(media_content_frame, bg=self.colors['main']) - show_frame.pack(side=TOP, fill=X, anchor=NW) - show_title_frame = Frame(show_frame, bg=self.colors['special']) - show_title_frame.pack(side=TOP, fill=X, anchor=NW) - # Toggle everything for each TV Show title - self.toggle_title_buttons[kind][t] = Button(show_title_frame, - image=self.image_config['deselect']['image_obj'], - text='deselect', cursor='hand2', relief=FLAT, - bg=self.colors['main']) - self.toggle_title_buttons[kind][t].pack(side=LEFT, fill=Y) - show_title_button = Button(show_title_frame, text=t, cursor='hand2', - font=self.fonts['small'], anchor=W, bd=2, relief=RAISED, - bg=self.colors['main'], fg=self.colors['font'], - justify=LEFT) - show_title_button.pack(side=LEFT, fill=X, expand=True, anchor=W, padx=1, pady=1) - show_checklist_frame = Frame(show_frame, bg=self.colors['main'], bd=2, relief=SUNKEN) - show_checklist_frame.pack(side=TOP, fill=X, anchor=NW) - show_title_button['command'] = lambda x=show_checklist_frame: collapse_show(x) - show_checklist_frame.pack_forget() - season_frames = {} - try: - for s in sorted(list(set([v['season'] for k, v in info.items() if v['title'] == t]))): - season_frames[s] = Frame(show_checklist_frame, bg=self.colors['main']) - season_frames[s].pack(side=TOP, fill=X, anchor=NW) - season = 'Season ' + str(s) if s != -1 else 'Extras' - season_label = Label(season_frames[s], text=season, font=self.fonts['small'], - bg=self.colors['alt'], fg=self.colors['font']) - season_label.pack(side=TOP, fill=X, padx=1, pady=1) - except Exception as e: - print(t) - print([v['season'] for k, v in info.items() if v['title'] == t]) - raise Exception(e) - - for file_path, i in {f: i for f, i in info.items() if i['title'] == t}.items(): - dictionary[file_path] = i - if kind == 'TV Show': - dictionary[file_path]['button'] = Checkbutton(season_frames[i['season']]) - else: - dictionary[file_path]['button'] = Checkbutton(movie_frame) - dictionary[file_path]['button'].config(text=i['renamed_file_name'], fg=self.colors['font'], - onvalue=True, offvalue=False, anchor=NW, - bg=self.colors['sub'], font=self.fonts['xsmall'], - selectcolor=self.colors['main']) - dictionary[file_path]['button'].var = BooleanVar(value=True) - dictionary[file_path]['button']['variable'] = dictionary[file_path]['button'].var - dictionary[file_path]['button']['command'] = lambda w=dictionary[file_path]: upon_select(w) - dictionary[file_path]['button'].pack(side=TOP, fill=X, anchor=NW, pady=1, padx=1) - dictionary[file_path]['destination'] = folder_path - kind_dict[file_path] = dictionary[file_path] - if kind == 'TV Show': - self.toggle_title_buttons[kind][t]['command'] = lambda d=(self.toggle_title_buttons[kind][t] - , kind, dictionary): toggle_all(d) - self.toggle_all_buttons[kind]['command'] = lambda d=(self.toggle_all_buttons[kind], kind, - kind_dict): toggle_all(d) - - main_frame = Frame(self.canvas_frame, bg=self.colors['sub'], name='filterwindow') - main_frame.pack(fill=BOTH) - create_checklist(main_frame, files=all_media) + inc_max_progress(d[k]) + + def move_files(file_dict, path=[], level=0): + """ Callback function to moves the files """ + level += 1 + for file_path, new_file_name in file_dict.items(): + file_folder = os.path.dirname(file_path) + if not isinstance(new_file_name, dict): + delete = False + output_path = os.path.join(media_path, *path) + if not os.path.exists(output_path): + os.makedirs(output_path) + move_path = os.path.join(output_path, os.path.basename(file_path)) + rename_path = os.path.join(output_path, new_file_name) + if not os.path.exists(move_path) and not os.path.exists(rename_path): + # Move to the new location + shutil.move(file_path, move_path) + # Then rename the file + os.rename(move_path, rename_path) + status_message = f'Moved & Renamed:\nFrom: {file_path}\nTo: {rename_path}' + delete = True + elif os.path.exists(move_path) and not os.path.exists(rename_path): + # Then rename the file + os.rename(move_path, rename_path) + status_message = f'Renamed:\nFrom: {file_path}\nTo: {rename_path}' + else: + status_message = f'Skipping: {file_path}\nFile exists {rename_path}' + + self.progress_bar['value'] += 1 + self.s.configure(style=self.style, text=status_message) + + # Add the moved file's folder path to the list of folders to delete + if file_folder != dl_path and file_folder != media_path and file_folder != output_path and delete \ + and file_folder not in folders_to_delete: + folders_to_delete.append(file_folder) + else: + if len(path) >= level: + path = path[:level-1] + path.append(file_path) + move_files(file_dict[file_path], path=path, level=level) - def recursively_organize_shows_and_movies(self, delete_folders=True): dl_path = get_downloads_or_media_path('downloads') media_path = get_downloads_or_media_path('media') folders_to_delete = [] - self.progress_bar_appear() - self.progress_bar['maximum'] = len(self.filtered_media.keys()) + self.progress_bar['maximum'] = 0 self.progress_bar['value'] = 0 + inc_max_progress(self.all_media_info) + self.progress_bar_appear() - for folder_path, i in self.filtered_media.items(): - for file_path, info in i.items(): - path = os.path.dirname(file_path) - file = os.path.basename(file_path) - extension = file.split('.')[-1] - skip = False - # Route for TV Shows - if info['kind'] == 'TV Show': - season_folder = 'Extras' if info["season"] == -1 else f'Season {info["season"]}' - output_folder = os.path.join(media_path, 'TV Shows', info['title'], season_folder) - # Route for Movies - else: - output_folder = os.path.join(media_path, 'Movies') - output_path = os.path.join(output_folder, file) - renamed_file = info['renamed_file_name'] + '.' + extension - renamed_file_path = os.path.join(output_folder, renamed_file) - # Create output folder if it does not exist - if not os.path.exists(output_folder): - os.makedirs(output_folder) - # Move and then rename file - if not os.path.exists(output_path) and not os.path.exists(renamed_file_path): - shutil.move(file_path, output_path) - os.rename(output_path, renamed_file_path) - status_message = f'Moved & Renamed {info["kind"].title()}:\nFrom: {file}\nTo: {renamed_file}' - # Update status and increment progress bar to show that the file has moved - - elif not os.path.exists(renamed_file_path): - os.rename(output_path, renamed_file_path) - # Update status and increment progress bar to show that the file has moved - status_message = f'File exists in {output_folder},' \ - f' Renamed {info["kind"].title()}:\nFrom: {file}\nTo: {renamed_file}' - else: - skip = True - status_message = f'Skipping: {file}\nFile exists in {output_folder}:\n{renamed_file_path}' - self.progress_bar['value'] += 1 - self.s.configure(style=self.style, text=status_message) - # Add the moved file's folder path to the list of folders to delete - if path != dl_path and path != media_path and path != os.path.dirname(output_path) \ - and path not in folders_to_delete and not skip: - folders_to_delete.append(path) + for folder_path, i in self.all_media_info.items(): + move_files(i) # Delete folders that contained media files that were moved if delete_folders: @@ -640,8 +472,8 @@ def on_submit(): submit_button.destroy() main_frame.destroy() - main_frame = Frame(self.canvas_frame, bg=self.colors['main'], bd=1, relief=RAISED) - main_frame.pack(padx=8, pady=90) + main_frame = Frame(self.canvas_frame, bg=self.colors['sub'], bd=1, relief=RIDGE) + main_frame.pack(padx=8, pady=8) self.toggle_buttons_enabled() self.final_todo_list = [] @@ -651,24 +483,29 @@ def on_submit(): 'media': 'Movies and TV Shows' } Label(main_frame, text='Which folders would you like to organize?', font=self.fonts['medium'], - bg=self.colors['main'], fg=self.colors['font']).pack(side=TOP, fill=X, ipady=20, ipadx=20) + bg=self.colors['sub'], fg=self.colors['font']).pack(side=TOP, fill=X, ipady=20, ipadx=20) - options_frame = Frame(main_frame, bg=self.colors['main'], bd=2, relief=SUNKEN) - options_frame.pack(side=TOP) + outer_options_frame = Frame(main_frame, bg=self.colors['main'], bd=1, relief=RIDGE) + outer_options_frame.pack(side=TOP) + options_frame = Frame(outer_options_frame, bg=self.colors['main']) + options_frame.pack(padx=6, pady=6) dictionary = dict() for i, desc in todo_list.items(): dictionary[i] = {'button': Checkbutton(options_frame, text=desc, onvalue=True, offvalue=False, - font=self.fonts['xsmall'], - anchor=NW, bg=self.colors['alt'], fg=self.colors['font'], + activebackground=self.colors['alt'], + activeforeground=self.colors['font'], + anchor=W, cursor='hand2', font=self.fonts['xsmall'], + bg=self.colors['alt'], fg=self.colors['font'], selectcolor=self.colors['main'])} dictionary[i]['button'].var = BooleanVar(value=True) self.final_todo_list.append(i) dictionary[i]['button']['variable'] = dictionary[i]['button'].var dictionary[i]['button']['command'] = lambda w=dictionary[i]: upon_select(w) - dictionary[i]['button'].pack(side=TOP, fill=X, padx=1, pady=1) + dictionary[i]['button'].pack(fill=X, padx=1, pady=1) dictionary[i]['todo'] = i - submit_button = Button(main_frame, text='Select', command=on_submit, font=self.fonts['small'], + submit_button = Button(main_frame, text='Select', command=on_submit, font=self.fonts['small'], cursor='hand2', + activebackground=self.colors['special'], activeforeground=self.colors['font'], bg=self.colors['special'], fg=self.colors['font']) submit_button.pack(side=BOTTOM, pady=20) self.wait_window(main_frame) @@ -699,11 +536,18 @@ def on_press_select_media(self): else: paths.append(get_downloads_or_media_path(option)) - outcome = self.media_files_info(folder_paths=paths) - if outcome != 'No Media': - self.organize_button_appear() - else: + self.all_media_info = self.get_media_info_from_paths(paths) + + if not self.all_media_info: + self.progress_bar_appear() + self.progress_complete('\nNo Media Detected...\n') self.organize_button_disappear() + else: + CheckList(self.canvas_frame, dictionary=self.all_media_info, toggle_config=self.image_config, + toggle_type='image', font=self.fonts['large'], + bg=self.colors['main'], fg=self.colors['font'], + highlight_color=self.colors['sub'], border_color=self.colors['special']) + self.organize_button_appear() def on_press_organize_media(self): """ Start organizing media """ @@ -712,7 +556,7 @@ def on_press_organize_media(self): def organize_button_appear(self): self.buttons_config['Organize']['button'].pack() - self.buttons_config['Organize']['label'].pack() + self.buttons_config['Organize']['label'].pack(fill=X) def organize_button_disappear(self): self.buttons_config['Organize']['button'].pack_forget() @@ -728,7 +572,7 @@ def progress_bar_appear(self): def progress_complete(self, message): self.toggle_buttons_enabled() - self.progress_bar['value'] = 0 + self.progress_bar['value'] = self.progress_bar['maximum'] self.s.configure(style=self.style, text=message) def toggle_buttons_enabled(self): diff --git a/Python3_Projects/Organize_Media/components/__init__.py b/Python3_Projects/Organize_Media/components/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/Python3_Projects/Organize_Media/components/checklist.py b/Python3_Projects/Organize_Media/components/checklist.py new file mode 100644 index 00000000..a986f78d --- /dev/null +++ b/Python3_Projects/Organize_Media/components/checklist.py @@ -0,0 +1,274 @@ +from tkinter import * +from copy import deepcopy +from PIL import Image, ImageTk +# Import components +from .collapsible_frame import CollapsibleFrame + + +class CheckList(Frame): + """ Creates a series of checklists based on a dictionary. + The first level of Keys int the dictionary are a column (and the title of that column). + Each level of the dictionary adds a sub-section. + The lowest level of the dictionary is the checklist. + + Dictionary Sample: + { + 'column': { + 'section': + sub_section: { + 'checklist_name': 'alias_name' + } + } + } + } + + Sample output: + Column1 | Column2 + section1 | section1 + sub_section1 | sub_section1 + [x] checkbutton1 | [x] checkbutton1 + [x] checkbutton2 | [x] checkbutton2 + sub_section2 | sub_section2 + [x] checkbutton1 | [x] checkbutton1 + [x] checkbutton2 | [x] checkbutton2 + section2 | section2 + sub_section1 | sub_section1 + [x] checkbutton1 | [x] checkbutton1 + + + :param dictionary: The dictionary of all the media to filter + :return: Filtered media + """ + + def __init__(self, widget, dictionary, **kwargs): + Frame.__init__(self) + # self.config(bg=bg) + self.bg = kwargs.pop('bg', None) + self.fg = kwargs.pop('fg', None) + self.highlight_color = kwargs.pop('highlight_color', None) + self.border_color = kwargs.pop('border_color', None) + self.section_color = kwargs.pop('section_color', None) + self.font = kwargs.pop('font', ('none', 20, 'bold')) + self.dictionary = dictionary + self.reference_dict = deepcopy(dictionary) + self.check_button_dict = dict() + self.toggle_button_dict = deepcopy(dictionary) + self.toggle_config = kwargs.pop('toggle_config', {'deselect': 'X', 'select': 'O'}) + self.toggle_type = kwargs.pop('toggle_type', 'text') + self.deepest_level = 0 + + main_frame = Frame(widget, bg=self.bg, name='checklist') + main_frame.pack(fill=BOTH) + self.construct_columns(main_frame, checklist_dict=self.dictionary) + + def add_to_dict(self, path, update_dict, source_dict): + if len(path) == 1 or path[0] not in update_dict: + update_dict[path[0]] = source_dict[path[0]] + else: + self.add_to_dict(path[1:], update_dict[path[0]], source_dict[path[0]]) + + def remove_from_dict(self, path, update_dict): + if len(path) == 1 and path[0] in update_dict: + del update_dict[path[0]] + elif path[0] in update_dict: + self.remove_from_dict(path[1:], update_dict[path[0]]) + + def upon_select(self, widget): + # Add or remove items from list when they are toggled + path = self.check_button_dict[widget] + if widget.var.get(): + self.add_to_dict(path, self.dictionary, self.reference_dict) + widget['bg'] = self.highlight_color + else: + self.remove_from_dict(path, self.dictionary) + widget['bg'] = self.bg + + def all_children(self, widget, callback): + _list = widget.winfo_children() + for w in _list: + callback.append(w) + self.all_children(w, callback) + return callback + + def update_button_toggle(self, path, dictionary, frame, first_run=True): + if len(path) >= 1 and isinstance(dictionary[path[0]], dict): + f = dictionary[path[0]]['frame'] + b = dictionary[path[0]]['button'] + callback = [] + checkbutton_children = [c for c in self.all_children(f, callback) + if type(c) == Checkbutton and not c.var.get()] + if checkbutton_children: + b[self.toggle_type] = self.toggle_config['select'] + else: + b[self.toggle_type] = self.toggle_config['deselect'] + # Set all child toggle buttons equal to the parent on the first + if first_run: + callback = [] + toggle_children = [b for b in self.all_children(frame, callback) + if type(b) == Button + and b[self.toggle_type] in [str(i) for i in self.toggle_config.values()]] + for child in toggle_children: + child[self.toggle_type] = b[self.toggle_type] + self.update_button_toggle(path[1:], dictionary[path[0]], frame, first_run=False) + + def toggle_all(self, x): + """ Functionality when clicking a toggle all button: + - Checks all dependant check buttons: on or off depending on the status of the toggle button + - Adjusts the status of other dependant toggle buttons """ + + frame, button, path = x + child_callback = [] + checklist_children = [obj for obj in self.all_children(frame, child_callback) if type(obj) == Checkbutton] + # Toggle each item in the dictionary based on the toggle + toggle = str(button[self.toggle_type]) == str(self.toggle_config['select']) + for c in checklist_children: + c.var = BooleanVar(value=toggle) + c['variable'] = c.var + self.upon_select(c) + + # Go down the path, and toggle button on or off based on each dependant checkbutton's status + self.update_button_toggle(path, self.toggle_button_dict, frame) + + def create_checklist_button(self, frame, dict_path, **kwargs): + """ Creates a Checkbutton and adds it to the dictionary of Checkbuttons """ + name = kwargs.pop('name', None) + font = kwargs.pop('font', None) + cbutton = Checkbutton(frame, text=name, fg=self.fg, onvalue=True, highlightthickness=0, bd=0, + offvalue=False, anchor=W, justify=LEFT, font=font, cursor='hand2', + selectcolor=self.highlight_color, bg=self.highlight_color, + activeforeground=self.fg, activebackground=self.highlight_color) + cbutton.var = BooleanVar(value=True) + cbutton['variable'] = cbutton.var + cbutton['command'] = lambda w=cbutton: self.upon_select(w) + self.check_button_dict[cbutton] = dict_path + + def add_to_toggle_button_dict(self, path, dict, button, frame): + if len(path) > 1: + self.add_to_toggle_button_dict(path[1:], dict[path[0]], button, frame) + elif len(path) == 1: + dict[path[0]]['button'] = button + dict[path[0]]['frame'] = frame + + def create_section_frame(self, frame, **kwargs): + """ Create the section frame for a given section """ + path = kwargs.pop('path', None) + title = kwargs.pop('title', None) + font = kwargs.pop('font', None) + padx = kwargs.pop('padx', None) + pady = kwargs.pop('pady', None) + start_collapsed = kwargs.pop('start_collapsed', True) + + section_frame = Frame(frame, bg=self.bg) + section_frame.pack(side=TOP, fill=X, padx=padx, pady=pady, anchor=NW) + + f = CollapsibleFrame(section_frame, title=title, font=font, bg=self.bg, fg=self.fg, + highlight_color=self.highlight_color, title_color=self.border_color, + start_collapsed=start_collapsed) + section_bottom_frame = Frame(f.collapse_frame, bg=self.bg) + toggle_button = Button(f.top_frame, cursor='hand2', relief=FLAT, bd=0, highlightthickness=0, + bg=self.bg, fg=self.fg, activebackground=self.bg, activeforeground=self.fg) + toggle_button[self.toggle_type] = self.toggle_config['deselect'] + toggle_button['command'] = lambda x=(section_bottom_frame, toggle_button, path): self.toggle_all(x) + toggle_button.pack(side=LEFT) + self.add_to_toggle_button_dict(path=path, dict=self.toggle_button_dict, button=toggle_button, + frame=section_bottom_frame) + + section_bottom_frame.pack(side=TOP, fill=X, anchor=NW) + + return section_bottom_frame + + def construct_sections(self, frame, level=0, section=None, checklist_dict=None, + path=None, **kwargs): + """ Constructs all sections for a given dictionary. + Will continue creating sections until the value is not a dictionary. """ + font = kwargs.pop('font', None) + padx = kwargs.pop('padx', None) + pady = kwargs.pop('pady', None) + # Use the level to keep track of how deep we are in the dictionary + level += 1 + if level > self.deepest_level: + self.deepest_level = level + font = (font[0], font[1] - 1, font[2]) + for sub_section in sorted([k for k in checklist_dict[section].keys()]): + # Adjust path for the section based on the level of the dictionary + if len(path) >= level + 1: + path = path[:level + 1] + path.pop(level) + path.append(sub_section) + + if isinstance(checklist_dict[section][sub_section], dict): + section_frame = self.create_section_frame(frame, title=sub_section, font=font, padx=padx, pady=pady, + path=path) + self.construct_sections(frame=section_frame, font=font, padx=padx, pady=pady, + section=sub_section, checklist_dict=checklist_dict[section], + path=path, level=level) + else: + self.create_checklist_button(frame, name=checklist_dict[section][sub_section], font=font, + dict_path=path) + + def construct_columns(self, frame, checklist_dict): + """ Constructs the columns for the checklists. + Assumes the first key in the dict is the column name """ + temp = deepcopy(checklist_dict) + indent = 25 + num_cols = 0 + for column in sorted(list(temp.keys())): + num_cols += 1 + if isinstance(temp[column], dict): + toggle_buttons = [] + c_frame = Frame(frame, bg=self.bg) + c_frame.pack(side=LEFT, anchor=NW) + column_frame = self.create_section_frame(c_frame, title=column, font=self.font, path=[column], + toggle_buttons=toggle_buttons, start_collapsed=False, + padx=2, pady=2) + self.construct_sections(frame=column_frame, font=self.font, section=column, + checklist_dict=temp, path=[column], toggle_buttons=toggle_buttons, + padx=(indent, 0), pady=4) + else: + raise Exception(f'Not a proper dictionary. Expected {column} to have a dictionary.') + # Pack the Check Buttons + for ch in self.check_button_dict.keys(): + ch['font'] = (self.font[0], self.font[1] - self.deepest_level, self.font[2]) + ch.pack(side=TOP, fill=X, anchor=NW, pady=1, padx=(indent, 0)) + + +if __name__ == '__main__': + dictionary = { + 'HEADER_1': { + 'Zheckbox_1': 'Zheck 1', + 'Section_1': { + 'Sub_Section_1': { + 'Checkbox_1': 'Check 1'}}, + 'Section_2': { + 'Sub_Section_1': { + 'Checkbox_1': 'Check 1', + 'Checkbox_2': 'Check 2'}, + 'Sub_Section_2': { + 'Checkbox_1': 'Check 1'}}, + 'Section_3': { + 'Checkbox_1': 'Check 1'}, + 'Checkbox_2': 'Check 2'}, + 'HEADER_2': { + 'Section_1': { + 'Checkbox_1': 'Check 1'}, + 'Checkbox_1': 'Check 1'}, + 'HEADER_3': { + 'Checkbox_1': 'Check 1'} + } + + colors = { + 'main': '#%02x%02x%02x' % (20, 20, 20), + 'sub': '#%02x%02x%02x' % (35, 35, 35), + 'special': '#%02x%02x%02x' % (92, 15, 128), + 'special_alt': '#%02x%02x%02x' % (180, 53, 240), + 'alt': '#%02x%02x%02x' % (60, 111, 194), + 'font': '#%02x%02x%02x' % (255, 255, 255) + } + font = ('Arial', 20, 'bold') + + app = Tk() + app.title('Example Checklist') + checklist = CheckList(app, dictionary=dictionary, bg=colors['main'], fg=colors['font'], font=font, + highlight_color=colors['sub'], border_color=colors['special']) + checklist.pack() + app.mainloop() diff --git a/Python3_Projects/Organize_Media/components/collapsible_frame.py b/Python3_Projects/Organize_Media/components/collapsible_frame.py new file mode 100644 index 00000000..1ff94080 --- /dev/null +++ b/Python3_Projects/Organize_Media/components/collapsible_frame.py @@ -0,0 +1,53 @@ +from tkinter import * + + +class CollapsibleFrame(Frame): + """ Creates a collapsible frame, with a button to expand and collapse the frame. + Frame starts collapsed by default. """ + def __init__(self, top, **kwargs): + Frame.__init__(self) + self.bg = kwargs.pop('bg', None) + self.fg = kwargs.pop('fg', None) + self.title_color = kwargs.pop('title_color', None) + self.highlight_color = kwargs.pop('highlight_color', None) + self.font = kwargs.pop('font', None) + self.title = kwargs.pop('title', None) + self.alignment = kwargs.pop('alignment', W) + self.start_collapsed = kwargs.pop('start_collapsed', True) + + main_frame = Frame(top, bg=self.bg, bd=0, relief=RIDGE) + main_frame.pack(fill=BOTH, expand=True) + self.top_frame = Frame(main_frame, bg=self.bg) + self.top_frame.pack(fill=X) + title_frame = Frame(self.top_frame, bg=self.title_color) + title_frame.pack(side=RIGHT, fill=X, expand=True) + + title = Button(title_frame, text=self.title, bg=self.bg, fg=self.fg, cursor='hand2', relief=RIDGE, bd=0, + font=self.font, anchor=self.alignment, command=self.collapse, highlightthickness=0, + activebackground=self.highlight_color, activeforeground=self.fg) + self.collapse_frame = Frame(main_frame, bg=self.bg) + title.pack(side=TOP, fill=X, padx=1, pady=1, ipadx=4, anchor=self.alignment) + if not self.start_collapsed: + self.collapse_frame.pack(fill=X) + + def collapse(self): + if self.collapse_frame.winfo_viewable(): + self.collapse_frame.pack_forget() + else: + self.collapse_frame.pack(fill=X) + + +if __name__ == '__main__': + """ Example """ + + app = Tk() + app.title('Test') + app.geometry('300x300') + + f = CollapsibleFrame(app, title='Title', bg='black', fg='white', title_color='yellow') + label = Label(f.collapse_frame, text=' - Test') + label.pack(side=LEFT) + button = Button(f.top_frame, text='Add Button') + button.pack(side=LEFT) + + app.mainloop() diff --git a/Python3_Projects/Organize_Media/components/menu_bar.py b/Python3_Projects/Organize_Media/components/menu_bar.py new file mode 100644 index 00000000..4de749c0 --- /dev/null +++ b/Python3_Projects/Organize_Media/components/menu_bar.py @@ -0,0 +1,129 @@ +from tkinter import * +from tkinter.ttk import Separator + + +class MenuButton: + def __init__(self, widget, cascade_window, **kwargs): + + self.text = kwargs.pop('text', None) + self.font = kwargs.pop('font', None) + self.bg = kwargs.pop('bg', None) + self.fg = kwargs.pop('fg', None) + self.activebackground = kwargs.pop('activebackground', self.bg) + self.activeforeground = kwargs.pop('activeforeground', self.fg) + self.cursor = kwargs.pop('cursor', 'hand2') + + self.__cascade_window = cascade_window + self.__cascade_frame = Frame(self.__cascade_window, bg=self.bg, bd=2, relief=SUNKEN) + + self.__button = Button(widget, text=self.text, command=self.__toggle_cascade_window, cursor=self.cursor, + font=self.font, relief=FLAT, bd=0, bg=self.bg, fg=self.fg, + activebackground=self.activebackground, activeforeground=self.activeforeground) + self.__button.pack(side=LEFT, padx=(3, 0), pady=(3, 1)) + + self.__button.bind('', self.__on_enter_tab) + self.__button.bind('', self.__set_background_default) + + def add_menu_item(self, text=None, image=None, command=None): + b = Button(self.__cascade_frame, text=text, image=image, compound=LEFT, command=command, cursor=self.cursor, + font=self.font, relief=FLAT, bd=0, bg=self.bg, fg=self.fg, + activebackground=self.activebackground, activeforeground=self.activeforeground) + b.pack(side=TOP, fill=X, padx=2, pady=1) + b.bind('', self.__set_background_default) + b.bind('', self.__set_background_highlight) + + def add_separator(self): + Separator(self.__cascade_frame).pack(fill=X, padx=2, pady=1) + + def __on_enter_tab(self, event): + self.__set_background_highlight(event) + # Hid all frames, show the cascade window, and then show the frame we want + for f in self.__cascade_window.winfo_children(): + f.pack_forget() + self.__cascade_window.update() + x, y = self.__button.winfo_rootx(), self.__button.winfo_rooty() + self.__button.winfo_reqheight() + self.__cascade_window.geometry(f'+{x}+{y}') + self.__cascade_frame.pack() + + def __toggle_cascade_window(self): + if self.__cascade_window.winfo_ismapped(): + self.__cascade_window.withdraw() + else: + self.__cascade_window.update() + x, y = self.__button.winfo_rootx(), self.__button.winfo_rooty() + self.__button.winfo_reqheight() + self.__cascade_window.geometry(f'+{x}+{y}') + self.__cascade_window.deiconify() + + def __set_background_highlight(self, event): + event.widget.config(bg=self.activebackground, fg=self.activeforeground) + + def __set_background_default(self, event): + event.widget.config(bg=self.bg, fg=self.fg) + + +class MenuBar(Frame): + def __init__(self, widget, **kwargs): + Frame.__init__(self, widget) + + def on_click(event): + """ toggle off the cascade window """ + if self.__cascade_window.winfo_ismapped(): + # Does not turn off right away to give time for selecting options + self.after(300, self.__cascade_window.withdraw) + + def on_window_adjustment(event): + """ toggle off the cascade window """ + self.__cascade_window.withdraw() + + self.font = kwargs.pop('font', None) + self.bg = kwargs.pop('bg', None) + self.fg = kwargs.pop('fg', None) + self.activebackground = kwargs.pop('activebackground', self.bg) + self.activeforeground = kwargs.pop('activeforeground', self.fg) + self.cursor = kwargs.pop('cursor', 'hand2') + + self.__menu_bar = Frame(widget, bg=self.bg, bd=1, relief=RIDGE, name='menu_bar') + self.__menu_bar.pack(side=TOP, fill=X) + + self.__cascade_window = Toplevel(widget) + self.__cascade_window.overrideredirect(True) + self.__cascade_window.withdraw() + self.bind_all('', on_click) + self.master.bind('', on_window_adjustment) + + def add_menu_tab(self, text=None): + b = MenuButton(self.__menu_bar, self.__cascade_window, text=text, cursor=self.cursor, + font=self.font, bd=0, bg=self.bg, fg=self.fg, + activebackground=self.activebackground, activeforeground=self.activeforeground) + return b + + +if __name__ == '__main__': + def on_save(): + print('Save...') + + def on_open(): + print('Open...') + + def on_copy(): + print('Copy...') + + def on_cut(): + print('Cut...') + + app = Tk() + app.title('Menu Bar Example') + app.geometry('300x200') + app.config(bg='darkgrey') + + menu = MenuBar(app, font=('Helvetica', 12, 'bold'), bg='black', fg='white', + activebackground='#444444', activeforeground='white') + tab1 = menu.add_menu_tab(text='File') + tab2 = menu.add_menu_tab(text='Edit') + tab1.add_menu_item(text='Save', command=on_save) + tab1.add_separator() + tab1.add_menu_item(text='Exit', command=app.destroy) + tab2.add_menu_item(text='Copy', command=on_copy) + tab2.add_menu_item(text='Cut', command=on_cut) + + app.mainloop() diff --git a/Python3_Projects/Organize_Media/components/tooltip.py b/Python3_Projects/Organize_Media/components/tooltip.py new file mode 100644 index 00000000..59eff3d8 --- /dev/null +++ b/Python3_Projects/Organize_Media/components/tooltip.py @@ -0,0 +1,56 @@ +from tkinter import * + + +class CreateToolTip(object): + """ Create a tooltip for a given widget """ + + def __init__(self, widget, text='widget info', bg='#ffffff', fg='#000000'): + self.waittime = 500 #miliseconds + self.wraplength = 180 #pixels + self.widget = widget + self.text = text + self.bg = bg + self.fg = fg + self.widget.bind("", self.enter) + self.widget.bind("", self.leave) + self.widget.bind("", self.leave) + self.id = None + self.tw = None + + def enter(self, event=None): + self.schedule() + + def leave(self, event=None): + self.unschedule() + self.hidetip() + + def schedule(self): + self.unschedule() + self.id = self.widget.after(self.waittime, self.showtip) + + def unschedule(self): + id = self.id + self.id = None + if id: + self.widget.after_cancel(id) + + def showtip(self, event=None): + x = y = 0 + x, y, cx, cy = self.widget.bbox("insert") + x += self.widget.winfo_rootx() + 25 + y += self.widget.winfo_rooty() + 20 + # creates a toplevel window + self.tw = Toplevel(self.widget) + # Leaves only the label and removes the app window + self.tw.wm_overrideredirect(True) + self.tw.wm_geometry("+%d+%d" % (x, y)) + label = Label(self.tw, text=self.text, justify='left', + background=self.bg, foreground=self.fg, relief='solid', borderwidth=1, + wraplength=self.wraplength) + label.pack(ipadx=1) + + def hidetip(self): + tw = self.tw + self.tw = None + if tw: + tw.destroy() \ No newline at end of file diff --git a/Python3_Projects/Organize_Media/requirements.txt b/Python3_Projects/Organize_Media/requirements.txt new file mode 100644 index 00000000..df39c0ef --- /dev/null +++ b/Python3_Projects/Organize_Media/requirements.txt @@ -0,0 +1,2 @@ +python3-tk +pillow diff --git a/Python3_Projects/Organize_Media/settings/config.json b/Python3_Projects/Organize_Media/settings/config.json index bc367e41..f4c22b9a 100644 --- a/Python3_Projects/Organize_Media/settings/config.json +++ b/Python3_Projects/Organize_Media/settings/config.json @@ -15,5 +15,7 @@ "required_paths": [ "downloads", "media" - ] + ], + "downloads": "C:/Users/JustinGrilli/Documents/TEST OM/downloads", + "media": "C:/Users/JustinGrilli/Documents/TEST OM/media" } \ No newline at end of file diff --git a/Python3_Projects/Organize_Media/settings/general_functions.py b/Python3_Projects/Organize_Media/settings/general_functions.py index 24f80798..c7f42597 100644 --- a/Python3_Projects/Organize_Media/settings/general_functions.py +++ b/Python3_Projects/Organize_Media/settings/general_functions.py @@ -29,10 +29,10 @@ def tv_show_ep_from_file_name(file): episode = None # Search through the file name for the following patterns patterns = [ - # s1e1 or s01e01 - r'[^\w\d](s\d{1,2}e\d{1,2})$|' - r'^(s\d{1,2}e\d{1,2})[^\w\d]|' - r'[^\w\d](s\d{1,2}e\d{1,2})[^\w\d]', + # s1e1 or s01e01 or s01e01e02 + r'[^\w\d](s\d{1,2}e\d{1,2}e?\d{0,2})$|' + r'^(s\d{1,2}e\d{1,2}e?\d{0,2})[^\w\d]|' + r'[^\w\d](s\d{1,2}e\d{1,2}e?\d{0,2})[^\w\d]', # s1 e1 or s01 e01 r'[^\w\d](s\d{1,2}[^\w\d]e\d{1,2})$|' r'^(s\d{1,2}[^\w\d]e\d{1,2})[^\w\d]|' @@ -57,8 +57,9 @@ def tv_show_ep_from_file_name(file): if matches: if patterns.index(pattern) != 4: tv_show_episode = [x for x in matches[0] if x][0] - season, episode = [int(x) for x in re.findall(r'\d+', tv_show_episode)] - return tv_show_episode, season, episode + nums = [int(x) for x in re.findall(r'\d+', tv_show_episode)] + # tv_ep, season, episode(s) + return tv_show_episode, nums[0], nums[1:] if len(nums) > 2 else nums[1] else: matches = [num for num in matches[0] if num and int(num[-2:]) < 30 and num[-2:] != '00' and num not in bad_nums @@ -145,10 +146,8 @@ def get_media_title(tv_show_episode, file_name): r'[a-z:\- ]+[0-9]?[^\d]\([a-z]+\)[^\d]|' r'[a-z:\- ]+[0-9]?[^\d]|[0-9]+[^\w\d]?' r')', clean_file_name, re.IGNORECASE) - year = re.findall(r'19\d\d|20\d\d', clean_file_name) title = re.sub(r'[^a-zA-Z0-9\-]', ' ', title[0]).strip() if title else None - new_file_name = f'{title} ({year[0]})' if year else title if title else None - return new_file_name + return title def rename_all_media_in_directory(media_info): diff --git a/Python3_Projects/Organize_Media/test.py b/Python3_Projects/Organize_Media/test.py deleted file mode 100644 index 7365adcf..00000000 --- a/Python3_Projects/Organize_Media/test.py +++ /dev/null @@ -1,13 +0,0 @@ -import re -from settings.general_functions import initcap_file_name - -clean_file_name = 'x-factor (1999)' -clean_file_name = initcap_file_name(clean_file_name) -print(clean_file_name) -title = re.findall(r'^(' - r'[a-z:\- ]+[0-9]?$|' - r'[a-z:\- ]+[0-9]?[^\d]\([a-z]+\)$|' - r'[a-z:\- ]+[0-9]?[^\d]\([a-z]+\)[^\d]|' - r'[a-z:\- ]+[0-9]?[^\d]|[0-9]+[^\w\d]?' - r')', clean_file_name, re.IGNORECASE) -print(title)