Adding a file/folder comment capability to Windows

This is something I wrote a few years ago in vb.Net and just recently ported to python/wxpython. As I keep discovering, just about everything is easier in python.

Windows does not have a commenting facility so I decided to write something simple. NTFS supports something called Alternate Data Streams (ADS). For example, when you write text to a file named myfile.txt, the text goes where you would expect. But you can create an ADS for that file by appending a colon and a name to the file name. So you can write text to the ADS myfile.txt:myads and the text, rather than being stored in the file, is stored as a file attribute.

Technical note - the file text is also a data stream but with a name of null so any named data stream is an alternate data stream. Confused yet?

Anyway, since you can create any name for an ADS, I used the name comment. When I want to create a comment for a file or folder I just add :comment and write to that.

The commenting utility consists of four parts:

  1. The commenting code
  2. The GUI code
  3. The registry patch
  4. The IMDB code (optional)

The actual comment interface code consists of four methods

  1. hasComment(item)
  2. getComment(item)
  3. setComment(item, comment)
  4. deleteComment(item)

I don't think explanation is necessary. Here is the code for comment.py

"""                                                                             
    Name:                                                                       

        Comment.py                                                              

    Description:                                                                

        A set of methods to maintain a :comment alternate data stream in files  
        and folders. Alternate data streams are a feature of NTFS and are not    
        available on other filing systems.                                      

    Audit:                                                                      

        2020-06-29  rj  original code                                           

"""

import os


def __ads__(item):
    """Returns the name of the comment ADS for the given file or folder"""
    return item + ":comment"


def hasComment(item):
    """Returns True if the given file or folder has a comment"""
    return os.path.exists(__ads__(item))


def getComment(item):
    """Returns the comment associated with the given file or folder"""
    return open(__ads__(item), 'r').read() if os.path.exists(item + ":comment") else ""


def setComment(item, comment):
    """Sets the comment for the given file or folder"""
    if os.path.exists(item):
        open(__ads__(item), 'w').write(comment)


def deleteComment(item):
    """Deletes the comment for the given file or folder"""
    if os.path.exists(__ads__(item)):
        os.remove(__ads__(item))


if __name__ == "__main__":

    file = 'comment.py'

    if hasComment(file):
        print(file, 'has comment', getComment(file))
    else:
        print(file, 'does not have a comment')
    setComment(file, "this is a comment")

    if hasComment(file):
        print(file, 'has comment', getComment(file))
    else:
        print(file, 'does not have a comment')

    deleteComment(file)

    if hasComment(file):
        print(file, 'has comment', getComment(file))
    else:
        print(file, 'does not have a comment')

The GUI consists of a window with two panels. The upper panel contains a word-wrapped text control with a vertical scrollbar (if needed). The lower panel contains two button, Save and IMDB. The first button is self-explanatory. The second button, which you are free to remove, is a hook into the Internet Movie DataBase. If you should happen to have files or folders named for movies or TV series, clicking on the IMDB button will attempt to fetch the IMDB info for that name. More on this later. The code for Comment.pyw is as follows:

"""
Name:

    Comment.pyw

Description:

    Allows the user to add a comment to a file or a folder. The comment is saved
    in an alternate data stream (ADS) named "comment". If the file/folder name is
    a string that corresponds to an IMDB entry then clicking the IMDB button will
    attempt to fetch the IMDB data for that string.

Usage:

    comment file/folder

    Running the Comment.reg file in this folder will add a "Comment" context menu
    item to Windows Explorer. Before running comment.reg, ensure that the path names
    for pythonw.exe and Comment.pyw match your system.

    If you intend to use the IMDB facility you must first go to omdbapi.com and get
    a free access key which you must assign to the variable, KEY in getIMDB.py.

Audit:

    2022-09-12  rj  original code

"""

import os
import sys
import wx

from comment import *
from getIMDB import *


class MyFrame(wx.Frame):
    def __init__(self, *args, **kwds):
        # begin wxGlade: MyFrame.__init__
        kwds["style"] = kwds.get("style", 0) | wx.CAPTION | wx.CLIP_CHILDREN | wx.CLOSE_BOX | wx.RESIZE_BORDER | wx.SYSTEM_MENU
        wx.Frame.__init__(self, *args, **kwds)

        self.SetSize((400, 300))
        self.SetTitle(sys.argv[1])

        # Define a GUI consisting of an upper panel for the comment and a lower
        # panel with Save and IMDB buttons.

        self.panel_1 = wx.Panel(self, wx.ID_ANY)

        sizer_1 = wx.BoxSizer(wx.VERTICAL)

        style = wx.HSCROLL | wx.TE_MULTILINE | wx.TE_PROCESS_ENTER | wx.TE_PROCESS_TAB | wx.TE_WORDWRAP
        style = wx.TE_MULTILINE | wx.TE_PROCESS_ENTER | wx.TE_PROCESS_TAB | wx.TE_WORDWRAP

        self.txtComment = wx.TextCtrl(self.panel_1, wx.ID_ANY, "", style=style)
        self.txtComment.SetFont(wx.Font(12, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, 0, ""))
        sizer_1.Add(self.txtComment, 1, wx.EXPAND, 0)

        sizer_2 = wx.BoxSizer(wx.HORIZONTAL)

        self.btnSave = wx.Button(self.panel_1, wx.ID_ANY, "Save")
        self.btnSave.SetToolTip("Save comment and close")
        sizer_2.Add(self.btnSave, 0, 0, 0)

        sizer_2.Add((20, 20), 1, 0, 0)

        self.btnIMDB = wx.Button(self.panel_1, wx.ID_ANY, "IMDB")
        self.btnIMDB.SetToolTip("Fetch info from IMDB")
        sizer_2.Add(self.btnIMDB, 0, 0, 0)

        sizer_1.Add(sizer_2, 0, wx.EXPAND, 0)

        self.panel_1.SetSizer(sizer_1)

        self.Layout()

        # Restore last window size and position

        self.LoadConfig()

        # Bind event handlers

        self.Bind(wx.EVT_BUTTON, self.evt_save, self.btnSave)
        self.Bind(wx.EVT_BUTTON, self.evt_imdb, self.btnIMDB)
        self.Bind(wx.EVT_CLOSE, self.evt_close)
        self.Bind(wx.EVT_TEXT, self.evt_text, self.txtComment)

        # Get file/folder name from command line
        self.item = sys.argv[1]
        self.btnSave.Enabled = False

        # Display comment if one exists        
        if hasComment(item):
            self.txtComment.SetValue(getComment(item))

    def LoadConfig(self):
        """Load the last run user settings"""
        self.config = os.path.splitext(__file__)[0] + ".ini"       
        try:
            with open(self.config,'r') as file:
                for line in file.read().splitlines():
                    exec(line)
        except: pass

    def SaveConfig(self):
        """Save the current user settings for the next run"""

        x,y = self.GetPosition()
        w,h = self.GetSize()

        with open(self.config,'w') as file:
            file.write('self.SetPosition((%d,%d))\n' % (x, y))
            file.write('self.SetSize((%d,%d))\n' % (w, h))

    def evt_imdb(self, event):
        """Set comment to IMDB data for the current file/folder"""
        self.txtComment.SetValue(getIMDB(self.item))
        event.Skip()

    def evt_text(self, event):
        """Comment has changed - Enable Save button"""
        self.btnSave.Enabled = True
        event.Skip()

    def evt_save(self, event):
        """Update the comment for the current file/folder"""
        comment = self.txtComment.GetValue().strip()
        if comment:
            setComment(self.item, self.txtComment.GetValue())
        else:
            deleteComment(item)

        self.Destroy()
        event.Skip()

    def evt_close(self, event):
        """Delete the file/folder comment if all blank or null"""
        if not self.txtComment.GetValue().strip():
            deleteComment(item)
        self.SaveConfig()
        event.Skip()


class MyApp(wx.App):
    def OnInit(self):
        self.frame = MyFrame(None, wx.ID_ANY, "")
        self.SetTopWindow(self.frame)
        self.frame.Show()
        return True


if __name__ == "__main__":
    if len(sys.argv) <= 2:
        item = sys.argv[1]
        if os.path.exists(item):
            app = MyApp(0)
            app.MainLoop()

The registry patch adds the context menu for files and folders. The given patch is configured for my system. You will have to modify the path strings to correspond to

  1. The location of pythonw.exe

  2. The location of Comment.pyw

    REGEDIT4

    [HKEY_CLASSES_ROOT*\shell\Comment]
    @="Comment"

    [HKEY_CLASSES_ROOT*\shell\Comment\command]
    @="\"C:\Users\rjdeg\AppData\Local\Programs\Python\Python310\pythonw.exe\" \"D:\apps\Comment\Comment.pyw\" \"%1\""

    [HKEY_CLASSES_ROOT\Directory\Shell\Comment]
    @="Comment"

    [HKEY_CLASSES_ROOT\Directory\Shell\Comment\command]
    @="\"C:\Users\rjdeg\AppData\Local\Programs\Python\Python310\pythonw.exe\" \"D:\apps\Comment\Comment.pyw\" \"%1\""

Finally, the code for getIMDB.py is as follows:

"""
Name:

    getIMDB.py

Description:

    Given a string (item), getIMDB will query the internet moovie database (IMDB)
    and attempt to return the info for any item matching that string.

Usage:

    info = getIMDB(item)

    Item can be a string such as "War of the Worlds". Note that there are multiple
    movies with this name. If you use "War of the Worlds [1953]" you should get
    the correct info (but it's not perfect).

    Info is returned formatted as (for example)

        Year:       1953
        Runtime:    85 min
        Genre:      Action, Sci-Fi, Thriller
        Rating:     7.0
        Director:   Byron Haskin
        IMDB ID:    tt0046534

        H.G. Wells' classic novel is brought to life in this tale of alien invasion. The residents of a small town in California are excited when a flaming meteor lands in the hills. Their joy is tempered somewhat when they discover that it has passengers who are not very friendly.

        Gene Barry
        Ann Robinson
        Les Tremayne

Note:

    In order to use this you must get a free key from omdbapi.com and modify the
    value of KEY, replacing ######## with your assigned key.

Audit:

    2022-09-12  rj  original code
"""

import requests
import json
import os

def getKey(info, key):
    if key in info:
        return info[key]
    else:
        return 'n/a'

def getIMDB(item):

    # Replace ######## with your key assigned from omdapi.com
    URL = "http://www.omdbapi.com/?r=JSON&plot=FULL"
    KEY = "&apikey=########"

    # item can contain the year as 'name [yyyy]' or 'name (yyyy)'

    item  = item.replace('(','[').replace(')',']')
    temp  = item.split('\\')[-1]
    movie = os.path.splitext(temp)[0]
    title = movie.split('[')[0].strip()

    try:
        year = item.split('[')[1].replace(']','')
    except:
        year = ''

    req = URL + "&t=" + title + "&y=" + year + KEY
    res = requests.get(req)

    if res.status_code == 200:
        info = json.loads(res.text)
        return (
            'Year:\t' + getKey(info,'Year')[:4]       + '\n'   +
            'Runtime:\t' + getKey(info,'Runtime')    + '\n'   +
            'Genre:\t' + getKey(info,'Genre')      + '\n'   +
            'Rating:\t' + getKey(info,'imdbRating') + '\n'   +
            'Director:\t' + getKey(info,'Director')   + '\n' +
            'IMDB ID:\t' + getKey(info,'imdbID') + '\n\n' +
            getKey(info,'Plot') + '\n\n' +
            getKey(info,'Actors').replace(', ', '\n')
        )
    else:
        return None

if __name__ == '__main__':
    name = r'd:\My Videos\War of the Worlds [1953'
    imdb = getIMDB(name)
    print(imdb)

You can modify the format of the returned string as you like.

If you have ever gotten a warning from Windows when first trying to run a downloaded program and wondered why this warning appears, it is because the downloaded file has an ADS named Zone.Identifier which contains something like

[ZoneTransfer]
ZoneId=3
HostUrl=about:internet

When you tell Windows that the program is safe to run it removes the ADS so that you will no longer get the warning.

For anyone interested in learning more about alternate data streams there is an excellent introduction at MalwareBytes.com.

Also, the free Sysinternals suite has a utility, streams.exe which you can run from the command line to see what streams exist for files. The suite can be downloaded at https://docs.microsoft.com/

Thanks, that's very helpful!

Nice work! I wish I'd known about the IMDB API. I wrote a database application to keep track of my movie DVDs and one of the fields is runtime, so I can search for a video which I rated well, haven't seen in a few years, and has a runtime less than the time available before bedtime. Entering the runtimes manually involved a manual search with a magnifying glass to read the text on the DVD cover! Some time into the development I added a button in my app which invokes a browser window and a google search for "movie" plus the movie title for the current database item. Better, but not as direct as a link into IMDB. (I used Delphi with an Access database.)

commented: Access? Aaaccckkk! Try sqlite3. +15

These days I'm more into photography and video making than programming, so I go with what is available, free (to me), and known. My version of Delphi is a bit long in the tooth, so it doesn't support newer databases. But next time I'm in the mood, I'll give it a shot. I never did finish the Python course I got!

If you have python questions I might have the answers. The more I use python the more I freaking love it. I found the book by Magnus Lie Hetland (Beginning Python from Novice to Pro) was better than the online courses.

As for video, are you familiar with ffmpeg? A few years ago I found references to it but initially found it too arcane. Then I found out that a lot of video GUI based tools use it as a back end so I read more about it. When my older son asked me to do some slicing and dicing on a video conference he hosted on aging research I ended up using ffmpeg to do all the editing and conversion. I've found it to be remarkably versatile. I can easily do things with ffmpeg that are difficult or impossible to do with my other tools.

Anyone have any interest in 3D 360 degree virtual reality video?

It's rather a sore subject. We hired a professional VR videographer to capture our wedding in 10K resolution 3D 360 degrees so we could relive the entire day on the Oculus for years to come.

There was a misunderstanding. Instead he captured it all in 11K 2D.

Wow. That's unfortunate. Adam brought a 360 camera to the lake last summer and the videos he took are amazing on the Oculus. It's great to show people who have never been there.

You might try Insta360 Studio (free) to see if your video can be converted.

We actually rented the $20,000 Insta360 Titan for the wedding and hired a professional movie videographer with experience shooting 3D blockbusters.

I don’t want to talk about this anymore, please.

Be a part of the DaniWeb community

We're a friendly, industry-focused community of developers, IT pros, digital marketers, and technology enthusiasts meeting, networking, learning, and sharing knowledge.