├── .github └── workflows │ └── updateMarkdown.yml ├── .gitignore ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── dirutils.py ├── mdsnippets.json ├── obsidian-repos-downloader.py ├── requirements.txt ├── tests ├── tree-output-grouped.txt ├── tree-output-ungrouped.txt ├── update_usage.sh └── usage.txt └── utils.py /.github/workflows/updateMarkdown.yml: -------------------------------------------------------------------------------- 1 | name: on-push-do-doco 2 | on: 3 | push: 4 | jobs: 5 | release: 6 | runs-on: windows-latest 7 | steps: 8 | - uses: actions/checkout@v2 9 | - name: Run MarkdownSnippets 10 | run: | 11 | dotnet tool install --global MarkdownSnippets.Tool 12 | mdsnippets ${GITHUB_WORKSPACE} 13 | shell: bash 14 | - name: Push changes 15 | run: | 16 | git config --local user.email "action@github.com" 17 | git config --local user.name "GitHub Action" 18 | git commit -m "d Doco changes" -a || echo "nothing to commit" 19 | remote="https://${GITHUB_ACTOR}:${{secrets.GITHUB_TOKEN}}@github.com/${GITHUB_REPOSITORY}.git" 20 | branch="${GITHUB_REF:11}" 21 | git push "${remote}" ${branch} || echo "nothing to push" 22 | shell: bash 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /themes/ 2 | /plugins/ 3 | /tests/downloads-for-docs-grouped/ 4 | /tests/downloads-for-docs-ungrouped/ 5 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Thank you. Contributions are welcome, via 4 | [Issues](https://github.com/claremacrae/obsidian-repos-downloader/issues) or 5 | [Pull-Requests](https://github.com/claremacrae/obsidian-repos-downloader/pulls). 6 | 7 | Please check if someone else has made the same suggestion first. Thank you. 8 | 9 | ## Maintenance notes 10 | 11 | ### Automated updating the table of contents 12 | 13 | The table of contents is updated automatically by a GitHub Action, on every push to GitHub. 14 | 15 | ### Almost-automated updating of usage 16 | 17 | The above usage is updated automatically, on push, whenever `tests/usage.txt` is changed. 18 | 19 | To update `tests/usage.txt: 20 | 21 | ```bash 22 | # 1. Update tests/usage.txt 23 | cd tests 24 | ./update_usage.sh 25 | # 2. Commit update usage.txt 26 | # 3. Push to github - a GitHub Action will then update the usage text in this README 27 | ``` 28 | 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Clare Macrae 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 | # obsidian-repos-downloader 2 | 3 | 4 | ## Contents 5 | 6 | * [What?](#what) 7 | * [Why?](#why) 8 | * [Setup](#setup) 9 | * [Requirements](#requirements) 10 | * [Download](#download) 11 | * [Run](#run) 12 | * [Getting Started](#getting-started) 13 | * [Usage - all the arguments](#usage---all-the-arguments) 14 | * [Output Directories](#output-directories) 15 | * [Flatter Structure](#flatter-structure) 16 | * [Grouped by User name](#grouped-by-user-name) 17 | * [Likely Questions](#likely-questions) 18 | * [How do I update repos I have already downloaded?](#how-do-i-update-repos-i-have-already-downloaded) 19 | * [What order are plugins and themes downloaded in?](#what-order-are-plugins-and-themes-downloaded-in) 20 | * [What if there is an error?](#what-if-there-is-an-error) 21 | * [Alternatives](#alternatives) 22 | 23 | [![on-push-do-doco](https://github.com/claremacrae/obsidian-repos-downloader/actions/workflows/updateMarkdown.yml/badge.svg)](https://github.com/claremacrae/obsidian-repos-downloader/actions/workflows/updateMarkdown.yml) 24 | 25 | ## What? 26 | 27 | Clone every approved Obsidian.md community Plugin and Theme - to read and search the source code and learn from the community. 28 | 29 | This is a Python3 script to download a local copy of all the [published community Obsidian plugins and themes](https://github.com/obsidianmd/obsidian-releases), to be used as a large body of example code. 30 | 31 | It inspects these files, and then downloads (clones) all the repos listed in them: 32 | 33 | - [community-css-themes.json](https://github.com/obsidianmd/obsidian-releases/blob/master/community-css-themes.json) 34 | - [community-plugins.json](https://github.com/obsidianmd/obsidian-releases/blob/master/community-plugins.json) 35 | 36 | 37 | ## Why? 38 | 39 | I cannot put it better than the author of the similar project [luckman212/**obsidian-plugin-downloader**](https://github.com/luckman212/obsidian-plugin-downloader): 40 | 41 | > As an absolute beginner to TypeScript, and a lover of [Obsidian](https://obsidian.md/) I often want to take a look at how someone has achieved a certain feature, called on an API, etc. A quick way to do that is by searching through the existing codebase of the ever growing library of plugins out there. 42 | 43 | ## Setup 44 | 45 | ### Requirements 46 | 47 | - Python 3.6 or above 48 | 49 | ### Download 50 | 51 | 1. Download the [Latest Release](https://github.com/claremacrae/obsidian-repos-downloader/releases). 52 | - Choose one of: 53 | - "Source code (zip)" 54 | - "Source code (tar.gz)" 55 | - If you can't see them, click to expand the "Assets" 56 | 2. Expand the downloaded Source Code file 57 | - This will give you a folder name such as "obsidian-repos-downloader-0.1.0" 58 | 59 | ## Run 60 | 61 | ### Getting Started 62 | 63 | The script to run is `obsidian-repos-downloader.py` 64 | 65 | Depending on your platform, here are some example ways you might need to run it: 66 | 67 | ```bash 68 | obsidian-repos-downloader.py 69 | ./obsidian-repos-downloader.py 70 | python3 obsidian-repos-downloader.py 71 | ``` 72 | 73 | ### Usage - all the arguments 74 | 75 | Running `obsidian-repos-downloader.py --help` gives this output: 76 | 77 | 78 | 79 | ```txt 80 | usage: obsidian-repos-downloader.py [-h] [-o OUTPUT_DIRECTORY] [-l LIMIT] [-n] 81 | [-t [{plugins,themes,all}]] 82 | [--group-by-user] [--no-group-by-user] 83 | 84 | Clone repos included in the obsidian-releases repo, to provide a body of 85 | example plugins and CSS themes. 86 | 87 | optional arguments: 88 | -h, --help show this help message and exit 89 | -o OUTPUT_DIRECTORY, --output_directory OUTPUT_DIRECTORY 90 | The directory where repos will be downloaded. Must 91 | already exist. (default: . which means "current 92 | working directory") 93 | -l LIMIT, --limit LIMIT 94 | Limit the number of plugin and theme repos that will 95 | be downloaded. This is useful when testing the script. 96 | 0 (zero) means "no limit". Note: the count currently 97 | includes any repos already downloaded.(default: 0) 98 | -n, --dry-run Print out the commands to be executed, but do no run 99 | them. This is useful for testing. Note: it does not 100 | print the directory-creation commands, just the git 101 | ones 102 | -t [{plugins,themes,all}], --type [{plugins,themes,all}] 103 | The type of repositories to download: plugins, themes 104 | or both. (default: all) 105 | --group-by-user Put each repository in a sub-folder named for the 106 | GitHub user. For example, the plugin 107 | "https://github.com/phibr0/obsidian-tabout" would be 108 | placed in "plugins/phibr0/obsidian-tabout" 109 | --no-group-by-user Put each repository in the same folder, prefixed by 110 | the user name. This is the default behaviour. For 111 | example, the plugin 112 | "https://github.com/phibr0/obsidian-tabout" would be 113 | placed in "plugins/phibr0-obsidian-tabout" 114 | ``` 115 | 116 | 117 | ## Output Directories 118 | 119 | The script always creates a `plugins/` and `themes/` directories for its output. 120 | 121 | There are the command-line arguments to determine the structure inside those directories. 122 | 123 | ### Flatter Structure 124 | 125 | By default, or when the argument `--no-group-by-user` is supplied, all the downloaded repos are placed side-by-side. 126 | They are prefixed with the username of the developer who wrote them. 127 | 128 | For example, running this command (limiting the output to only 4 repositories, for brevity).... 129 | 130 | ```bash 131 | obsidian-repos-downloader.py --limit 4 132 | ``` 133 | 134 | ... gives this directory structure: 135 | 136 | 137 | ```txt 138 | plugins 139 | ├── agathauy-wikilinks-to-mdlinks-obsidian 140 | ├── aidenlx-alx-folder-note 141 | ├── aidenlx-better-fn 142 | └── aidenlx-cm-chs-patch 143 | themes 144 | ├── ArtexJay-Obsidian-CyberGlow 145 | ├── auroral-ui-aurora-obsidian-md 146 | ├── bcdavasconcelos-Obsidian-Ayu 147 | └── bcdavasconcelos-Obsidian-Ayu_Mirage 148 | 149 | 8 directories 150 | ``` 151 | 152 | 153 | 154 | 155 | ### Grouped by User name 156 | 157 | When the argument `--group-by-user` is supplied, all the downloaded repos are placed in sub-directories 158 | named with the username of the developer who wrote them. 159 | 160 | For example, running this command (limiting the output to only 4 repositories, for brevity).... 161 | 162 | ```bash 163 | obsidian-repos-downloader.py --limit 4 --group-by-user 164 | ``` 165 | 166 | ... gives this directory structure: 167 | 168 | 169 | ```txt 170 | plugins 171 | ├── agathauy 172 | │   └── wikilinks-to-mdlinks-obsidian 173 | └── aidenlx 174 | ├── alx-folder-note 175 | ├── better-fn 176 | └── cm-chs-patch 177 | themes 178 | ├── ArtexJay 179 | │   └── Obsidian-CyberGlow 180 | ├── auroral-ui 181 | │   └── aurora-obsidian-md 182 | └── bcdavasconcelos 183 | ├── Obsidian-Ayu 184 | └── Obsidian-Ayu_Mirage 185 | 186 | 13 directories 187 | ``` 188 | 189 | 190 | ## Likely Questions 191 | 192 | ### How do I update repos I have already downloaded? 193 | 194 | This is now done automatically, via `git pull`, for repos that have already been cloned. 195 | 196 | ### What order are plugins and themes downloaded in? 197 | 198 | They are downloaded in case-insensitive alphabetical order of the repository's GitHub URL, so effectively in order 199 | of user name and then repo name. 200 | 201 | ### What if there is an error? 202 | 203 | Sometimes it is not possible to update a repo, for example of there are edited files on the local machine, or the name of the remote branch has changed (such as from 'master' to 'main'). 204 | 205 | The script accumulates a list of errors, and prints them on completion. 206 | 207 | The easiest way to deal with such errors is to delete the downloaded repo, and run the script again. 208 | 209 | Example error output: 210 | 211 | ``` 212 | The following errors occurred: 213 | updating Slowbad/obsidian-solarized 214 | command: git pull --quiet 215 | in: /Users/clare/obsidian-repos-downloader/themes/Slowbad-obsidian-solarized 216 | exit code: 1 217 | stdout: 218 | stderr: Your configuration specifies to merge with the ref 'refs/heads/master' 219 | from the remote, but no such ref was fetched. 220 | 221 | ------------------------------------------------------------------------------- 222 | ``` 223 | 224 | ## Alternatives 225 | 226 | There is a growing number of alternative mechanisms for downloading Obsidian repos: 227 | 228 | - [konhi/**obsidian-repositories-downloader**](https://github.com/konhi/obsidian-repositories-downloader): 229 | - Requires Node 230 | - Downloads plugins only 231 | - [luckman212/**obsidian-plugin-downloader**](https://github.com/luckman212/obsidian-plugin-downloader) 232 | - Written in bash, and a number of other freely-downloadable tools 233 | - You use a console GUI each run, to search and control which repos to download 234 | - Downloads plugins only 235 | -------------------------------------------------------------------------------- /dirutils.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import os 3 | 4 | # Comments for review: 5 | # - File name: 6 | # - I thought it was big enough to separate from utils.py 7 | # - I tried directory-utils.py - but the hyphen meant I could not import it 8 | # - And an underscore in directory_utils.py was inconsistent with existing filenames 9 | 10 | 11 | pushstack = list() 12 | 13 | 14 | def pushdir(dirname: str): 15 | global pushstack 16 | pushstack.append(os.getcwd()) 17 | os.chdir(dirname) 18 | 19 | 20 | def popdir(): 21 | global pushstack 22 | os.chdir(pushstack.pop()) 23 | 24 | 25 | def use_directory(dir, create_if_missing): 26 | class PushPopDirectory: 27 | def __init__(self, dir): 28 | self.dir = dir 29 | 30 | def __enter__(self): 31 | pushdir(dir) 32 | 33 | def __exit__(self, exc_type, exc_val, exc_tb): 34 | popdir() 35 | 36 | if create_if_missing: 37 | os.makedirs(dir, exist_ok=True) 38 | return PushPopDirectory(dir) 39 | 40 | 41 | # From https://stackoverflow.com/q/11415570/104370 42 | def readable_dir(prospective_dir): 43 | if not os.path.isdir(prospective_dir): 44 | raise argparse.ArgumentTypeError("readable_dir:{0} is not a valid path".format(prospective_dir)) 45 | if os.access(prospective_dir, os.R_OK): 46 | return prospective_dir 47 | else: 48 | raise argparse.ArgumentTypeError("readable_dir:{0} is not a readable dir".format(prospective_dir)) 49 | -------------------------------------------------------------------------------- /mdsnippets.json: -------------------------------------------------------------------------------- 1 | { 2 | "Convention": "InPlaceOverwrite", 3 | "ExcludeDirectories": [ "themes", "plugins" ], 4 | "OmitSnippetLinks": true 5 | } 6 | -------------------------------------------------------------------------------- /obsidian-repos-downloader.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import os 4 | import sys 5 | import argparse 6 | import subprocess 7 | 8 | from utils import ( 9 | get_json_from_github 10 | ) 11 | from utils import PLUGINS_JSON_FILE, THEMES_JSON_FILE 12 | from dirutils import use_directory, readable_dir 13 | 14 | 15 | class DownloaderOptions: 16 | def __init__(self): 17 | self.parser = self.make_parser() 18 | self.args = None 19 | 20 | def make_parser(self): 21 | parser = argparse.ArgumentParser( 22 | description="Clone repos included in the obsidian-releases repo, " 23 | "to provide a body of example plugins and CSS themes." 24 | ) 25 | parser.add_argument('-o', '--output_directory', default='.', type=readable_dir, 26 | help='The directory where repos will be downloaded. Must already exist. ' 27 | '(default: %(default)s which means "current working directory")' 28 | ) 29 | 30 | parser.add_argument('-l', '--limit', type=int, default=0, 31 | help='Limit the number of plugin and theme repos that will be downloaded. ' 32 | 'This is useful when testing the script. ' 33 | '0 (zero) means "no limit". ' 34 | 'Note: the count currently includes any repos already downloaded.' 35 | '(default: %(default)s)') 36 | 37 | parser.add_argument('-n', '--dry-run', action="store_true", 38 | help='Print out the commands to be executed, but do no run them. ' 39 | 'This is useful for testing. ' 40 | 'Note: it does not print the directory-creation commands, ' 41 | 'just the git ones') 42 | 43 | parser.add_argument('-t', '--type', 44 | default='all', 45 | const='all', 46 | nargs='?', 47 | choices=['plugins', 'themes', 'all'], 48 | help='The type of repositories to download: plugins, themes or both. ' 49 | '(default: %(default)s)') 50 | 51 | parser.add_argument('--group-by-user', dest='group_by_user', action='store_true', 52 | help='Put each repository in a sub-folder named for the GitHub user. ' 53 | 'For example, the plugin "https://github.com/phibr0/obsidian-tabout" would be placed ' 54 | 'in "plugins/phibr0/obsidian-tabout"') 55 | parser.add_argument('--no-group-by-user', dest='group_by_user', action='store_false', 56 | help='Put each repository in the same folder, prefixed by the user name. ' 57 | 'This is the default behaviour. ' 58 | 'For example, the plugin "https://github.com/phibr0/obsidian-tabout" would be placed ' 59 | 'in "plugins/phibr0-obsidian-tabout"') 60 | parser.set_defaults(group_by_user=False) 61 | 62 | return parser 63 | 64 | def parse_args(self, argv): 65 | self.args = self.parser.parse_args(argv) 66 | 67 | def limit(self): 68 | return self.args.limit 69 | 70 | def dry_run(self): 71 | return self.args.dry_run 72 | 73 | def need_to_download_type(self, type): 74 | return self.args.type in ["all", type] 75 | 76 | def root_output_directory(self): 77 | return self.args.output_directory 78 | 79 | def repo_output_directory(self, user): 80 | if self.args.group_by_user: 81 | return user 82 | else: 83 | return '.' 84 | 85 | def repo_output_name(self, user, repo): 86 | if self.args.group_by_user: 87 | return repo 88 | else: 89 | # Prefix with username, in case there are any duplicated repo names 90 | return f"{user}-{repo}" 91 | 92 | 93 | class Downloader: 94 | def __init__(self, options): 95 | self.options = options 96 | self.errors = "" 97 | 98 | def download(self): 99 | with use_directory(self.options.root_output_directory(), create_if_missing=False): 100 | print(f"Working directory: {os.getcwd()}") 101 | self.process_released_plugins() 102 | self.process_released_themes() 103 | 104 | self.print_any_errors() 105 | 106 | def process_released_plugins(self): 107 | self.process_released_repos("plugins", PLUGINS_JSON_FILE) 108 | 109 | def process_released_themes(self): 110 | self.process_released_repos("themes", THEMES_JSON_FILE) 111 | 112 | def process_released_repos(self, type, json_file): 113 | if not self.options.need_to_download_type(type): 114 | return 115 | 116 | print(f"-----\nProcessing {type}....\n") 117 | with use_directory(type, create_if_missing=True): 118 | plugin_list = get_json_from_github(json_file) 119 | sorted_list = sorted(plugin_list, key=lambda d: d['repo'].lower()) 120 | self.clone_or_update_repos(sorted_list) 121 | 122 | def clone_or_update_repos(self, plugin_list): 123 | count = 0 124 | limit = self.options.limit() 125 | for plugin in plugin_list: 126 | self.clone_or_update_repo(plugin) 127 | count += 1 128 | if limit > 0 and count >= limit: 129 | print("Maximum number of new repos exceeded. Stopping.") 130 | return 131 | 132 | def clone_or_update_repo(self, plugin): 133 | repo = plugin.get("repo") 134 | branch = plugin.get("branch", "master") 135 | user, repo_name = repo.split("/") 136 | # if user != 'Slowbad': 137 | # print('Skipping user') 138 | # return 139 | directory_for_repo = self.options.repo_output_directory(user) 140 | with use_directory(directory_for_repo, create_if_missing=True): 141 | repo_output_name = self.options.repo_output_name(user, repo_name) 142 | if not os.path.isdir(repo_output_name): 143 | command = self.get_download_command(repo, repo_output_name) 144 | self.run_or_log("cloning", command, repo) 145 | else: 146 | with use_directory(repo_output_name, create_if_missing=False): 147 | command = self.get_clone_command() 148 | self.run_or_log(f"updating", command, repo) 149 | 150 | def run_or_log(self, verb, command, repo): 151 | message = f"{verb} {repo}" 152 | print(message) 153 | if self.options.dry_run(): 154 | self.log_dry_run(command) 155 | else: 156 | result = subprocess.run(command, shell=True, check=False, capture_output=True, text=True) 157 | if result.returncode != 0: 158 | self.log_error(result, message, command) 159 | 160 | def log_dry_run(self, command): 161 | print(f'Dry run mode: {command}') 162 | 163 | def get_download_command(self, repo, repo_output_name): 164 | url = f'https://github.com/{repo}' 165 | print(url) 166 | command = f"git clone --quiet {url}.git {repo_output_name}" 167 | return command 168 | 169 | def get_clone_command(self): 170 | command = 'git pull --quiet' 171 | return command 172 | 173 | def log_error(self, result, message2, command): 174 | message = f"""{message2} 175 | command: {command} 176 | in: {os.getcwd()} 177 | exit code: {result.returncode} 178 | stdout: {result.stdout} 179 | stderr: {result.stderr} 180 | ------------------------------------------------------------------------------- 181 | """ 182 | print(message) 183 | self.errors += message 184 | 185 | def print_any_errors(self): 186 | if self.errors != "": 187 | print("The following errors occurred:") 188 | print(self.errors) 189 | 190 | 191 | def download_repos(argv=sys.argv[1:]): 192 | options = DownloaderOptions() 193 | options.parse_args(argv) 194 | 195 | downloader = Downloader(options) 196 | downloader.download() 197 | 198 | 199 | if __name__ == "__main__": 200 | download_repos() 201 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | jinja2 2 | -------------------------------------------------------------------------------- /tests/tree-output-grouped.txt: -------------------------------------------------------------------------------- 1 | plugins 2 | ├── agathauy 3 | │   └── wikilinks-to-mdlinks-obsidian 4 | └── aidenlx 5 | ├── alx-folder-note 6 | ├── better-fn 7 | └── cm-chs-patch 8 | themes 9 | ├── ArtexJay 10 | │   └── Obsidian-CyberGlow 11 | ├── auroral-ui 12 | │   └── aurora-obsidian-md 13 | └── bcdavasconcelos 14 | ├── Obsidian-Ayu 15 | └── Obsidian-Ayu_Mirage 16 | 17 | 13 directories 18 | -------------------------------------------------------------------------------- /tests/tree-output-ungrouped.txt: -------------------------------------------------------------------------------- 1 | plugins 2 | ├── agathauy-wikilinks-to-mdlinks-obsidian 3 | ├── aidenlx-alx-folder-note 4 | ├── aidenlx-better-fn 5 | └── aidenlx-cm-chs-patch 6 | themes 7 | ├── ArtexJay-Obsidian-CyberGlow 8 | ├── auroral-ui-aurora-obsidian-md 9 | ├── bcdavasconcelos-Obsidian-Ayu 10 | └── bcdavasconcelos-Obsidian-Ayu_Mirage 11 | 12 | 8 directories 13 | -------------------------------------------------------------------------------- /tests/update_usage.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Force execution to halt if there are any errors in this script: 4 | set -e 5 | set -o pipefail 6 | 7 | ../obsidian-repos-downloader.py --help > usage.txt 8 | 9 | # TODO Clean out previous downloads before running 10 | 11 | mkdir -p downloads-for-docs-ungrouped 12 | pushd downloads-for-docs-ungrouped 13 | ../../obsidian-repos-downloader.py --limit 4 14 | tree -L 1 -d plugins themes 2>&1 > ../tree-output-ungrouped.txt 15 | popd 16 | 17 | mkdir -p downloads-for-docs-grouped 18 | pushd downloads-for-docs-grouped 19 | ../../obsidian-repos-downloader.py --limit 4 --group-by-user 20 | tree -L 2 -d plugins themes 2>&1 > ../tree-output-grouped.txt 21 | popd 22 | -------------------------------------------------------------------------------- /tests/usage.txt: -------------------------------------------------------------------------------- 1 | usage: obsidian-repos-downloader.py [-h] [-o OUTPUT_DIRECTORY] [-l LIMIT] [-n] 2 | [-t [{plugins,themes,all}]] 3 | [--group-by-user] [--no-group-by-user] 4 | 5 | Clone repos included in the obsidian-releases repo, to provide a body of 6 | example plugins and CSS themes. 7 | 8 | optional arguments: 9 | -h, --help show this help message and exit 10 | -o OUTPUT_DIRECTORY, --output_directory OUTPUT_DIRECTORY 11 | The directory where repos will be downloaded. Must 12 | already exist. (default: . which means "current 13 | working directory") 14 | -l LIMIT, --limit LIMIT 15 | Limit the number of plugin and theme repos that will 16 | be downloaded. This is useful when testing the script. 17 | 0 (zero) means "no limit". Note: the count currently 18 | includes any repos already downloaded.(default: 0) 19 | -n, --dry-run Print out the commands to be executed, but do no run 20 | them. This is useful for testing. Note: it does not 21 | print the directory-creation commands, just the git 22 | ones 23 | -t [{plugins,themes,all}], --type [{plugins,themes,all}] 24 | The type of repositories to download: plugins, themes 25 | or both. (default: all) 26 | --group-by-user Put each repository in a sub-folder named for the 27 | GitHub user. For example, the plugin 28 | "https://github.com/phibr0/obsidian-tabout" would be 29 | placed in "plugins/phibr0/obsidian-tabout" 30 | --no-group-by-user Put each repository in the same folder, prefixed by 31 | the user name. This is the default behaviour. For 32 | example, the plugin 33 | "https://github.com/phibr0/obsidian-tabout" would be 34 | placed in "plugins/phibr0-obsidian-tabout" 35 | -------------------------------------------------------------------------------- /utils.py: -------------------------------------------------------------------------------- 1 | # Original code copied with permission: 2 | # Location: https://github.com/obsidian-community/obsidian-hub/blob/main/.github/scripts/utils.py 3 | # Author: argentum (she/her) 4 | 5 | import json 6 | 7 | from urllib.request import urlopen 8 | 9 | PLUGIN_MANIFEST = "https://raw.githubusercontent.com/{}/{}/manifest.json" 10 | PLUGINS_JSON_FILE = "https://raw.githubusercontent.com/obsidianmd/obsidian-releases/master/community-plugins.json" 11 | THEMES_JSON_FILE = "https://raw.githubusercontent.com/obsidianmd/obsidian-releases/master/community-css-themes.json" 12 | 13 | 14 | def get_json_from_github(url): 15 | with urlopen(url) as response: 16 | json_file = json.loads(response.read()) 17 | 18 | return json_file 19 | 20 | 21 | def get_plugin_manifest(repository, branch): 22 | manifest = get_json_from_github(PLUGIN_MANIFEST.format(repository, branch)) 23 | return manifest 24 | --------------------------------------------------------------------------------