8.7 KiB
Orvani Tagger
(From עוֹרְבָנִי, the Hebrew name for th Eurasian Jay)
Platform independent, folder-based, and portable tagging system for organizing and searching files. The command line interface and organizing principles are heavily inspired by the todo.txt standard.
Every file can be assigned projects, contexts, and tags and have a descriptive note.
Exactly like tasks in todo.txt, files can be categorized by freely defined contexts, defined by the
@ prefix or projects, defined by the + prefix. In addition, the # prefix defines hashtags that can be assigned.
So a as an example an audio-file could be assigned to project +album and have a context of @drums and tagged with #boombap, #lofi, #snare. It's completely up to you how you use the different means of categorization.
Installation
$ uv tool install 'orvanitag[all]'
Requires Python 3.11 or newer. The core only needs rich. Reading and writing embedded metadata uses optional extras: audio (pytaglib; its prebuilt wheels include TagLib, building from source needs the TagLib C library), pdf (pypdf), office (olefile, only for legacy .doc/.xls/.ppt) and markdown (PyYAML). Plugins whose extra is not installed are skipped.
Command Line Tool
$ otag <command> [flags] [arguments ...]
Every command has a short alias, listed in brackets. otag -h prints an overview, otag <command> -h the arguments and flags of one command; flags go after the command. Words starting with +, @ or # become projects, contexts and tags; all other words form the note. Quote hashtags in the shell ('#lofi'), because an unquoted # starts a comment.
Tagging
add (a) adds projects, contexts, tags and a note to an existing file or folder. Adding to an already tagged file merges the tags and replaces the note.
$ otag add song.mp3 +album @drums '#lofi' '#snare' first take
remove (r) removes the given projects, contexts and tags. Without any, it removes the whole entry.
$ otag remove song.mp3 '#snare'
$ otag remove song.mp3
Searching
list (ls, l) searches the current directory tree and prints the matching files with their tags. Without a query it lists every tagged file. Each file is shown on one color coded line, like in the .otag file but with its full path; -t shows a table instead.
$ otag ls '#lofi' @drums
song.mp3 --- +album @drums #lofi #snare first take
A term with a prefix only matches that category; a term without a prefix matches any category and the note. Terms match partially (lof finds #lofi) and all terms have to match.
| Flag | Effect |
|---|---|
-e, --exact |
Match whole terms only |
-or, --either |
Match any term instead of all |
--files |
Only show files |
-d, --dirs |
Only show folders |
-t, --table |
Show a table, with columns only for the categories the results use |
-p, --path PATH |
Search another directory tree |
-n, --no-recursive |
Skip subdirectories (also for clean and scan) |
-c, --cache |
Read the index from .otag.cache.json, which is rebuilt whenever an .otag file changed |
-dc, --delete-cache |
Delete .otag.cache.json before searching |
Copying, Moving and Deleting
copy (cp) and move (mv) transfer the tags of a file to another path. By default only the tags are transferred; -f (--with-file) also copies or moves the file itself. With -f, an existing folder as destination keeps the file name. Without -f, a folder as destination gets the tags itself.
$ otag cp song.mp3 song-copy.mp3
$ otag mv -f song.mp3 archive/
del (rm) removes the entry of a file; -f also deletes the file.
$ otag rm -f song.mp3
Maintenance
clean (c) removes entries whose files no longer exist, in the given folder (default: current) and its subdirectories.
purge (p) deletes all .otag and .otag.cache.json files in the given folder and its subdirectories. This removes every tag in that tree and cannot be undone.
$ otag clean
$ otag purge old-project/
Embedded Metadata
scan (s) reads tags that are embedded in files and adds them to their entries, in the given folder (default: current) and its subdirectories. --replace makes the found tags replace the entry instead, so tags removed in another app also disappear from otag; files without embedded tags keep their entry.
write (w) writes the entry of a file into the file itself, using the named plugin. It replaces what the plugin stored before, so tags removed with otag remove disappear from the file on the next write. -c (--clear) removes the plugin's tags from the file instead, e.g. after all tags were removed; it is never done implicitly, because a file without entry may also be one otag never tagged.
$ otag scan music/
$ otag write id3 song.mp3
$ otag write --clear id3 song.mp3
| Plugin | File types | Where the tags are stored |
|---|---|---|
id3 |
.mp3 .wav .flac .ogg |
The comment tag |
image |
.jpg .jpeg .png .gif |
An otag: comment of its own next to any other comments: a COM segment in JPEG (after the JFIF and EXIF segments; some apps only show a JPEG's last comment), an iTXt Comment chunk in PNG, a comment extension in GIF (turning a GIF87a into GIF89a). The image data stays unchanged |
pdf |
.pdf |
The keywords field |
office |
.docx .xlsx .pptx .odt .ods .odp, reading also .doc .xls .ppt |
The description field |
ebook |
.epub .fb2 .cbz |
One otag line where other apps don't look: a <meta name="otag"> in the EPUB package file, a <custom-info info-type="otag"> in FB2, the archive comment of CBZ (never overwriting another tool's comment); the book's own metadata is left untouched |
archive |
.zip .tar .gz .tgz (so also .tar.gz) |
An otag: comment that unpacking ignores: the archive comment of ZIP, the comment field of the gzip header, a pax global header at the start of a tar. Another tool's comment, like the commit id stored by git archive, is never replaced. Writing a tar or gzip file copies it once, the packed data stays unchanged |
frontmatter |
.md .markdown .txt |
projects, contexts, tags and note keys of the YAML front matter; other keys, comments and formatting are left untouched |
comment |
Source code such as .py .sh .toml .js .c .go .rs .php .html, and .svg images |
An otag: +project @context #tag note comment line within the first ten lines, written in the file's comment style below any shebang, encoding, <?xml>, <!DOCTYPE> or <?php line. Tags with -- are refused for HTML and SVG, where they would end the comment |
Piping
add, remove, del and write read the paths from stdin, one per line, when the file is given as -. A file actually named - is written as ./-. The command runs for every path; a failing path prints its error without stopping the others, and otag exits with 1 at the end. When the output of list goes into a pipe or file instead of the terminal, it prints the bare paths, one per line, so otag can feed itself and other tools.
$ ls *.mp3 | otag add - +album '#lofi'
$ find . -name '*.pdf' | otag write pdf -
$ otag ls '#draft' | otag remove - '#draft'
ls folder prints the names without the folder/ prefix, so use ls folder/* or find folder for files outside the current folder.
Import and Export
export (e) writes all entries of the file's directory tree to a text file (default: context.txt), one path --- tags line per file. import (i) reads such a file back and adds the tags that are missing.
$ otag export backup.txt
$ otag import backup.txt
How Tags Are Stored
Each folder keeps the entries of its files and subfolders in a plain text .otag file, so tags travel with the folder when it is copied, synced or archived. Every line holds one entry in the same syntax as the command line: the name, ---, then projects, contexts, tags and the note. Folder names end with /.
Boombap Loop.wav --- +album @drums #boombap #lofi
song.mp3 --- +album @drums #lofi #snare first take
stems/ --- @archive
The file can be edited by hand. A line without --- stops otag with an error naming the file and line, so nothing gets overwritten until it is fixed. Note words starting with +, @ or # are read as tags.
An optional .otag.toml configures scan and write for its folder and all subfolders. Each file uses the nearest .otag.toml above it, which replaces any further up as a whole:
plugins = ["id3", "pdf"]
[id3]
types = [".mp3", ".flac"]
[id3.map]
tags = "genre"
plugins limits which plugins run, types narrows the file types a plugin handles (types the plugin does not support are ignored), and id3.map additionally reads an audio tag (here GENRE) into a category.