15
\$\begingroup\$

I'm making a file management project that I eventually want to be automated later in the future (once I learn how to). What it does is take files from my download folder and move those files to the appropriate folder. i.e pictures to the picture folder.

Before I go any further, are there any hidden improvements that you guys may see? It's my first month being a self-taught computer programmer.

# list of some possible file extentions for pics sound, sounds and video
import os
import shutil

# only works with tuples not list (be carefull when adding and updating new formats)
picture_format = (".png",".jpg",".JPG",".jpeg",".jpe",".jif",".jfif",".jfi",".gif",".webp",".tiff",".tif",".psd",".raw", ".arw", ".cr2", ".nrw", ".k25",".bmp", 
                    ".dib",".heif", ".heic",".ind", ".indd", ".indt",".jp2", ".j2k", ".jpf", ".jpx", ".jpm", ".mj2",".svg", ".svgz", ".CR2")
audio_format = (".mp3",".acc",".wma")
video_format =(".mp4",".mov",".wmv",".avi","avchd",".mkv")
download_folder = "C:/Users/Peter/Downloads"

def picture_management():
    source_path = os.listdir(download_folder)
    destination = "C:\\Users\\Peter\\Pictures\\DOWNLOADED PICTURES"
    for file in source_path:
        if file.endswith(picture_format):
            shutil.move(os.path.join(download_folder,file),os.path.join(destination,file))

def video_management():
    source_path = os.listdir(download_folder)
    destination = "C:\\Users\\Peter\\Videos"
    for file in source_path:
        if file.endswith(video_format):
            shutil.move(os.path.join(download_folder,file),os.path.join(destination,file))

def audio_management():
    source_path = os.listdir(download_folder)
    destination = "C:\\Users\\Peter\\Music"
    for file in source_path:
        if file.endswith(audio_format):
            shutil.move(os.path.join(download_folder,file),os.path.join(destination,file))


picture_management()
video_management()
audio_management()
\$\endgroup\$
2
  • \$\begingroup\$ I would use a library for searching the type of the file via its mime type \$\endgroup\$ Commented Sep 30, 2022 at 6:57
  • \$\begingroup\$ here is my attempt at organizing my files when I started learning programming (github.com/prashantsengar/ArrangePy). It later got attention from other uni students who helped improve the code. Looks like organizing files is a popular requirement... \$\endgroup\$ Commented Oct 4, 2022 at 6:17

4 Answers 4

18
\$\begingroup\$

I'm sure Peter is a nice guy, but you shouldn't write programs only for him. Use a parametric home directory.

Some of your extensions support either case and some don't. Why not make them all support either case?

avchd is missing a dot prefix.

Factor out a common method to manage your files based on a common set of destination folder and extension properties.

Use pathlib.

Suggested

from pathlib import Path
from typing import Iterator


class Format:
    def __init__(self, dest: str, extensions: set[str]) -> None:
        self.dest = Path.home() / dest
        self.matches = extensions.__contains__

    @property
    def matching_files(self) -> Iterator[Path]:
        for file in download_folder.iterdir():
            if file.is_file() and self.matches(file.suffix.lower()):
                yield file

    def manage(self) -> None:
        for file in self.matching_files:
            file.rename(self.dest / file.name)


PICTURES = Format(
    'Pictures/DOWNLOADED PICTURES',
    {
        '.png', '.jpg', '.jpeg', '.jpe', '.jif', '.jfif', '.jfi', '.gif', '.webp', '.tiff', '.tif', '.psd', '.raw',
        '.arw', '.cr2', '.nrw', '.k25', '.bmp', '.dib', '.heif', '.heic', '.ind', '.indd', '.indt', '.jp2', '.j2k',
        '.jpf', '.jpx', '.jpm', '.mj2', '.svg', '.svgz', '.cr2',
    },
)
AUDIO = Format(
    'Music',
    {'.mp3', '.acc', '.wma'},
)
VIDEO = Format(
    'Videos',
    {'.mp4', '.mov', '.wmv', '.avi', '.avchd', '.mkv'},
)
download_folder = Path('~/Downloads').expanduser()


if __name__ == '__main__':
    PICTURES.manage()
    AUDIO.manage()
    VIDEO.manage()
\$\endgroup\$
6
  • 1
    \$\begingroup\$ I was thinking of doing that as well, just haven't googled how to do that part :D. I really appreciate it \$\endgroup\$ Commented Sep 29, 2022 at 23:05
  • 3
    \$\begingroup\$ Indeed, I'd say I am a nice guy, but I don't use Windows. Thanks for writing a program for me, though, @Tam-FilsSalomon >.< :P \$\endgroup\$ Commented Sep 30, 2022 at 2:26
  • \$\begingroup\$ Nice improvement suggestion. One further improvement - it would avoid a lot of repetition to remove the dots from the listed file extensions \$\endgroup\$
    – Woodz
    Commented Sep 30, 2022 at 7:40
  • 1
    \$\begingroup\$ I find the idea of storing extensions.__contains__ instead of just extensions problematic: it is suddenly impossible to enumerate the list of extensions given a Format. \$\endgroup\$ Commented Sep 30, 2022 at 14:52
  • 2
    \$\begingroup\$ @Reinderien: A frozenset would allow enumeration without modification. \$\endgroup\$ Commented Oct 1, 2022 at 10:54
9
\$\begingroup\$

PEP-8

This already looks very readable. You use snake_case as per PEP-8 for variables and functions. However, the global constants should be in UPPER_SNAKE_CASE. Also items in sequences should be separated by a comma and a following space. Furthermore, prefer docstrings over comments.

Use fitting data structures

Currently, to match the file suffixes, you iterate over the tuples you defined as constants. Instead, define the groups of suffixes as sets. Then you can do an optimized if suffix in group: check on them.

Iterate once

Currently you iterate over the source_path three times, once for each media type. Just do it once and use a branch condition to determine the media type.

Suggested

"""Sort downloaded images, audio and video files."""

from pathlib import Path


FILE_DESTINATIONS = {
    'Music': {'.mp3', '.acc', '.wma'},
    r'Pictures\DOWNLOADED PICTURES': {
        '.png', '.jpg', '.JPG', '.jpeg', '.jpe', '.jif', '.jfif', '.jfi',
        '.gif', '.webp', '.tiff', '.tif', '.psd', '.raw', '.arw', '.cr2',
        '.nrw', '.k25', '.bmp', '.dib', '.heif', '.heic', '.ind', '.indd',
        '.indt', '.jp2', '.j2k', '.jpf', '.jpx', '.jpm', '.mj2', '.svg',
        '.svgz', '.CR2'
    },
    'Videos': {'.mp4', '.mov', '.wmv', '.avi', '.avchd', '.mkv'}
}


def main():
    """Sort downloaded multimedia files."""

    home = Path.home()

    for item in (home / 'Downloads').iterdir():
        if not item.is_file():
            continue

        for dst, suffixes in FILE_DESTINATIONS.items():
            if item.suffix in suffixes:
                item.rename(home / dst / item.name)
                break


if __name__ == '__main__':
    main()

Update: Generalization for $HOME stolen from @Reinderien.

\$\endgroup\$
1
  • \$\begingroup\$ oh, I didn't know globals should be all upper case. I'll keep that in mind, thanks \$\endgroup\$ Commented Sep 29, 2022 at 23:08
7
\$\begingroup\$

1. Coding style

There's a style sheet called PEP8 ("How to write beautiful code with PEP 8") that is strongly recommended for any Python programmer. Your code violates a few of the principles outlined there: PEP8 recommends a blank after a comma, a line length of 79 characters, and two blank lines between two function declarations.

You can check your Python code either using the command-line tool pycodestyle or one of the available online checkers.

2. Case handling of file extensions

When you create your three tuples with file extensions, you basically assume that they will occur all in lower case – with exceptions for .JPG and .CR2, which you include in your tuples in the upper case version as well. You could extend your tuples to include upper case versions for all extensions as well, but you'll still run into problems for files like Beautiful_Picture.Png that use mixed case for the file name (the internet is a chaotic place, and I'm sure that file names like this do exist). Probably the best way to make sure that your code handles these exceptions is by first converting the file name to lower case by using the lower() method, and then doing the check with endswith(), like so:

if file.lower().endswith(picture_format):
    print("This is probably a picture!")

3. Path management

Dealing with folder names can be a bit of a bother, not only in Python, but in any programming language (you could probably write a short book on the historical reasons and their contemporary implications). Your code alternates between using slashes /, and double backslashes \\. Python may be smart enough to sort this out, but as soon as your code is intended to run on multiple systems (e.g. not only Windows, but also macOS or Linux) or with users whose user name is not Peter, you need to be particularly careful how you specify paths.

Your code uses os.path.join(), which is actually not a bad idea at all, as this and the other functions from the os.path module help you making your file handling portable with regard to different user names and different operating systems. The Path class from the pathlib module is another approach that is even more powerful.

The first important class method provided by Path is home(). This method returns a path object that points toward the home directory of the current user:

from pathlib import Path

print(Path.home())

The output is probably something like WindowsPath("c:/Users/Peter"), assuming that your current user is Peter.

The next useful property of Path is that it defines an operator that allows you to concatenate path components, which is functionally very similar to the os.path.join() command that you already know about. The operator is / (i.e. a slash character), but it is transformed into whatever character your operating system handles. Thus, if you want to produce a path to the folder Downloads in the home directory of the current user Peter, you can do it like so:

from pathlib import Path

download_folder = Path.home() / "Downloads"
print(download_folder)

This should give you WindowsPath("c:/Users/Peter/Downloads"). You can use variants of this command to create the file paths in your three management functions. The Path class also provides iterdir() as an alternative to os.listdir(), and it provides the file name and the extension as a name and suffix property, respectively, so these features also open up room for improvement.

4. Boilerplate code

Your program contains what is often called "boilerplate code" – repetitions of the same code fragments that could be simplified to reduce redundancies. In your case, the three functions picture_management(), video_management(), and audio_management() may be considered boilerplate code because they contain the same commands, but differ only in some parameters. The advantage of removing boilerplate code is that your program may become shorter, easier to extend, more flexible, and that improvements need to be applied only in one place instead of, in your case, three places.

So, instead of having separate functions for each file type, why not have a single function that does the same job depending on the arguments that you provide? Here's what this would look like for the picture management part:

def manage_files(source_path, target_path, extensions):
    for source_file in source_path.iterdir():
        if source_file.suffix.lower() in extensions:
            target_file = Path(target_path / source_file.name)
            shutil.move(source_file, target_file)

manage_files(download_folder, 
             Path.home() / "Pictures" / "DOWNLOADED PICTURES",
             picture_format)

5. My version

There are a few more things that I can think of to improve your code. Here's my version. For one, I'm making use of the if __name__ == "__main__" idiom that allows you to run the script from the command line. I've also reduced some more boilerplate code by looping over the manage_files() function that I've suggested above, and I've cleaned up the code in agreement with PEP8. Have a look:

import shutil
from pathlib import Path


EXT_AUDIO = (".mp3", ".acc", ".wma")
EXT_VIDEO = (".mp4", ".mov", ".wmv", ".avi", ".avchd", ".mkv")
EXT_PICTURE = (
    ".png", ".jpg", ".jpeg", ".jpe", ".jif", ".jfif", ".jfi", ".gif", ".webp", 
    ".tiff", ".tif", ".psd", ".raw", ".arw", ".cr2", ".nrw", ".k25", ".bmp", 
    ".dib", ".heif", ".heic", ".ind", ".indd", ".indt", ".jp2", ".j2k", 
    ".jpf", ".jpx", ".jpm", ".mj2", ".svg", ".svgz")


def manage_files(source_path, target_path, extensions):
    for source_file in source_path.iterdir():
        if source_file.suffix.lower() in extensions:
            target_file = Path(target_path / source_file.name)
            shutil.move(source_file, target_file)

    
def main():
    home = Path.home()
    source_path = home / "Downloads"
    for target_path, extensions in (
            (home / "Music", EXT_AUDIO),
            (home / "Videos", EXT_VIDEO),
            (home / "Pictures" / "DOWNLOADED PICTURES", EXT_PICTURE)):
        manage_files(source_path, target_path, extensions)
        

if __name__ == "__main__":        
    main()
\$\endgroup\$
6
\$\begingroup\$

Adding to all answers, you have a really nasty bug:
ALL folders are hardcoded.

The problem

Windows lets you move the location of the following folders:

  • 3D Objects
  • Contacts
  • Desktop
  • Documents
  • Downloads
  • Favorites
  • Links
  • Music
  • Pictures
  • Saved games
  • Searches
  • Videos

In my case, I have the Pictures, Videos and Downloads folders in a different drive, with a different path.

Your code could break or move files into unexpected locations.

Additionally, some (all?) Linux distros seem to rename the folders to the locale you've chosen for your PC.
It is possible to force it to stay in English:
https://askubuntu.com/questions/398555/change-the-system-folder-name-back-to-english
However, this isn't guaranteed.

The solution

To get the Music, Pictures and Videos folder's locations, you can use the win32com.shell module.

from win32com.shell import shell, shellcon

# X:/some/path/to/Music
print(shell.SHGetFolderPath(0, shellcon.CSIDL_MYMUSIC, None, 0))

# X:/some/path/to/Pictures
print(shell.SHGetFolderPath(0, shellcon.CSIDL_MYPICTURES, None, 0))

# X:/some/path/to/Videos
print(shell.SHGetFolderPath(0, shellcon.CSIDL_MYVIDEO, None, 0))

Taken from: https://stackoverflow.com/questions/3858851/how-to-get-windows-special-folders-for-currently-logged-in-user

To get the Downloads folder location, on Windows, it's an absolute mess:

import winreg

with winreg.OpenKey(winreg.HKEY_CURRENT_USER, "SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\Shell Folders") as key:
    print(winreg.QueryValueEx(key, "{374DE290-123F-4565-9164-39C4925E467B}")[0])

Taken from: https://www.reddit.com/r/learnpython/comments/4dfh1i/how_to_get_the_downloads_folder_path_on_windows/

My opinion...

Personally, I would abstract this away into a different module.

You could take the same approach as some of the answers in: https://stackoverflow.com/questions/35851281/python-finding-the-users-downloads-folder

The (partial) re-write

This is how I would write that abstraction.

paths.py

import os

if os.name == 'nt':
    import winreg
    from win32com.shell import shell, shellcon
else:
    import subprocess

class Paths:
    _downloads: str = None
    _pictures: str = None
    _music: str = None
    _videos: str = None
    
    
    def __get_linux_path(self, name: str):
        name = name.upper()
        defaults = {
            "DOWNLOAD": "~/Downloads",
            "PICTURES": "~/Pictures",
            "MUSIC": "~/Music",
            "VIDEOS": "~/Videos"
        }
        
        # https://stackoverflow.com/a/72591013
        try:
            return subprocess.run(["xdg-user-dir", name], capture_output=True, text=True).stdout.strip("\n")
        except FileNotFoundError:
            return os.path.expanduser(defaults[name])
    
    def __get_win_path(self, name: str):
        # https://stackoverflow.com/questions/3858851/how-to-get-windows-special-folders-for-currently-logged-in-user
        # Check all values in a Python REPL:
        #   for _ in csidl:
        #       try:
        #           print(f"{_}: {shell.SHGetFolderPath(0, getattr(shellcon, _), None, 0)}")
        #       except Exception:
        #           print(_)
        return shell.SHGetFolderPath(0, getattr(shellcon, f"CSIDL_MY{name.upper()}"), None, 0)
    
    
    def get_downloads(self):
        if self._downloads is None:
            if os.name == 'nt':
                # https://www.reddit.com/r/learnpython/comments/4dfh1i/how_to_get_the_downloads_folder_path_on_windows/
                with winreg.OpenKey(winreg.HKEY_CURRENT_USER, "SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\Shell Folders") as key:
                    self._downloads = winreg.QueryValueEx(key, "{374DE290-123F-4565-9164-39C4925E467B}")[0]
            else:
                self._downloads  = self.__get_linux_path("download")
        
        return self._downloads
    
    
    def get_pictures(self):
        if self._pictures is None:
            self._pictures = self.__get_win_path("pictures") if os.name == "nt" else self.__get_linux_path("pictures")
        
        return self._pictures
    
    
    def get_music(self):
        if self._music is None:
            self._music = self.__get_win_path("music") if os.name == "nt" else self.__get_linux_path("music")
        
        return self._music
    
    
    def get_videos(self):
        if self._videos is None:
            self._videos = self.__get_win_path("video") if os.name == "nt" else self.__get_linux_path("videos")
        
        return self._videos

Usage

You can use it like this:

from paths import Paths

paths = Paths()

print(paths.get_downloads())
print(paths.get_pictures())
print(paths.get_music())
print(paths.get_videos())

Why I wrote this way?

I decided to write this way because I wanted to cache the results you get.

For Windows, you need to run something that does an unknown amount of work.
For Linux and others, you have to, at least, run an external process.
That process can fail, and in which case, the path is expanded from a sane default.

Anytime you run a function, it will automatically store the results, which means, you can call the same function 100 times and it only does the "expensive" stuff once.

Additional notes

You're missing a few common formats:

Video

You're missing:

  • .webm
  • .3gp
  • .3g2

Audio

You're missing:

  • .ogg
  • .mpa
  • .m3u (Playlist file)
  • .flac
  • .amr (Old phone's recording format)
  • .3ga

Image

You're missing:

  • .apng
\$\endgroup\$
2
  • 1
    \$\begingroup\$ Thanks for the update. This is a lot of information, I will definitely implement them once i understand it clearly \$\endgroup\$ Commented Oct 1, 2022 at 15:27
  • \$\begingroup\$ @Tam-FilsSalomon You're welcome. If there's any specific part that you have doubts or questions about, just ask and I will answer. \$\endgroup\$ Commented Oct 1, 2022 at 15:37

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.