Contents

Downloads Organizer

Updated on 2021-May-18

Project repository

Why

It seems that for most of my life I have been dealing with a quite messy downloads folder. I try to organize it by moving some of the things outside of it. Sometimes I even delete all its contents. Most of the time though, I end up leaving quite a lot of files in there as I realize I might need those later but I have nowhere else to put them.

This is quite a common problem of mine and I decided to look for a solution. I tried looking for some kind of application that manages my downloads or that will organize them into different categories, but this not only implies that I’ll have to use a different application apart from my default file manager, but it may not work on all my devices as it would require a cross-platform application, or searching for a different application for each platform.

This is how I found that an easy way of doing it is using a Python script. This will not only allow me to customize the file organization according to my needs but it will also work on all platforms that support Python (Linux, Windows, MacOS).

Alt text
End results

How

Create a categorization system

We will use the enum module to create our own categorization system. Each enum will correspond to a folder inside of which we define the file types that will be added. We will do this by bundling types of file extensions.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
from enum import Enum

Images     = Enum('Images', 'jpg jpeg png svg gif bmp')
Videos     = Enum('Videos', 'mp4 mkv mov avi wmv mpeg flv')
Archives   = Enum('Archives', 'zip 7z gz xz rar iso')
Documents  = Enum('Documents', 'pdf doc docx ppt pptx xls xlsx rtf odt csv txt')
Installers = Enum('Installers', 'torrent sh deb exe appimage Appimage')
Others     = Enum('Others', '')

type_list = [Images, Videos, Archives, Documents, Installers, Others]

In this example, we have defined 5 categories, but you can go ahead and customize them however you like. On top of these five, we will also add an empty category named ‘Others’ that will provide a folder for the unknown types of files that we may encounter.

Now that we have defined the folders, we will add all the enums in a list that we can easily iterate through.

Info
The order in which you add them to the list doesn’t matter as long as you keep the backup folder (the one we named Others) at the end of our list. The folder at the end of our list will always be the backup folder in which all the unknown files (the ones that our script won’t be able to categorize) will be moved.

Find and move the files

The most important function of the script is organize_files(). In this function we search the directory located at the path provided as a function argument and add all the found files to the list “files”. This will skip over the folders that are located at this location so it will not mess with the already organized files that you have there. Only the files outside of a folder will be affected.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import os
import shutil

def organize_files(path):
    print("==> Organizing files in: {}".format(path))

    # Find all the files in the searched path (this will exclude folders).
    files = [f for f in os.listdir(path) if os.path.isfile(os.path.join(path, f))]
    file_type_variation_list = []
    filetype_folder_dict = {}

    for file in files:
        # Get the file extension
        filetype = file.split('.')[-1]

        # Find the suitable folder for the file
        for type in type_list:
            if (filetype in type._member_names_):
                break

        # Find the folder or create it
        folder_name = os.path.join(path, type.__name__)
        if os.path.isdir(folder_name) == False:
            os.mkdir(folder_name)

        # Move file to folder
        src_path = os.path.join(path, file)
        dest_path = folder_name
        shutil.move(src_path, dest_path)

Once we get all the files, we iterate through the file list and take action for each file.

  • First we take the file extension by splitting the file name using ‘.’ as a delimiter and taking the last string.
  • Now that we have our file extension we iterate through our enum list that we have created earlier and see in which folder our file will be moved. If we iterate through the entire list and don’t manage to find a folder, we use our last enum defined in this list, in our case being Others that we have specifically defined for this scenario.
  • Before moving our file to the destination path, we must verify if the folder already exists and create it in case it doesn’t. An error will occur if we try to move our file to a path that doesn’t exist and our script will close.
  • Finally, we move the file to the destination path using the move function from the shutil module.

Make it cross platform

As we set our goal to make this script work on multiple platforms we need to make sure it works regardless of the operating system it’s running on. While the Python runtime can be installed on all three major platforms (Linux, Windows, MacOS), the way each operating system encodes paths is different. Not only is the format different but they also have different file system hierarchies that we need to account for.

We first need to find the Downloads folder path on each platform:

Linux
/home/{USER}/Downloads

Windows
C:\Users{USER}\Downloads
MacOS
Users/{USER}/Downloads
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
from sys import platform
import getpass

def get_path():
    if platform == "linux" or platform == "linux2":
        print("==> Running on linux")
        path = '/home/{}/Downloads'.format(getpass.getuser())
    elif platform == "darwin":
        print("==> Running on mac")
        path = 'Users/{}/Downloads'.format(getpass.getuser())
    elif platform == "win32":
        print("==> Running on windows")
        path = 'C:\\Users\\{}\\Downloads'.format(getpass.getuser())
    else:
        print("==> Could not determine platform.")
        path = None

    return path

We use the sys.platform function to find the operating system we are running on and define our path accordingly.

Now that we have the paths we need to get the user that launches this script, as most of the times this is where our target folder will be. We do this by using the get getuser function from the getpass module and replace the {USER} string in the path with the result of the function.

Info

For Windows, the paths are written using ‘\’ instead of ‘/’. The backslash is used in most programming languages as an escape character. This means that it is a special character that our program will interpret differently and it will not show in our string. For more information search for escaping character, but the gist of it is that we use a backslash before a special character to specify that we want the special character to be interpreted litteraly.

To solve this problem we can use another backslash to escape the second one like this:

1
path = 'C:\\Users\\{}\\Downloads'

Or we can define the string as a raw string so the characters will be interpreted as they are written:

1
path = r'C:\Users\{}\Downloads'

Both methods will work the same. For this script I have chosen the first one.

Add command line argument

We now have a complete script that will do exactly what we want and it will work on all platforms. But I still think it needs a little addition to make it more versatile.

We have defined a default way in which our script can find a correct path to work in. These are fine most of the time, but sometimes we may have a different directory the we want to organize and we need to cover that aspect as well.

We make the script to check for command line arguments. If an argument is provided it will use that as the path, otherwise it will go and try to find that path on its own using the get_path() function we have created above.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
from sys import argv

def main():
    print("==> Getting folder path.")

    # If an argument is given use that as the path, otherwise check the OS and
    # try to get the correct path
    if len(argv) == 1:
        path = get_path()
    elif len(argv) == 2:
        print("==> Using path provided as argument.")
        path = argv[1]
    else:
        print("==> Too many arguments provided.")
        path = None

    # If a path is found, organize the files in that path
    if path is not None:
        organize_files(path)
        print("==> Success.")
    else:
        print("==> Closing script.")

We check for command line arguments using the system.argv variable. It is a list containing all the arguments given when the script is called.

Important

For example, if we call $ python main.py a the arguments that are stored in the argv list are: ['main.py','a']

This means that the second element in the list is the one we are looking for. If there is only one element in the list it means no argument has been provided.

Automate the script trigger

Now that our script is final we need to call it. We can do so using:

1
python main.py <path>

Or

1
python3 main.py <path>

This is quite simple, but it will get more complicated when the working directory is not the same as the directory where we keep the script. To make the process of calling it easier we will explore two solutions. It is up to you to decide which one you think will work best for you.

Bash alias

The first solution is to simply create a bash alias that will call our script. This is a quick and simple solution. You can call the script easier from anywhere, but you will have to do it manually every time and it will only work on Linux (MacOS should work as well, but I cannot test it).

All you have to do is add the following line to your .bashrc file and then log out and log back in. Make sure to replace the absolute path to your python script.

1
alias organize-downloads="python <path-to-the-script>"

This way you can call your script by simply opening a terminal and typing:

1
organize-downloads

If you need more information I sugest searching how to create an alias on your working environment.

Cronjobs and Task Scheduler

The second solution, and the more elegant one, is to schedule a task that will be triggered at a certain time. This is a bit more complicated than setting a bash alias, but it will provide a seamless integration with your work flow. This will work on all platforms as well.

  • If you are looking for a solution on Linux or MacOS you should checkout how to setup a cronjob.
  • If you are on Windows create a batch file that inside of which you call the python script. Then look for scheduling the execution of this batch fiel using the windows task scheduler.

Conclusion

Our file organizing solution is finished. Using this script you can keep your downloads folder completely organized and make sure you can easily find the file you are looking for.

As an added bonus, the newly downloaded files are always outside the organized folders so that way you can easily see them without searching too much and after you run the script they will be categorized and moved to their own respective folder.

If you want to take a look at the source code you can find it on Github. If you liked it please give it a star or support me directly.