feat: store tags in plain-text .otag files with .otag.toml config and document all commands

This commit is contained in:
randogoth 2026-10-24 21:20:39 +03:00
parent f04554a52c
commit b2ab1421df
8 changed files with 222 additions and 369 deletions

126
README.md
View file

@ -26,18 +26,128 @@ So a as an example an audio-file could be assigned to project `+album` and have
$ uv tool install 'orvanitag[all]'
```
The core only needs PyYAML and rich. Reading and writing embedded metadata uses optional extras: `audio` (pytaglib, needs the TagLib C library), `pdf` (pypdf), `office` (olefile, only for legacy `.doc`/`.xls`/`.ppt`) and `markdown` (python-frontmatter). Plugins whose extra is not installed are skipped.
Requires Python 3.11 or newer. The core only needs rich. Reading and writing embedded metadata uses optional extras: `audio` (pytaglib, needs the TagLib C library), `pdf` (pypdf), `office` (olefile, only for legacy `.doc`/`.xls`/`.ppt`) and `markdown` (python-frontmatter). Plugins whose extra is not installed are skipped.
## Command Line Tool
### Assigning Contextual Tags
Let's
```
$ otag add <file/path> [contextual tags ...]
$ otag <command> [arguments ...] [flags]
```
Removing contextual tags from a file
Every command has a short alias, listed in brackets. `otag -h` prints an overview. 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 remove <file/path> [contextual tags ...]
```
$ 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.
```
$ otag ls '#lofi' @drums
```
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 |
| `-p`, `--path PATH` | Search another directory tree |
| `-r`, `--recursive` | Skip subdirectories (despite its name; subdirectories are included by default) |
| `-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` (`--file-operation`) also copies or moves the file itself. The destination needs a directory part, so write `./new.mp3` rather than `new.mp3`. With `-f`, a destination ending in `/` keeps the file name.
```
$ 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.
`write` (`w`) writes the entry of a file into the file itself, using the named plugin.
```
$ otag scan music/
$ otag write id3 song.mp3
```
| Plugin | File types | Where the tags are stored |
|---|---|---|
| `id3` | `.mp3` `.wav` `.flac` `.ogg` | The comment tag |
| `pdf` | `.pdf` | The keywords field |
| `office` | `.docx` `.xlsx` `.pptx` `.odt` `.ods` `.odp`, reading also `.doc` `.xls` `.ppt` | The description field |
| `frontmatter` | `.md` `.txt` `.markdown` `.rst` | `projects`, `contexts`, `tags` and `note` keys of the YAML front matter |
| `hash//` | Source code such as `.py` `.js` `.c` `.rs`, plus `.md` `.txt` | A `#// +project @context #tag note` line within the first five lines |
### 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` in the same folder configures `scan` and `write` there:
```toml
plugins = ["id3", "pdf"]
[id3]
types = [".mp3", ".flac"]
[id3.map]
tags = "genre"
```
`plugins` limits which plugins run (`hash//` is listed as `hashdoubleslash`), `types` overrides the file types a plugin handles, and `id3.map` additionally reads an audio tag (here `GENRE`) into a category.