├── .gitignore ├── LICENSE ├── README.md ├── main.py └── requirements.txt /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | config.yml 3 | venv -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 kometenstaub 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Metadata updater 2 | 3 | ## Introduction 4 | 5 | This Python script is made for Breadcrumbs users. 6 | It does two things after you select on which `keys` it shall act: 7 | 8 | 1. It converts all the values (which need to be either a `string` or a `list`) and formats them as wikilinks. The result will be in `list` format. 9 | 2. If you set `convert_inline` to `true` in the config file, it will move the `[[wikilinks]]` to the frontmatter, turning them into a `list`. **Attention**: This will remove anything that is not a wikilink from that value. 10 | 11 | **Disclaimer: I have tested this on my own vault and written it for my own use case, but have not done extensive testing. *Make sure to have backups, use version control and test it on a test vault first*.** 12 | 13 | **Syncing tools (like Obsidian Sync) are *not* backups!** 14 | 15 | ## Shortcomings 16 | 17 | 1. It will turn nested lists of the form ``- [[link name]]`` into `- - - link name` 18 | 2. It will use single quotes instead of double quotes. You may try [this fix](https://github.com/kometenstaub/metadata-changer/pull/1), which I have not tested however. 19 | 3. The script will fail on invalid YAML which is why it is possible to exclude a path in the config file. (I personally needed this for my templates.) However, the script will show you on which file it failed. 20 | 21 | ## Usage 22 | 23 | 1. Download or clone this repository. 24 | 2. Create a YAML config file `config.yml` (see example below) 25 | 3. `pip3 install -r requirements.txt` 26 | 4. Set the desired config values. 27 | 5. Run the following in the directory where you saved/cloned this repo: 28 | - Windows: `python3 main.py` 29 | - Linux/macOS: `./main.py` 30 | 31 | ### Example config: 32 | 33 | 34 | ```yml 35 | keys: ["parent"] # the YAML keys of which the values shall be transformed 36 | vault_path: "/Users/username/vault_path/" # absolute path to your vault 37 | exclude: "Templates" # optional, leave string empty if nothing is to be excluded, but the key needs to exist 38 | convert_inline: false # whether to convert key:: value 39 | ``` 40 | 41 | Easy creation on Linux/macOS 42 | 43 | ```shell 44 | cat <<< '''keys: ["parent"] # the YAML keys of which the values shall be transformed 45 | vault_path: "/Users/username/vault_path/" # absolute path to your vault 46 | exclude: "Templates" # optional, leave string empty if nothing is to be excluded, but the key needs to exist 47 | convert_inline: false # whether to convert key:: value 48 | ''' > config.yml 49 | ``` 50 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import os, frontmatter, yaml, re 3 | from typing import TypedDict 4 | 5 | 6 | class Config(TypedDict): 7 | keys: list 8 | path: str 9 | exclude: str 10 | convert_inline: bool 11 | 12 | 13 | def get_config() -> Config: 14 | with open("config.yml", "r") as f: 15 | config = yaml.safe_load(f.read()) 16 | return { 17 | "keys": config["keys"], 18 | "path": config["vault_path"], 19 | "exclude": config["exclude"], 20 | "convert_inline": config["convert_inline"] 21 | } 22 | 23 | 24 | def main(): 25 | config = get_config() 26 | keys = config["keys"] 27 | vault_path = config["path"] 28 | exclude = config["exclude"] 29 | convert = config["convert_inline"] 30 | if len(vault_path) > 0 and len(keys) > 0: 31 | for dirpath, dirnames, files in os.walk(vault_path): 32 | # print(f"Found directory: {dirnames}, located here:{dirpath}") 33 | for file_name in files: 34 | if file_name.endswith(".md"): 35 | if (len(exclude) == 0) or (len(exclude) > 0 and exclude not in dirpath): 36 | normalised_path = os.path.normpath(dirpath + "/" + file_name) 37 | print(normalised_path) 38 | with open(normalised_path, "r") as f: 39 | post = frontmatter.load(f) 40 | change_keys(post, normalised_path, keys) 41 | if convert: 42 | convert_inline(post, keys) 43 | if len(post.keys()) > 0: 44 | with open(normalised_path, "w") as f: 45 | f.write(frontmatter.dumps(post)) 46 | print("Done!") 47 | else: 48 | print("Set a vault path and/or add a key!") 49 | 50 | 51 | def convert_inline(post: frontmatter.Post, keys: list): 52 | content = post.content 53 | lines = content.split("\n") 54 | indices = [] 55 | for index, line in enumerate(lines): 56 | inline = line.find("::") 57 | if inline > 0: 58 | raw_key = line[:inline] 59 | raw_value = line[inline + 2:] 60 | excluded_chars = "[]{}*-_># " # may need fine-tuning 61 | new_key = raw_key.strip(excluded_chars) 62 | match = re.findall(r"(\[\[.+?]])", raw_value) 63 | if len(match) > 0 and new_key in keys: 64 | indices.append(index) 65 | current_value = post.get(new_key) 66 | new_values = [] 67 | if current_value: 68 | if isinstance(current_value, str): 69 | new_values.append(current_value) 70 | elif isinstance(current_value, list): 71 | for value in current_value: 72 | new_values.append(value) 73 | for el in match: 74 | new_values.append(el) 75 | post.__setitem__(new_key, new_values) 76 | print("Fixed inline key: '" + new_key + "' with value(s): '" + ", ".join(match) + "'") 77 | if len(indices) > 0: 78 | new_indices = reversed(indices) 79 | for index in new_indices: 80 | lines.pop(index) 81 | post.content = "\n".join(lines) 82 | 83 | 84 | def change_keys(post: frontmatter.Post, norm_path: str, keys: list): 85 | for key in keys: 86 | value = post.get(key) 87 | if value is not None: 88 | new_value = [] 89 | if isinstance(value, list) and len(value) > 0: 90 | for el in value: 91 | if el is not None and not isinstance(el, list) and el[0:2] != "[[": 92 | new_value.append("[[" + el + "]]") 93 | print("File: " + norm_path) 94 | print("Fixed value: '" + el + "' of key: '" + key + "'") 95 | else: 96 | new_value.append(el) 97 | elif isinstance(value, str): 98 | if value[0:2] != "[[": 99 | new_value.append("[[" + value + "]]") 100 | print("File: " + norm_path) 101 | print("Fixed value: '" + value + "' of key: '" + key + "'") 102 | else: 103 | new_value.append(value) 104 | post.__setitem__(key, new_value) 105 | 106 | 107 | if __name__ == "__main__": 108 | main() 109 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | python-frontmatter 2 | pyyaml 3 | --------------------------------------------------------------------------------