This is a script I made for a friend who has a huge movie collection. He wanted to keep track of movie's directors, countries and a few other fields. He bought Opus just for this script. I thought it was so cool I had to perfect it and share it.
This is what the script does for movies in any file format. There are many other columns not shown.
There's also a cool button that opens the imdb page for the selected movie.
I'm really excited about this way of working with files and look forward to feedback and a fruitful discussion. Apart from feedback and brainstorming on this script, I am particularly interested in hearing your thoughts about using file naming conventions to manage metadata.
Quick Tour
1. The script is based on RegexColumnsReloaded (mmm, actually it's the other way around)
2. There is a full tutorial on my new page about Using File Names to Make a Database
3. It showcases how if you use a simple naming convention, Opus can display any column you invent for files of any kind.
4. You can adapt this simple naming convention to files of any kind.
In fact I adapted it to make Columns: Books & Comics Filename Database.
The basic convention for a file is:
Title [metadata].extension
Inside the brackets, the metadata looks like this:
[year=2004_dir=Martin Scorcese]
- It is a sequence of key=value, separated by underscores.
- The fields can be specified in any order.
- Many fields have aliases in case you don't remember the name.
- For each file, you only specify the fields you want.
5. Adding or editing columns is a snap. The column definitions have really cool features. See the section of the tutorial on editing and defining fields.
6. The Magic Button
Movies.dcf (7.87 KB)
(How to use buttons and scripts from this forum)
The button has three functions:
- Toggles a Movies format you specify in Prefs / Folder Formats / Favorite Formats. (If you prefer to use the Content Type Format in the same menu, the function's code is easy to adapt.)
- Launches the imdb page for the selected movie. For that, the filename must already contain a TT field corresponding to the movie page. For instance, set tt=tt0258463 for The Bourne Identity [LINK imdb.com/title/tt0258463/ ]
- Google searches for the movie's imdb page when the tt field is not yet in the filename. The first one is usually right, so just double-click the tt code and add it to the file name.
7. List of fields
8. Movies format
You will want to make a Books+Comics format either in
Prefs / Folder Formats / Favorite Formats
or in
Prefs / Folder Formats / Content Type Formats
In that format, I recommend shrinking the filename column, as you don't need to display it. Even when the file name is shrunk, you can easily select the file when enable full row selection in on (clicking anywhere on the row selects the file). The Movie button toggles full row selection so you shouldn't have to worry about that.
This is what my current view looks like.
9. Bulk addition of metadata on many files
This is a rename problem. See the bulk metadata operations section of the tutorial.
In short, you can set up rename presets.
Here's one which inserts a sample token ( cc=US token ) if you already have [metadata in brackets]:
- Opus rename in regex mode
- Search for
^([^\]]+)(\].*)
- Replace with
\1_cc=US\2
- Save the preset, adapt when needed.
10. Limitation
Please bear in mind that Windows has a limit on the total length of path + file name: 260 characters. That's a lot of characters but it's good to keep in mind. Better now bury the files deep in the file hierarchy.
11. Script Add-In Download
MovieFilenameDB_underscores_only.js.txt (37.1 KB)
(To install, download the file and drag it to the list under Preferences / Toolbars / Scripts.)
12. Changelog
0.9.3 Small suggested fix
0.9.2 At tbone's request, changed the column prefix from Movies_ to Movies.
0.9.1 Added three properties (got these ideas from Apocalypse): fileOnly, dirOnly, fullCupValue.
The code in the two downloads above is reproduced here for reference. This is just to help people browsing the forum for scripting techniques.
Code for 6. The Magic Button:
The button is a button-menu with five functions.
Main button part:
var FirstFileOnly = true;
///////////////////////////////////////////////////////////////////////////////
function OnClick(data) {
LaunchUrl(data, FirstFileOnly);
}
///////////////////////////////////////////////////////////////////////////////
function LaunchUrl( data, firstFileOnly){
var cmd = data.func.command, tab = data.func.sourcetab;
var pattern = /\[(?:[^\]]+_)?tt=([^\]_]+)/i;
cmd.ClearFiles();
for (var iEnum = new Enumerator(tab.selected); !iEnum.atEnd(); iEnum.moveNext()){
var file = iEnum.item();
var matchArray = pattern.exec(file);
if (matchArray != null) {
var tt = matchArray[1];
var url = "http://www.imdb.com/title/" + tt;
cmd.RunCommand(url);
}
if (firstFileOnly) break;
}
}
Toggle Movie View menu item:
@ifset:FULLROWSELECT=on
Set FULLROWSELECT=off
Set FORMAT !custom
@ifset:else
Set FULLROWSELECT=on
Set Format "Movies"
Search IMDb menu item:
var FirstFileOnly = true;
///////////////////////////////////////////////////////////////////////////////
function OnClick(data) {
LaunchUrl(data, FirstFileOnly);
}
///////////////////////////////////////////////////////////////////////////////
function LaunchUrl( data, firstFileOnly){
var cmd = data.func.command, tab = data.func.sourcetab;
var pattern = /^(?:[^ ]| (?!\[))+(?=.*\..)/;
cmd.ClearFiles();
for (var iEnum = new Enumerator(tab.selected); !iEnum.atEnd(); iEnum.moveNext()){
var file = iEnum.item();
var fileName = new String(file.name);
var matchArray = pattern.exec(fileName);
if (matchArray != null) {
var tt = matchArray[0];
// var url = "https://www.google.com/search?q=site:imdb.com" + encodeURIComponent(" " + tt);
// NOTE THAT the % char is escaped: %%20
var url = "https://www.google.com/search?q=site:imdb.com%%20" + tt.replace(/ /g,"-");
// DOpus.Output(tt);
// DOpus.Output(url);
cmd.RunCommand(url);
}
if (firstFileOnly) break;
}
}
Launch IMDb (First File) menu item:
var FirstFileOnly = true;
///////////////////////////////////////////////////////////////////////////////
function OnClick(data) {
LaunchUrl(data, FirstFileOnly);
}
///////////////////////////////////////////////////////////////////////////////
function LaunchUrl( data, firstFileOnly){
var cmd = data.func.command, tab = data.func.sourcetab;
var pattern = /\[(?:[^\]]+_)?tt=([^\]_]+)/i;
cmd.ClearFiles();
for (var iEnum = new Enumerator(tab.selected); !iEnum.atEnd(); iEnum.moveNext()){
var file = iEnum.item();
var matchArray = pattern.exec(file);
if (matchArray != null) {
var tt = matchArray[1];
var url = "http://www.imdb.com/title/" + tt;
cmd.RunCommand(url);
}
if (firstFileOnly) break;
}
}
Launch IMDb (All Files) menu item:
var FirstFileOnly = false;
///////////////////////////////////////////////////////////////////////////////
function OnClick(data) {
LaunchUrl(data, FirstFileOnly);
}
///////////////////////////////////////////////////////////////////////////////
function LaunchUrl( data, firstFileOnly){
var cmd = data.func.command, tab = data.func.sourcetab;
var pattern = /\[(?:[^\]]+_)?tt=([^\]_]+)/i;
cmd.ClearFiles();
for (var iEnum = new Enumerator(tab.selected); !iEnum.atEnd(); iEnum.moveNext()){
var file = iEnum.item();
var matchArray = pattern.exec(file);
if (matchArray != null) {
var tt = matchArray[1];
var url = "http://www.imdb.com/title/" + tt;
cmd.RunCommand(url);
}
if (firstFileOnly) break;
}
}
Code for 11. Script Add-In Download:
///////////////////////////////////////////////////////////////
// Movie Filename Database //
// //
// Add movie columns to Opus, using either //
// - file naming convention + regex search //
// - other means //
// //
///////////////////////////////////////////////////////////////
/*
___
| |
| |
| |
| |
| |
__! !__
\ /
\ /
\ /
\ /
Y
==> TO DEFINE THE COLUMNS, SCROLL ONE PAGE DOWN <==
Full Tutorial: http://www.dearopus.com/filename-database.html
*/
///////////////////////////////////////////////////////////////
// ADAPTING THE SCRIPT FOR OTHER KINDS OF FILES //
// 1. Edit the initData fields just below //
// 2. Edit the ColumnPrefix value right below initData //
// 3. DEFINE THE COLUMNS: SCROLL ONE PAGE DOWN //
///////////////////////////////////////////////////////////////
// Opus calls OnInit to initialize the script add-in
function OnInit(initData) {
//uid added via script wizard (do not change after publishing this script)
var uid = "98D528BF-0B42-4F51-85E0-6770DABF47BE";
//resource center url added via script wizard (required for updating)
var url = "http://resource.dopus.com/viewtopic.php?f=35&t=24708";
// basic information about the script
initData.name = "Movie Filename Database";
initData.desc = "Adds columns defined by regex search against filenames.";
initData.copyright = "playful & contributors 2015";
initData.min_version = "11.5.1"
initData.version = "0.9.2";
initData.default_enable = true;
// The following prefix is added to the column names when looking at columns in
// the Folder Format panel, in order to distinguish the column from
// similar columns from other scripts.
// The prefix will not show in the actual column header
var ColumnPrefix = "Movies.";
// Add all columns (create ScriptColumn objects via AddColumn()
// See http://www.gpsoft.com.au/help/opus11/index.html#!Documents/Scripting/ScriptColumn.htm
for(var key in columns) {
if (columns.hasOwnProperty(key)) {
var column = columns[key];
var cmd = initData.AddColumn();
cmd.autorefresh = true;
cmd.defsort = (typeof column.sort === 'undefined') || (column.sort != "DESC") ? 1 : -1;
cmd.defwidth = (typeof column.width === 'undefined') ? 5 : column.width;
cmd.header = key;
cmd.infotiponly = (typeof column.infotiponly === 'undefined') ? false : column.infotiponly;
cmd.justify = (typeof column.justify === 'undefined') ? "left" : column.justify;
cmd.label = ColumnPrefix + key;
cmd.method = "OnRegexColumn";
cmd.name = key;
cmd.namerefresh = true;
cmd.type = (typeof column.type === 'undefined') ? null : column.type;
}
}
} // End OnInit
///////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////
///// DEFINE YOUR FIELDS (i.e. COLUMNS) HERE /////
///////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////
// Most columns will use a regex pattern, but you can also set columns
// that don't use regex (see EXAMPLES OF NON-REGEX COLUMN below)
// For the regex fields / columns, you set several properties:
// 1. "pattern", i.e. the regex pattern,
// 2. "target" (optional), i.e. the subject for the regex (e.g. the file name, the extension: see TARGET section below for details),
// 3. "group" (optional), i.e. the capture group containing the column text (0 by default, i.e. the entire match)
// 4. "width" (optional) to set the columns default width in characters.
// 5. "justify" (optional), i.e. should the column be justified "left", "right", "center" or "path". The default is "left".
// 6. "sort" (optional). When set to "DESC", if you click on the column to sort it, it will first sort in descending order
// 7. "type" (optional). The column defaults to plain text, but it can also be set to number, double, size, zip, percent, igraph, date, time, datetime, stars
// 8. "infotiponly" (optional): when set to true, the column will only dispay in file hover info tips, which are defined for file types in the file type editor
// 9. "subjectPrefix" (optional): a string that gets preppended to the target string to form the regex subject. This facility enables you to transform the text in the regex without writing a special column case.
// 10. "returnTheFirstNonEmptyGroup" (optional): a Boolean (true/false) that indicates we should return the first group that is not-empty. Takes precedence over "group"
// 11. "fileOnly": restrict the column to files (don't display for folders)
// 12. "dirOnly": restrict the column to folders (don't display for files)
// 13. "fullCupValue" (optional): using this value, convert to a scale of 5 or 100 depending on whether the type is stars (5) or percent, graph and igraph (100)
// TARGETS
// The regex pattern can be applied to several properties of the file item
// We configure which property to run the regex on by setting "target"
// Possible values for target:
// 1. default: no value or "name_stem: (the file name without the extension)
// 2. "realpath" (the pattern applies to the full pathname, i.e. path plus filename)
// 3. "name" (the full file name including the extension)
// 4. "ext" (the extension)
// and more: see http://www.gpsoft.com.au/help/opus11/index.html#!Documents/Scripting/Item.htm
var columns = {
/////// EXAMPLES OF REGEX COLUMNS //////////
// You can delete these if you don't need them
'Title' : {
// The title (stripping [the keys=values] )
pattern: /^(?:[^ ]| (?!\[))*/,
width: 30
},
'Actor' : {
// Who featured in it?
// acceptable aliases for key: act, actor, actress
pattern: /\[(?:[^\]]+_)?act(?:or|ress)?=([^\]_]+)/i,
group: 1
},
'Again' : {
// Would you like to watch it again some day?
// acceptable aliases for key: again
pattern: /\[(?:[^\]]+_)?again=([^\]_]+)/i,
group: 1,
width: 3
},
'Award' : {
// awards won. e.g. oscar, palme d'or
// acceptable aliases for key: awd, award
pattern: /\[(?:[^\]]+_)?aw(?:ar)?d=([^\]_]+)/i,
group: 1
},
'Camera' : {
// Cinematographer e.g. Robby Muller
// acceptable aliases for key: cam, camera, cin
pattern: /\[(?:[^\]]+_)?c(?:am(?:era)?|in)=([^\]_]+)/i,
group: 1
},
'Color' : {
// Is the movie in color? b&W?
// acceptable aliases for key: col, color
pattern: /\[(?:[^\]]+_)?col(?:or)?=([^\]_]+)/i,
group: 1
},
'CC' : {
// Country code: Which country is the movie from?
// acceptable aliases for key: cc, country
pattern: /\[(?:[^\]]+_)?c(?:c|ountry)?=([^\]_]+)/i,
group: 1,
width: 2
},
'Critique' : {
// Critique, e.g. great, meh
// acceptable aliases for key: crit, critique
pattern: /\[(?:[^\]]+_)?crit(?:ique)?=([^\]_]+)/i,
group: 1
},
'Director' : {
// Who directed it?
// acceptable aliases for key: dir, director
pattern: /\[(?:[^\]]+_)?dir(?:ector)?=([^\]_]+)/i,
group: 1,
width: 15
},
'IR' : {
// imdb rating, e.g. 7.5
// acceptable aliases for key: ir
pattern: /\[(?:[^\]]+_)?ir=([^\]_]+)/i,
group: 1,
width: 3,
sort: "DESC"
},
'Lang' : {
// Language, e.g. english
// acceptable aliases for key: lang, lng
pattern: /\[(?:[^\]]+_)?la?ng=([^\]_]+)/i,
group: 1,
width: 3
},
'Min' : {
// Duration, e.g. 120
// acceptable aliases for key: min, dur, duration
pattern: /\[(?:[^\]]+_)?(?:min|dur(?:ation)?)=([^\]_]+)/i,
group: 1,
width: 3,
sort: "DESC"
},
'Num' : {
// Number in the series, e.g. 01
// acceptable aliases for key: num, nb, no
pattern: /\[(?:[^\]]+_)?n(?:[ob]|um)=([^\]_]+)/i,
group: 1,
width: 3
},
'Qual' : {
// Quality, e.g. great, blurry
// acceptable aliases for key: q, qual, quality
pattern: /\[(?:[^\]]+_)?q(?:ual(?:ity)?)?=([^\]_]+)/i,
group: 1
},
'Rating' : {
// your rating
// acceptable aliases for key: rat, rating
pattern: /\[(?:[^\]]+_)?rat(?:ing)?=([^\]_]+)/i,
group: 1,
sort: "DESC"
},
'Script' : {
// Who wrote the script?
// acceptable aliases for key: txt, text, scr, script
pattern: /\[(?:[^\]]+_)?(?:te?xt|scr(?:ipt)?)=([^\]_]+)/i,
group: 1
},
'Seen' : {
// have you watched it? E.g. yes, T, false, N, 1, 0...
// acceptable aliases for key: seen, sn
pattern: /\[(?:[^\]]+_)?s(?:een?|n)=([^\]_]+)/i,
group: 1,
width: 1,
sort: "DESC"
},
'Series' : {
// e.g.
// acceptable aliases for key: ser, series
pattern: /\[(?:[^\]]+_)?ser(?:ies)?=([^\]_]+)/i,
group: 1
},
'Subs' : {
// subtitles, e.g. en,eng, de, fr
// acceptable aliases for key: sb, sub, subs, sbs, subtitle, subtitles
pattern: /\[(?:[^\]]+_)?s(?:b|ub(?:title)?)s?=([^\]_]+)/i,
group: 1,
width: 5
},
'Tags' : {
// tags, e.g. "comedy,romance"
// acceptable aliases for key: tag, tags
pattern: /\[(?:[^\]]+_)?tags?=([^\]_]+)/i,
group: 1
},
'TT' : {
// imdb code, e.g. tt0478331
// acceptable aliases for key: tt
pattern: /\[(?:[^\]]+_)?tt=([^\]_]+)/i,
group: 1,
width: 2
},
'Year' : {
// What year was it released?
// acceptable aliases for key: y, yr, year
pattern: /\[(?:[^\]]+_)?y(?:(?:ea)?r)?=([^\]_]+)/i,
group: 1,
justify: "center",
width: 4,
sort: "DESC"
},
/////// EXAMPLES OF NON-REGEX COLUMN //////////
// You can delete these if you don't need them
// Define the columns here (name, width if appropriate etc)
// then add an "if" or "else if" case in the "First, handle special NON-REGEX Columns" section
'Namelen' : {
// This column gives you the length of the file name
},
'Charsleft' : {
// This column gives you the number of chars still available to use in the path + file name
// Windows imposes a 260 maximum length for the Path+Filename
},
/////// EXAMPLES OF TRICK COLUMNS ////////////////
// You can delete these if you don't need them
// For explanations, see www.dearopus.com/filename-database.html/filename-database.html#inspectiontrick
// Trick Column, feel free to delete
'modify' : {
// This column illustrates how you can display one of the file properties in a column
pattern: /.*/,
target: "modify"
},
// Trick Column, feel free to delete
'3-deep' : {
// This column illustrates how you can create a Boolean column without creating a
// special if case in the code
pattern: /^(Y)(?:[^\\]*\\){4}/,
target: "realpath",
group: 1,
subjectPrefix: "Y"
},
// Trick Column, feel free to delete
'3-deep YN' : {
// This variation on the previous column not only displays "Y" if the path is at least
// 3-deep, it displays "N" if it isn't
pattern: /^Y(?=(?:[^\\]*\\){4})|N/,
target: "realpath",
subjectPrefix: "YN"
},
// Trick Column, feel free to delete
'NumCapsLow' : {
// This column shows "num" if a file stem is all numeric, "CAPS" if it is all uppercase, "low" if it is all lowercase
pattern: /^(num)CAPSlow\d+$|^num(CAPS)low[A-Z]+$|^numCAPS(low)[a-z]+$/,
subjectPrefix: "numCAPSlow",
returnTheFirstNonEmptyGroup: true
},
// Trick Column, feel free to delete
'pathdepth' : {
// This column shows one of three values: "0-2" if a file's path depth is less than 3, "4-5", or "6+"
pattern: /^0-2(3-5)6\+(?:[^\\]*\\){4,6}[^\\]*$|^0-23-5(6\+)(?:[^\\]*\\){7}|(0-2)/,
target: "realpath",
subjectPrefix: "0-23-56+",
returnTheFirstNonEmptyGroup: true
}
};
///////////////////////////////////////////////////////////////
///// END OF FIELD / COLUMN DEFINITIONS /////
///////////////////////////////////////////////////////////////
function OnRegexColumn(ColumnData) {
// OnRegexColumn is an OnScriptColumn method
// See http://www.gpsoft.com.au/help/opus11/index.html#!Documents/Scripting/OnScriptColumn.htm
// ColumnData is a ScriptColumnData object
// See http://www.gpsoft.com.au/help/opus11/index.html#!Documents/Scripting/ScriptColumnData.htm
var colName = ColumnData.col;
if(!columns[colName]) return;
////////////////////////////////////////////////////////////////
//// Is the column only restricted to files or to folders? ////
//// If so, bail if needed ////
////////////////////////////////////////////////////////////////
var dirOnly = (typeof columns[colName].dirOnly === 'undefined') ? false : columns[colName].dirOnly;
var fileOnly = (typeof columns[colName].fileOnly === 'undefined') ? false : columns[colName].fileOnly;
var haveFileButDirOnly = (dirOnly && ! ColumnData.item.is_dir);
var haveDirButFileOnly = (fileOnly && ColumnData.item.is_dir);
if (haveFileButDirOnly || haveDirButFileOnly) {
// we're not supposed to compute the column for this item: let's bail
return;
}
/////////////////////////////////////////////////
//// First, handle special NON-REGEX Columns ////
/////////////////////////////////////////////////
// Is this the filename length column?
if (colName == 'Namelen') {
ColumnData.value = ColumnData.item.name.length.toString();
}
// Is this the "how many chars available in the filename" column?
else if (colName == 'Charsleft') {
var PathLength = new String(ColumnData.item.realpath).length;
// how many chars available in the path + filename? Max = 260
ColumnData.value = (260-PathLength).toString();
}
/////////////////////////////////////////////////
//// Next, handle REGEX Columns ////
/////////////////////////////////////////////////
else { // it's a regex column
var regexMatch;
var fileSubject;
var subject;
// Did we specify a target, or are we matching on the default name_stem?
if(!columns[colName].target) {
fileSubject = ColumnData.item["name_stem"];
}
else {
fileSubject = ColumnData.item[columns[colName].target];
}
// Add prefix to the subject if present
subject = (typeof columns[colName].subjectPrefix === 'undefined') ? fileSubject : columns[colName].subjectPrefix + fileSubject;
regexMatch = columns[colName].pattern.exec(subject);
if (regexMatch != null) {
// The general case: we have not set the returnTheFirstNonEmptyGroup property
if (typeof columns[colName].returnTheFirstNonEmptyGroup === 'undefined') {
// display the value for the correct capture group
var capturegroup = (typeof columns[colName].group === 'undefined') ? 0 : columns[colName].group;
ColumnData.value = regexMatch[capturegroup];
}
else if(columns[colName].returnTheFirstNonEmptyGroup) {
// What is the first non-empty group (apart from zero)?
for(i = 1; i < regexMatch.length + 1; i++){
if (regexMatch[i] != '') {
ColumnData.value = regexMatch[i];
break;
}
}
}
} // regexmatch
} // regex column
//////////////////////////////////////////////////////
//// Optionally, TRANSFORM the value ////
//////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////
// Handle percent, graph, igraph and star values //
// - convert value to 0 if it is not numeric //
// - scale of 1 for percentage and graphs //
// - scale of 5 for stars //
////////////////////////////////////////////////////////////////
var haveType = ! (typeof columns[colName].type === 'undefined') ;
var haveCup = ! (typeof columns[colName].fullCupValue === 'undefined') ;
if ( haveType ) {
var colType = columns[colName].type;
// Is it graph, igraph, percent or stars?
if (colType=="graph"||colType=="igraph"||colType=="percent"||colType=="stars") {
// Not a number? Set to 0
if (isNaN(ColumnData.value)) {
ColumnData.value = 0;
}
// If we have a fullCupValue, convert the numbers
else if ( haveCup ){
// Stars? Scale of 5
if(colType == "stars" ) {
ColumnData.value = ColumnData.value * 5 / columns[colName].fullCupValue;
}
// Then it must be graph, igraph or percent: scale of 100
else {
ColumnData.value = ColumnData.value * 100 / columns[colName].fullCupValue;
}
}
} // relevant type
} // haveType
// If we have a fullCupValue but no type: convert to a scale of 100
// i.e. assume the type is percent, graph or igraph rather than stars
else if ( haveCup ) {
ColumnData.value = ColumnData.value * 100 / columns[colName].fullCupValue;
}
//////////// End graph, igraph, stars, percent /////////////////
// End Transformations
} // OnRegexColumn
///////////////////////////////////////////////////////////////////////////////
function OnAboutScript(data){ //v0.1
var cmd = DOpus.Create.Command();
if (!cmd.Commandlist('s').exists("ScriptWizard")){
if (DOpus.Dlg.Request("The 'ScriptWizard' add-in has not been found.\n\n"+
"Install 'ScriptWizard' from [resource.dopus.com].\nThe add-in enables this dialog and also offers "+
"easy updating of scripts and many more.","Yes, take me there!|Cancel", "No About.. ", data.window))
cmd.RunCommand('http://resource.dopus.com/viewtopic.php?f=35&t=23179');}
else
cmd.RunCommand('ScriptWizard ABOUT WIN='+data.window+' FILE="'+Script.File+'"');
}
//MD5 = "525b552d92437c52bbc6c672013f63ab"; DATE = "2015.06.26 - 17:39:03"