DEV Community

Nicolas Agudelo
Nicolas Agudelo

Posted on

Folder Organizer Using Python and Tkinter

Hello everyone!

Folder Organizer

Folder Organizer

Here is my approach to an application to organize the files you have on any folder on your computer sorting them according to their extension.

This was also the first time I experimented creating a (super simple) user interface on python instead of having the user doing everything on the terminal. I used the module Tkinter to create this window for the user to interact with.

Creating the User Interface

The first step was creating the main window. Using the Tkinter module, we can create an object of the class tkinter.Tk.
In my case, I called it root as most of the examples I found on using this module.

# Creating the main window for the user interface.
root = Tk()
Enter fullscreen mode Exit fullscreen mode

After that, I started personalizing my root object or main window. Changing the default icon for a free personalized icon, I found at freeicons.io (thanks to Ginkaewicons who created the icon.), setting the window geometry and adding a title to it.

# Replace the default icon
try:
    root.iconbitmap('organizer.ico')
except:
    pass

# Setting up the geometry of the window.
root.geometry('320x195')

# By setting both parameters to false the user can not resize the window.
root.resizable(False, False)

# We make sure the program gets the main focus on the user's Desktop.
root.focus()

# Title of the main window
root.title('Organizer.py')

Enter fullscreen mode Exit fullscreen mode

Next, I decided on the widgets I wanted the main window to have.

In my case, I decided to add:

  • A Label that will give information to the user about what the program does, what it is expecting, or when it has finished.
  • A Start Button which will start the primary function of the program.
  • A Progress Bar that gives the user a visual reference that tells him that the program is doing something and when it has finished.
# Creating our start button, which will start the program.

startbutton = tk.Button(
    root,                   # Setting the 'master' window for this widget
    text = 'Start',         # Setting the text to display on this widget
    command = find_dir,     # This will be the function that executes when you press the button.
    cursor='hand2',         # This will change the cursor when the user hovers it over the button.
    )

# A description label to give information to the user about the program status.

description_label = tk.Label(
    root,                   # Setting the 'master' window for this widget
                            # Setting the text to display on this widget
    text = 'You can use this program to organize all your different \ntype of files in folders\n\nJust choose the folder that you want to organize\nby clicking the Start button below',
    justify= 'left'         # With this, every new line will start at the left of the label.
    )

# A progress bar for the user to follow the program's progress.

progressbar = ttk.Progressbar(
    root,                   # Setting the 'master' window for this widget
    orient=HORIZONTAL,      # Setting the progress bar horizontally
    length = 240,           # Setting the lenght of the progress bar in pixels
    mode = 'determinate'    # We know when our program finishes so we use the determinate mode
    )
Enter fullscreen mode Exit fullscreen mode

You can find information about all of the attributes of these and more widgets here.

Finally, I placed the objects in the main window:

#Placing the objects we created for our user interface.

description_label.place(x=4, y= 5)
startbutton.place(x=140, y= 140)
progressbar.pack(side='bottom', pady=5)
Enter fullscreen mode Exit fullscreen mode

Since the user can not resize the window, I placed the label and button at specific positions using the x and y axes. For the progress bar, I put it at the bottom of the window, adding a 5px padding on top and bottom to avoid having it entirely on the window's border.

After doing this we get our main window:

Main Window

Main Window

Pressing the Start Button

Now that the main window is done the start button will be the one responsible of doing what the main goal of the program is. Once it is pressed it will call the find_dir() function which will prompt the user about which folder he wants to organize using the filedialog method from the Tkinter module.
# The find dir function will ask the user for the folder he wants to organize. 
# In case he presses 'Cancel', the `dirname` variable will be empty
# In this case, we ask the user to please select a folder to be able to organize it
def find_dir():
    global description_label
    frm = ttk.Frame(root, padding=10)
    ttk.Label(frm, text="Select the folder that you want to organize")
    dirname = filedialog.askdirectory(parent=root, initialdir='~', title='Select the folder that you want to organize')
    # If the user closes the file dialog or presses cancel the dirname will be empty in this case, we change the text on the label of the main window and do nothing else.
    if dirname == '':
        description_label['text'] = 'Please select a folder to continue\n\nJust choose the folder that you want to organize\nby clicking the Start button below'
    else:
    # Once we have the directory that the user wants to organize we pass it to the function organize()
        organize(dirname)
Enter fullscreen mode Exit fullscreen mode

Selecting the Folder

Selecting the Folder

If the user closes the window asking for the folder or presses 'Cancel' the program will return to the main window and inform him that a folder is needed for the program to work allowing him to press the start button to start the process again.

Not Selecting the Folder

Message displayed when no folder was selected

After that, if the user selected a folder, two things could happen:

  1. There are no files on the selected folder, in which case the program will inform the user about it and give him the option to press the start button again if he wants to.

    Folder with no files

    Message displayed when a folder with no files was selected

  1. There are files on the selected folder in which case the program will call the organize() function which will then organize all the files into folders.

Folder Organized Message


Folder Organized Message

The organize() function

The organize() function is in charge of putting every file in the correct folder. To achieve this, I created different categories of files, and using match case, I assigned every file to a folder. In case there is an extension that doesn't fall into any of the categories I created, the program will put it into a folder called Others

I won't put the portion of the code that does this here because it's a little bit too long, but you can check the code here to see the categories I created and what extension falls into each category.

Thanks to file-extensions.org, the place from where I got lists of the most common file extensions and got the idea of how to name each category.

Installing the Application

You can find the code for the application at this repository at GitHub.

You can also download the executable on this link

Closing notes

Feel free to contact me through my email (public on my profile) if you find any issue or problem with the application or if you have any advice about how something might be better implemented. You can also try adding more stuff to the application; I would love to see that.

Thanks.

Top comments (6)

Collapse
 
geraldew profile image
geraldew • Edited

Cool, and thanks for sharing this.

I also found myself with a particular need for a file tool and took the path of writing one in Python and turned to Tkinter when I wanted a GUI for it. It's also GPL3 and can be found at Foldatry. It was also my first ever Python program so it started small.

What your does is something I've occasionally needed so I'll be sure to give it a try next time that comes up.

p.s. a first try at running it failed as it appears to require Python version 3.10 - for the use of match case

Collapse
 
nicolasagudelo profile image
Nicolas Agudelo

Hi geraldew, thanks for your comment!

Yes, you are right for match case to work, you need to have python 3.10 at least.

However, you can also download the zip file with the executable only here if you just want to use the program. In that case, it should work without issues.

Do let me know if you find any issue when using it so I can check that out.

I also had a look at your repository, and it seems like an excellent app with much more functionalities than mine haha.

Thanks again!

Collapse
 
geraldew profile image
geraldew

Well, I didn't feel like changing my Python version (that's a whole other topic) so I figured I'd just rewrite the match case construct to the older style.

Except, when I noticed all the logical Or symbols I was prompted to re-imagine how I would handle that sort of thing. I decided that I preferred the run-time handling to be just looking into a dictionary rather than doing a cascade of boolean comparisons.

So after some looking at other code I'd written here's what I came up with.

The prep work is to define a look-up resource - implemented as instructions for making two layers of dictionary.

from enum import Enum, unique, auto

@unique
class TypeOfFile(Enum):
    Document = auto() 
    Audio = auto() 
    Video = auto() 
    Picture = auto() 
    Executable = auto()
    Graphic2D = auto()
    Graphic3D = auto()
    Font = auto()
    Text = auto()
    Compressed = auto()
    DiskImage = auto()
    MobilePhone = auto()
    Databases = auto()

def SubFolder_For_TypeOfFile( tof):
    if tof in [ TypeOfFile.Document ]:
        return '/Documents/'
    elif tof in [ TypeOfFile.Audio ]:
        return '/Audio Files/'
    elif tof in [ TypeOfFile.Video ]:
        return '/Video Files/'
    elif tof in [ TypeOfFile.Picture ]:
        return '/Images/'
    elif tof in [ TypeOfFile.Executable ]:
        return '/Executable Files/'
    elif tof in [ TypeOfFile.Graphic2D ]:
        return '/Graphic Files/'
    elif tof in [ TypeOfFile.Graphic3D ]:
        return '/3D Graphics/'
    elif tof in [ TypeOfFile.Font ]:
        return '/Font Files/'
    elif tof in [ TypeOfFile.Text ]:
        return '/Text Files/'
    elif tof in [ TypeOfFile.Compressed ]:
        return '/Compressed Files/'
    elif tof in [ TypeOfFile.DiskImage ]:
        return '/Disk Images/'
    elif tof in [ TypeOfFile.MobilePhone ]:
        return '/Mobile Phone Related Files/'
    elif tof in [ TypeOfFile.Databases ]:
        return '/Databases Files/'

def Extensions_For_TypeOfFile( tof):
    if tof in [ TypeOfFile.Document ]:
        return [ '.abw','.aww','.chm','.cnt','.dbx','.djvu','.doc','.docm','.docx','.dot','.dotm','.dotx','.epub','.gp4','.ind','.indd','.key','.keynote','.mht','.mpp','.odf','.ods','.odt','.opx','.ott','.oxps','.pages','.pdf','.pmd','.pot','.potx','.pps','.ppsx','.ppt','.pptm','.pptx','.prn','.ps','.pub','.pwi','.rtf','.sdd','.sdw','.shs','.snp','.sxw','.tpl','.vsd','.wpd','.wps','.wri','.xps','.numbers','.ods','.sdc','.sxc','.xls','.xlsm','.xlsx' ]
    elif tof in [ TypeOfFile.Audio ]:
        return [ '.3ga','.aac','.aiff','.amr','.ape','.arf','.asf','.asx','.cda','.dvf','.flac','.gp4','.gp5','.gpx','.logic','.m4a','.m4b','.m4p','.midi','.mp3','.ogg','.opus','.pcm','.rec','.snd','.sng','.uax','.wav','.wma','.wpl','.zab' ]
    elif tof in [ TypeOfFile.Video ]:
        return [ '.264','.3g2','.3gp','.ard','.asf','.asx','.avi','.bik','.dat','.dvr','.flv','.h264','.m2t','.m2ts','.m4v','.mkv','.mod','.mov','.mp4','.mpeg','.mpg','.mts','.ogv','.prproj','.rec','.rmvb','.swf ','.tod','.tp','.ts','.vob','.webm','.wlmp','.wmv' ]
    elif tof in [ TypeOfFile.Picture ]:
        return [ '.bmp','.cpt','.dds','.dib','.dng','.emf','.gif','.heic','.ico','.icon','.jpeg','.jpg','.pcx','.pic','.png','.psd','.psdx','.raw','.tga','.thm','.tif','.tiff','.wbmp','.wdp','.webp' ]
    elif tof in [ TypeOfFile.Executable ]:
        return [ '.air','.app','.application','.appx','.bat','.bin','.com','.cpl','.deb','.dll','.elf','.exe','.jar','.js' ]
    elif tof in [ TypeOfFile.Graphic2D ]:
        return [ '.abr','.ai','.ani','.cdt','.djvu','.eps','.fla','.icns','.ico','.icon','.mdi','.odg','.pic','.psb','.psd','.pzl','.sup','.vsdx','.xmp' ]
    elif tof in [ TypeOfFile.Graphic3D ]:
        return [ '.3d','.3ds','.c4d','.dgn','.dwfx','.dwg','.dxf','.ipt','.lcf','.max','.obj','.pro','.skp','.stl','.u3d','.x_t' ]
    elif tof in [ TypeOfFile.Font ]:
        return [ '.eot','.otf','.ttc','.ttf','.woff' ]
    elif tof in [ TypeOfFile.Text ]:
        return [ '.1st','.alx','.application','.asp','.csv','.htm','.html','.log','.lrc','.lst','.md','.nfo','.opml','.plist','.reg','.rtf','.srt','.sub','.tbl','.text','.txt','.xml','.xmp','.xsd','.xsl','.xslt','.ini' ]
    elif tof in [ TypeOfFile.Compressed ]:
        return [ '.001','.002','.003','.004','.005','.006','.007','.008','.009','.010','.7z','.7z.001','.7z.002','.7z.003','.7z.004','.7zip','.a00','.a01','.a02','.a03','.a04','.a05','.ace','.air','.appxbundle','.arc','.arj','.bar','.bin','.c00','.c01','.c02','.c03','.cab','.cbr','.cbz','.cso','.deb','.dlc','.gz','.gzip','.hqx','.inv','.isz','.jar','.msu','.nbh','.pak','.part1.exe','.part1.rar','.part2.rar','.pkg','.pkg','.r00','.r01','.r02','.r03','.r04','.r05','.r06','.r07','.r08','.r09','.r10','.rar','.rpm','.sit','.sitd','.sitx','.tar','.tar.gz','.tgz','.uax','.vsix','.webarchive','.z01','.z02','.z03','.z04','.z05','.zab','.zip','.zipx' ]
    elif tof in [ TypeOfFile.DiskImage ]:
        return [ '.000','.ccd','.cue','.daa','.dao','.dmg','.img','.img','.iso','.mdf','.mds','.mdx','.nrg','.tao','.tc','.toast','.uif','.vcd' ]
    elif tof in [ TypeOfFile.MobilePhone ]:
        return [ '.apk','.asec','.bbb','.crypt','.crypt14','.ipa','.ipd','.ipsw','.lqm','.mdbackup','.nbh','.nomedia','.npf','.pkpass','.rem','.rsc','.sbf','.sis','.sisx','.spd','.thm','.tpk','.vcf','.xap','.xapk' ]
    elif tof in [ TypeOfFile.Databases ]:
        return [ '.accdb','.accdt','.csv','.db','.dbf','.fdb','.gdb','.idx','.mdb','.mdf','.sdf','.sql','.sqlite','.wdb' ]

def make_ext_lookups():
    # make a lookup dictionary by TypeOfFile with each one's subfolder name
    dct_filetype_subfolder = {}
    for tof in TypeOfFile:
        dct_filetype_subfolder[ tof ] = SubFolder_For_TypeOfFile( tof)
    # make the extension list
    dct_extensions = {}
    for tof in TypeOfFile:
        for ext in Extensions_For_TypeOfFile( tof):
            if ext in dct_extensions:
                print( "Ignoring multiple use of " + ext + " for " + SubFolder_For_TypeOfFile( tof) + " is already in " +  dct_filetype_subfolder[ dct_extensions [ ext ] ])
            else:
                dct_extensions[ ext ] = tof
    return dct_filetype_subfolder, dct_extensions
Enter fullscreen mode Exit fullscreen mode

I used an enumeration as the link between the two - in effect this is a translation of your various case groups.

Then, to the top of:

def organize(directory):
Enter fullscreen mode Exit fullscreen mode

I added a line:

    dct_filetype_subfolder, dct_extensions = make_ext_lookups()
Enter fullscreen mode Exit fullscreen mode

so that constructs an instance of the nested dictionaries.

Then, instead of your match structure, I do:

        # replace use of match with a two-level dictionary lookup
        if ext in dct_extensions:
            move_files( directory, file, dct_filetype_subfolder[ dct_extensions [ ext ] ] )
        else:
            move_files(directory, file, '/Others/')
Enter fullscreen mode Exit fullscreen mode

By the way, inside def make_ext_lookups(): I added a check to tell me if I'd miskeyed when I adapted the extension lists. This was done with if ext in dct_extensions: and the print that it does.

I wasn't actually expecting that to show anything, but as it happened, it did - printing the following:

Ignoring multiple use of .ods for /Documents/ is already in /Documents/
Ignoring multiple use of .gp4 for /Audio Files/ is already in /Documents/
Ignoring multiple use of .asf for /Video Files/ is already in /Audio Files/
Ignoring multiple use of .asx for /Video Files/ is already in /Audio Files/
Ignoring multiple use of .rec for /Video Files/ is already in /Audio Files/
Ignoring multiple use of .djvu for /Graphic Files/ is already in /Documents/
Ignoring multiple use of .ico for /Graphic Files/ is already in /Images/
Ignoring multiple use of .icon for /Graphic Files/ is already in /Images/
Ignoring multiple use of .pic for /Graphic Files/ is already in /Images/
Ignoring multiple use of .psd for /Graphic Files/ is already in /Images/
Ignoring multiple use of .application for /Text Files/ is already in /Executable Files/
Ignoring multiple use of .rtf for /Text Files/ is already in /Documents/
Ignoring multiple use of .xmp for /Text Files/ is already in /Graphic Files/
Ignoring multiple use of .air for /Compressed Files/ is already in /Executable Files/
Ignoring multiple use of .bin for /Compressed Files/ is already in /Executable Files/
Ignoring multiple use of .deb for /Compressed Files/ is already in /Executable Files/
Ignoring multiple use of .jar for /Compressed Files/ is already in /Executable Files/
Ignoring multiple use of .pkg for /Compressed Files/ is already in /Compressed Files/
Ignoring multiple use of .uax for /Compressed Files/ is already in /Audio Files/
Ignoring multiple use of .zab for /Compressed Files/ is already in /Audio Files/
Ignoring multiple use of .img for /Disk Images/ is already in /Disk Images/
Ignoring multiple use of .nbh for /Mobile Phone Related Files/ is already in /Compressed Files/
Ignoring multiple use of .thm for /Mobile Phone Related Files/ is already in /Images/
Ignoring multiple use of .csv for /Databases Files/ is already in /Text Files/
Ignoring multiple use of .mdf for /Databases Files/ is already in /Disk Images/
Enter fullscreen mode Exit fullscreen mode

So you might want to check your source code for similar double presences in your case lines.

Anyway, now that I have a variant that runs of my older Python (which will be just whatever is installed on Xubuntu 20.04) - it seems to work nicely.

I don't know that I like "Others" as a folder name for unrecognised things - I'd prefer something either alphabetically before or after all the rest.

Thread Thread
 
nicolasagudelo profile image
Nicolas Agudelo

Hi geraldew,

It took me a moment to understand how your solution was working since I'm still learning a lot of new stuff and had not used enum before but after testing for a while with the debugger I understood how you solved the sorting using enum and dictionaries. I wanted to ask you, does this make the code run more efficiently? or was it just your workaround to not use match case? I'm not sure yet how to test when a code is more or less efficient so I would appreciate if you could tell me if there is a difference in performance with one approach or the other.

Regarding the duplicates I did have a look and found out that the source from where I got the extensions list does have some extensions listed in multiple categories which is why your code found the use of the same extension on different categories. I guess I'll have to manually decide to which categories I want those extensions to be sorted out.

Thanks again!

Thread Thread
 
geraldew profile image
geraldew

Well, I certainly made the change to not use match case.

My main reaction though was because I don't like seeing so much data-like material being hard coded. Where I can, I like to move that kind of thing into a data structure. This often has the advantage of making the code simpler at the point of decision.

But another advantage is that it prepares the ground for maybe loading that decision data from a config file, say from a JSON file. That way, fine tuning of what the program does can be done without rewriting it.

As for which is more "efficient" that might depend on quite what meaning you want for that.

As you had a match case construct, answering that will partly depend on quite how that gets implemented under the hood, i.e. by CPython. Double-guessing (or even checking the CPython source code) seems to be a popular game in some quarters. My personal view is that if that's a worry then its probably time to code in something other than Python. It is an interpreted scripting language after all.

Anyway, I wasn't really comparing to the match case, rather to the complex of if and elif blocks I would need and also the number of Or operators - just to replace what you had.

When I see a lot of Or usage I tend to remember that the speed of the operation becomes quite variable depending on the data. It was that thought of variance that prompted me to think that for each distinct extension, we (the programmers) already know which categorisation should be used. So how do we express that best in Python? Well in short, using a known value to get another known related value is what dictionaries are good at.

I strongly suspect that the dictionary lookup is faster than a lot of cascading conditional logic operators, but it does beg the question of how dictionaries are handled by CPython.

For that matter, I could have constructed a dictionary that directly mapped from extension to sub-folder - rather than the double-dictionary method that I first wrote. Thus are the many, many options of tackling these things.

As for when to construct those dictionaries, that comes down to knowing the scope and lifespan of the program. As a quick thing to do, I put that construction step - that is, calling the function to do it - inside the def organize(directory). But the way your program currently works, it could have been done outside that, thereby only be done once to cover all runs of organize. I was just too lazy to work which way to do that: make it global, pass it in as a control parameter etc.

BTW another thing that the as-data approach enables is having alternate dictionaries to pass to the organise function. For example, there could be some stock but varied combination for the user to select among.

Thread Thread
 
nicolasagudelo profile image
Nicolas Agudelo • Edited

Hi geraldew,

Sorry for the delay I was offline for a few days, yes I can see what you mean by loading the decision from a file by doing it using data structures instead of hard coding it and re-writing the code each time you want to add or remove something.

As for the efficiency topic I was asking more because right now I'm reading about time and space complexity and was curious if may be this was something that played a role on why you decided to do it like that. If I'm not mistaken python uses a garbage collector so space complexity is out of the hands of the programmer (I think) but I'm still not sure how to know when a program will be more or less time complex.

Thanks for your comments you have shared some really interesting stuff ^^