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:
- The commenting code
- The GUI code
- The registry patch
- The IMDB code (optional)
The actual comment interface code consists of four methods
- hasComment(item)
- getComment(item)
- setComment(item, comment)
- 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
-
The location of pythonw.exe
-
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.