├── bin ├── zotero-cli.d.ts └── zotero-cli.ts ├── examples ├── README.md ├── zotGetItemsWithTag.pl ├── zotDisplayCollectionNamesWithKeys.pl ├── zotCopyToLibrary.pl ├── zotUpdateTags.pl ├── zotUpdateExtraAppend.pl ├── zotTagToCollectionByName.pl ├── zotUpdateETHIAKA.pl ├── zotUpdateField.pl ├── zotShowAllCollections.pl ├── zotListItemsInCollection.pl ├── zotProcessCollections.pl ├── zotLinkToURI.pl ├── zotItemTracker.pl └── zotCreateCollectionsAndTags.pl ├── .gitignore ├── requirements.txt ├── search.json ├── .github └── workflows │ └── main.yml ├── tsconfig.json ├── package.json ├── README.md ├── tslint.json └── docs ├── COMMANDS.md └── Examples.md /bin/zotero-cli.d.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | export {}; 3 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | The files are some examples for using the API in perl. -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.ini 2 | *.toml 3 | *.swp 4 | node_modules 5 | *.js 6 | .env 7 | -------------------------------------------------------------------------------- /examples/zotGetItemsWithTag.pl: -------------------------------------------------------------------------------- 1 | 2 | system(qq{zotero-cli items --filter '{"tag": "$ARGV[0]"}'}); 3 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | pyzotero 2 | toml 3 | python-dotenv 4 | munch 5 | jsonschema 6 | pyOpenSSL 7 | idna 8 | -------------------------------------------------------------------------------- /search.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "My Search", 4 | "conditions": [ 5 | { 6 | "condition": "title", 7 | "operator": "contains", 8 | "value": "foo" 9 | }, 10 | { 11 | "condition": "date", 12 | "operator": "isInTheLast", 13 | "value": "7 days" 14 | } 15 | ] 16 | } 17 | ] 18 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: [push] 3 | jobs: 4 | job: 5 | runs-on: ubuntu-latest 6 | timeout-minutes: 3 7 | steps: 8 | - uses: actions/checkout@v2 9 | - name: Prepare 10 | run: npm ci 11 | - name: Lint 12 | uses: mooyoul/tslint-actions@v1.1.1 13 | with: 14 | token: ${{ secrets.GITHUB_TOKEN }} 15 | pattern: '*.ts' 16 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowUnreachableCode": false, 4 | "declaration": true, 5 | "importHelpers": true, 6 | "target": "es2017", 7 | "disableSizeLimit": true, 8 | "module": "commonjs", 9 | "noImplicitAny": false, 10 | "removeComments": false, 11 | "preserveConstEnums": false, 12 | "sourceMap": false, 13 | "downlevelIteration": true, 14 | "lib": [ "es2017", "dom" ], 15 | "typeRoots": [ 16 | "./typings", 17 | "./node_modules/@types" 18 | ] 19 | }, 20 | "include": [ 21 | "bin" 22 | ], 23 | "exclude": [ 24 | "node_modules", 25 | "**/*.spec.ts", 26 | "typings" 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /examples/zotDisplayCollectionNamesWithKeys.pl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/perl 2 | use warnings; 3 | use strict; 4 | use open IO => ':encoding(UTF-8)', ':std'; 5 | use utf8; 6 | use feature qw{ say }; 7 | use 5.18.2; 8 | #use String::ShellQuote; 9 | #$string = shell_quote(@list); 10 | #use Data::Dumper; 11 | my $home = $ENV{HOME}; 12 | (my $date = `date +'%Y-%m-%d_%H.%M.%S'`) =~ s/\n//; 13 | my $help = ""; 14 | my $string = ""; 15 | my $number = ""; 16 | use Getopt::Long; 17 | GetOptions ( 18 | "string=s" => \$string, 19 | "help" => \$help, 20 | "number=f" => \$number, 21 | ) or die("Error in command line arguments\n"); 22 | 23 | my $top = "--top"; 24 | #$top = ""; 25 | 26 | foreach my $item (@ARGV) { 27 | if ($item =~ m/^\d+$/) { 28 | say "Top collections in library:"; 29 | system qq{zotero-cli --group-id $item collections $top | jq '.[] | .data | { key, name } ' | jq -s -c 'sort_by(.name) | .[]'}; 30 | } else { 31 | (my $group, my $key) = ($item =~ m/(?:zotero\:\/\/select\/groups\/)?(\d+)(?:\/|\:)(?:collections\/)?([\w\d]+)/); 32 | say "Collections in collection: $group:$key"; 33 | system qq{zotero-cli --group-id $group collections $top --key $key | jq '.[] | .data | { key, name } ' | jq -s -c 'sort_by(.name) | .[]'}; 34 | }; 35 | }; 36 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "zotero-cli", 3 | "version": "1.0.6", 4 | "description": "Command line for interacting with Zotero libraries", 5 | "main": "bin/zotero-cli.js", 6 | "preferGlobal": true, 7 | "bin": { 8 | "zotero-cli": "./bin/zotero-cli.js" 9 | }, 10 | "scripts": { 11 | "start": "ts-node bin/zotero-cli.ts", 12 | "preversion": "npm test", 13 | "postversion": "git push --follow-tags", 14 | "test": "tslint -t stylish --project .", 15 | "build": "tsc && chmod +x bin/*.js", 16 | "prepublishOnly": "npm run build", 17 | "go": "npm run build && npm version patch && npm publish", 18 | "fixed": "npm test && npm run build && git add bin && git commit -m \"fixes #$FIXED\" && git push" 19 | }, 20 | "repository": { 21 | "type": "git", 22 | "url": "git+https://github.com/edtechhub/zotero-cli.git" 23 | }, 24 | "keywords": [ 25 | "zotero" 26 | ], 27 | "author": "Emiliano Heyns", 28 | "license": "ISC", 29 | "bugs": { 30 | "url": "https://github.com/edtechhub/zotero-cli/issues" 31 | }, 32 | "homepage": "https://github.com/edtechhub/zotero-cli#readme", 33 | "devDependencies": { 34 | "@types/node": "^13.11.1", 35 | "ts-node": "^8.8.2", 36 | "tslint": "^6.1.1", 37 | "typescript": "^3.8.3" 38 | }, 39 | "dependencies": { 40 | "@iarna/toml": "^2.2.3", 41 | "ajv": "^6.12.0", 42 | "argparse": "^1.0.10", 43 | "docstring": "^1.1.0", 44 | "dotenv": "^8.2.0", 45 | "http-link-header": "^1.0.2", 46 | "md5-file": "^5.0.0", 47 | "request": "^2.88.2", 48 | "request-promise": "^4.2.5" 49 | }, 50 | "files": [ 51 | "package.json", 52 | "bin/zotero-cli.d.ts", 53 | "bin/zotero-cli.js", 54 | "docs/COMMANDS.md", 55 | "README.md" 56 | ] 57 | } 58 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # zotero-cli 2 | 3 | A commandline tool to interact with the Zotero API. Developed by [@bjohas](https://github.com/bjohas), [@retorquere](https://github.com/retorquere) and [@a1diablo](https://github.com/a1diablo). 4 | 5 | ## Installation 6 | ### node 7 | 8 | Run the following command to install dependencies 9 | 10 | ``` 11 | npm install 12 | ``` 13 | 14 | Then run to run the script: 15 | 16 | ``` 17 | npm start -- 18 | ``` 19 | 20 | E.g. 21 | 22 | ``` 23 | npm start -- tags --count 24 | ``` 25 | 26 | #### For compiled JS: 27 | 28 | ``` 29 | npm i @types/node 30 | npm run build 31 | ``` 32 | 33 | Then: 34 | 35 | ``` 36 | ./bin/zotero-cli.js 37 | ``` 38 | 39 | E.g. 40 | 41 | ``` 42 | ./bin/zotero-cli.js tags --count 43 | ``` 44 | 45 | ## Documentation - overview 46 | 47 | ### Configuration 48 | 49 | Get help with 50 | 51 | ``` 52 | zotero-cli -h 53 | ``` 54 | 55 | Optional arguments: 56 | 57 | ``` 58 | -h, --help Show this help message and exit. 59 | --api-key API_KEY 60 | --config CONFIG 61 | --user-id USER_ID 62 | --group-id GROUP_ID 63 | --indent INDENT 64 | ``` 65 | 66 | You can create a config.toml file as follows 67 | 68 | ``` 69 | api-key = "..." 70 | group-id = 123 71 | library-type = "group" 72 | indent = 4 73 | ``` 74 | 75 | A file called zotero-cli.toml is picked up automatically. 76 | 77 | ### Commands 78 | 79 | Get help with 80 | 81 | ``` 82 | zotero-cli -h 83 | ``` 84 | 85 | which returns a set of commands, such as key, collection, collections, items, item, publications, trash, tags, searches, attachment, types, fields. You can get help on any of these with 86 | 87 | ``` 88 | zotero-cli --help 89 | ``` 90 | 91 | e.g. 92 | 93 | ``` 94 | zotero-cli collection --help 95 | ``` 96 | 97 | -------------------------------------------------------------------------------- /examples/zotCopyToLibrary.pl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/perl 2 | use warnings; 3 | use strict; 4 | use open IO => ':encoding(UTF-8)', ':std'; 5 | use utf8; 6 | use feature qw{ say }; 7 | use 5.18.2; 8 | #use String::ShellQuote; 9 | #$string = shell_quote(@list); 10 | #use Data::Dumper; 11 | my $home = $ENV{HOME}; 12 | (my $date = `date +'%Y-%m-%d_%H.%M.%S'`) =~ s/\n//; 13 | my $help = ""; 14 | my $string = ""; 15 | my $number = ""; 16 | use Getopt::Long; 17 | GetOptions ( 18 | "string=s" => \$string, 19 | "help" => \$help, 20 | "number=f" => \$number, 21 | ) or die("Error in command line arguments\n"); 22 | 23 | if (!@ARGV || $help) { 24 | print("Need arguments"); 25 | print "Sorry, no help."; 26 | system("less","$0"); 27 | exit; 28 | }; 29 | 30 | 31 | foreach my $a (@ARGV) { 32 | $a =~ s/.*\///; 33 | &process($a); 34 | }; 35 | 36 | exit(); 37 | 38 | 39 | sub process() { 40 | 41 | my $HFKVA2IQ = $_[0]; 42 | my $gsource = "2405685"; 43 | my $gtarget = "2129771"; 44 | my $collection = "QMCLH63J"; 45 | 46 | system qq{zotero-cli --group-id $gsource item --key $HFKVA2IQ > $HFKVA2IQ.json}; 47 | my $extra = `jq ".data.extra" $HFKVA2IQ.json`; 48 | if ($extra =~ m/EdTechHub.ItemAlsoKnownAs:[^\n]*\b2129771\:([\w\d]+)\b/s) { 49 | say "Already present as $1"; 50 | } else { 51 | say "Adding..."; 52 | system qq{jq ".data" $HFKVA2IQ.json | jq "del(.collections) | del(.key) | del(.version) | del(.dateAdded) | del(.dateModified)" | } . 53 | qq{jq '. += { "relations": {"owl:sameAs": "http://zotero.org/groups/$gsource/items/$HFKVA2IQ" }}' | }. 54 | qq{jq '. += { "collections": ["$collection"] }' > newElement.json }; 55 | system qq{zotero-cli --group-id 2129771 create-item newElement.json }; 56 | # Now need to patch the old element. 57 | }; 58 | }; 59 | 60 | -------------------------------------------------------------------------------- /examples/zotUpdateTags.pl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/perl 2 | use warnings; 3 | use strict; 4 | use open IO => ':encoding(UTF-8)', ':std'; 5 | use utf8; 6 | use feature qw{ say }; 7 | use 5.18.2; 8 | use String::ShellQuote; 9 | #use Data::Dumper; 10 | my $home = $ENV{HOME}; 11 | (my $date = `date +'%Y-%m-%d_%H.%M.%S'`) =~ s/\n//; 12 | my $help = ""; 13 | my $number = ""; 14 | use Getopt::Long; 15 | my $key = ""; 16 | my $value = ""; 17 | my $update = ""; 18 | my $group = ""; 19 | my $item = ""; 20 | my $file = ""; 21 | my $remove = ""; 22 | GetOptions ( 23 | "help" => \$help, 24 | "number=f" => \$number, 25 | "key=s" => \$key, 26 | "value=s" => \$value, 27 | "update" => \$update, 28 | "group=s" => \$group, 29 | "item=s" => \$item, 30 | "file=s" => \$file, 31 | "remove" => \$remove, 32 | ) or die("Error in command line arguments\n"); 33 | 34 | use JSON qw( decode_json encode_json to_json from_json); 35 | 36 | my $thegroup = ""; 37 | if ($group) { 38 | $thegroup = "--group $group"; 39 | } 40 | 41 | my @t; 42 | if (@ARGV) { 43 | @t = @ARGV; 44 | }; 45 | 46 | if ($file) { 47 | open F,"$file"; 48 | push @t, ; 49 | close F; 50 | } 51 | 52 | 53 | if ($help || !@t) { 54 | say " 55 | $0 --group 123 --key ABC Tag1 Tag2 Tag3 56 | $0 --group 123 --key ABC --file tags.txt 57 | 58 | The tags Tag1 Tag2 Tag3 are added to item ABC in group 123. 59 | "; 60 | exit; 61 | }; 62 | 63 | 64 | foreach (@t) { 65 | s/\n//; 66 | $_ = "\"$_\""; 67 | }; 68 | 69 | my $str = `./zoteroUpdateField.pl $thegroup --item $item --key tags | jq " .tags[] | .tag"`; 70 | if ($str =~ m/\S/s) { 71 | $str =~ s/\n$//s; 72 | push @t, split(/\n/,$str); 73 | }; 74 | 75 | my $string = shell_quote("[{\"tag\":".join("},{\"tag\":", @t)."}]"); 76 | 77 | if ($remove) { 78 | $string = shell_quote("[]"); 79 | }; 80 | 81 | say `./zoteroUpdateField.pl $thegroup --item $item --key tags --value $string --update`; 82 | 83 | -------------------------------------------------------------------------------- /examples/zotUpdateExtraAppend.pl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/perl 2 | use warnings; 3 | use strict; 4 | use open IO => ':encoding(UTF-8)', ':std'; 5 | use utf8; 6 | use feature qw{ say }; 7 | use 5.18.2; 8 | use String::ShellQuote; 9 | #use Data::Dumper; 10 | my $home = $ENV{HOME}; 11 | (my $date = `date +'%Y-%m-%d_%H.%M.%S'`) =~ s/\n//; 12 | my $help = ""; 13 | my $number = ""; 14 | use Getopt::Long; 15 | my $key = ""; 16 | my $value = ""; 17 | my $update = ""; 18 | my $group = ""; 19 | my $item = ""; 20 | my $file = ""; 21 | my $remove = ""; 22 | GetOptions ( 23 | "help" => \$help, 24 | "number=f" => \$number, 25 | "key=s" => \$key, 26 | "value=s" => \$value, 27 | "update" => \$update, 28 | "group=s" => \$group, 29 | "item=s" => \$item, 30 | "file=s" => \$file, 31 | "remove" => \$remove, 32 | ) or die("Error in command line arguments\n"); 33 | 34 | use JSON qw( decode_json encode_json to_json from_json); 35 | 36 | my $thegroup = ""; 37 | if ($group) { 38 | $thegroup = "--group $group"; 39 | } 40 | 41 | my @t; 42 | if (@ARGV) { 43 | @t = @ARGV; 44 | }; 45 | 46 | if ($file) { 47 | open F,"$file"; 48 | push @t, ; 49 | close F; 50 | } 51 | 52 | 53 | if ($help || !@t) { 54 | say " 55 | $0 --group 123 --key ABC gr:id 56 | $0 --group 123 --key ABC --file gr_id.txt 57 | 58 | The gr:id combinations are added to Extra > ETH.IAKA for item ABC in group 123. 59 | "; 60 | exit; 61 | }; 62 | 63 | 64 | foreach (@t) { 65 | s/\n//; 66 | }; 67 | 68 | my $str = `./zoteroUpdateField.pl $thegroup --item $item --key extra | jq " .extra "`; 69 | 70 | my @extra ; 71 | if ($str =~ m/\S/s) { 72 | $str =~ s/\n$//s; 73 | $str =~ s/\"$//s; 74 | $str =~ s/^\"//s; 75 | @extra = split(/\\n/,$str); 76 | }; 77 | 78 | push @extra, @t; 79 | 80 | my $string = shell_quote("\"" . join("\\n", @extra) . "\""); 81 | #print $string; 82 | 83 | say `./zoteroUpdateField.pl $thegroup --item $item --key extra --value $string --update`; 84 | 85 | -------------------------------------------------------------------------------- /examples/zotTagToCollectionByName.pl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/perl 2 | use warnings; 3 | use strict; 4 | use open IO => ':encoding(UTF-8)', ':std'; 5 | use utf8; 6 | use feature qw{ say }; 7 | use 5.18.2; 8 | #use String::ShellQuote; 9 | #$string = shell_quote(@list); 10 | use Data::Dumper; 11 | my $home = $ENV{HOME}; 12 | (my $date = `date +'%Y-%m-%d_%H.%M.%S'`) =~ s/\n//; 13 | my $help = ""; 14 | my $string = ""; 15 | my $number = ""; 16 | use Getopt::Long; 17 | GetOptions ( 18 | "string=s" => \$string, 19 | "help" => \$help, 20 | "number=f" => \$number, 21 | ) or die("Error in command line arguments\n"); 22 | 23 | use JSON qw( decode_json encode_json to_json from_json); 24 | my $group = "2129771"; 25 | 26 | my $root = "94GNF2EB"; 27 | $root = "HUMXGGB7"; 28 | 29 | 30 | for $root (qw{HUMXGGB7 YQXNIQJJ X4GFHXUR I4XU5IXD}) { 31 | my $colls = from_json(`zotero-cli --group-id $group collections --key $root | jq '.[] | .data | { key, name } ' | jq -s -c 'sort_by(.name) | .[]' | jq --slurp "." `); 32 | print Dumper($colls); 33 | my %assoc; 34 | foreach (@{$colls}) { 35 | say "$_->{name} ::: $_->{key}"; 36 | $assoc{"C:" . $_->{name}} = $_->{key}; 37 | $assoc{"CC:" . $_->{name}} = $_->{key}; 38 | }; 39 | 40 | 41 | foreach my $tag (keys %assoc) { 42 | say "==================== $tag -> $assoc{$tag} =============================="; 43 | my $coll = $assoc{$tag}; 44 | my $str = `zotero-cli --group-id $group items --filter '{"tag": "$tag"}' | jq '.[] | .data | { key, title, collections } ' | jq --slurp '.' `; 45 | my $keys = from_json($str); 46 | foreach my $item (@{$keys}) { 47 | my $key = $item->{key}; 48 | say "- $key: $item->{title}"; 49 | my $add = 1; 50 | foreach (@{$item->{collections}}) { 51 | if ($_ eq $coll) { 52 | $add = 0; 53 | }; 54 | }; 55 | if ($add) { 56 | say "... adding"; 57 | system("zotero-cli --group-id $group item --key $key --addtocollection $coll | jq \".data.title\""); 58 | }; 59 | }; 60 | }; 61 | }; 62 | 63 | -------------------------------------------------------------------------------- /examples/zotUpdateETHIAKA.pl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/perl 2 | use warnings; 3 | use strict; 4 | use open IO => ':encoding(UTF-8)', ':std'; 5 | use utf8; 6 | use feature qw{ say }; 7 | use 5.18.2; 8 | use String::ShellQuote; 9 | #use Data::Dumper; 10 | my $home = $ENV{HOME}; 11 | (my $date = `date +'%Y-%m-%d_%H.%M.%S'`) =~ s/\n//; 12 | my $help = ""; 13 | my $number = ""; 14 | use Getopt::Long; 15 | my $key = ""; 16 | my $value = ""; 17 | my $update = ""; 18 | my $group = ""; 19 | my $item = ""; 20 | my $file = ""; 21 | my $remove = ""; 22 | GetOptions ( 23 | "help" => \$help, 24 | "number=f" => \$number, 25 | "key=s" => \$key, 26 | "value=s" => \$value, 27 | "update" => \$update, 28 | "group=s" => \$group, 29 | "item=s" => \$item, 30 | "file=s" => \$file, 31 | "remove" => \$remove, 32 | ) or die("Error in command line arguments\n"); 33 | 34 | use JSON qw( decode_json encode_json to_json from_json); 35 | 36 | my $thegroup = ""; 37 | if ($group) { 38 | $thegroup = "--group $group"; 39 | } 40 | 41 | my @t; 42 | if (@ARGV) { 43 | @t = @ARGV; 44 | }; 45 | 46 | if ($file) { 47 | open F,"$file"; 48 | push @t, ; 49 | close F; 50 | } 51 | 52 | 53 | if ($help || !@t) { 54 | say " 55 | $0 --group 123 --key ABC gr:id 56 | $0 --group 123 --key ABC --file gr_id.txt 57 | 58 | The gr:id combinations are added to Extra > ETH.IAKA for item ABC in group 123. 59 | "; 60 | exit; 61 | }; 62 | 63 | 64 | foreach (@t) { 65 | s/\n//; 66 | }; 67 | 68 | my $str = `./zoteroUpdateField.pl $thegroup --item $item --key extra | jq " .extra "`; 69 | 70 | my @extra ; 71 | if ($str =~ m/\S/s) { 72 | $str =~ s/\n$//s; 73 | $str =~ s/\"$//s; 74 | $str =~ s/^\"//s; 75 | @extra = split(/\\n/,$str); 76 | }; 77 | 78 | use List::MoreUtils qw(uniq); 79 | 80 | foreach my $e (@extra) { 81 | if ($e =~ s/^EdTechHub\.ItemAlsoKnownAs\:\s*//) { 82 | $e =~ s/[\;\"]*\s*$//; 83 | my @e = split /\;/,$e; 84 | push @e, @t; 85 | my @ee = sort(uniq(@e)); 86 | $e = "EdTechHub.ItemAlsoKnownAs\: ".join(";",@ee); 87 | #say $e; 88 | }; 89 | }; 90 | 91 | my $string = shell_quote("\"" . join("\\n", @extra) . "\""); 92 | #print $string; 93 | 94 | say `./zoteroUpdateField.pl $thegroup --item $item --key extra --value $string --update`; 95 | 96 | -------------------------------------------------------------------------------- /examples/zotUpdateField.pl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/perl 2 | use warnings; 3 | use strict; 4 | use open IO => ':encoding(UTF-8)', ':std'; 5 | use utf8; 6 | use feature qw{ say }; 7 | use 5.18.2; 8 | #use String::ShellQuote; 9 | #$string = shell_quote(@list); 10 | #use Data::Dumper; 11 | my $home = $ENV{HOME}; 12 | (my $date = `date +'%Y-%m-%d_%H.%M.%S'`) =~ s/\n//; 13 | my $help = ""; 14 | my $string = ""; 15 | my $number = ""; 16 | use Getopt::Long; 17 | my $key = ""; 18 | my $value = ""; 19 | my $update = ""; 20 | my $group = ""; 21 | my $item = ""; 22 | GetOptions ( 23 | "string=s" => \$string, 24 | "help" => \$help, 25 | "number=f" => \$number, 26 | "key=s" => \$key, 27 | "value=s" => \$value, 28 | "update" => \$update, 29 | "group=s" => \$group, 30 | "item=s" => \$item, 31 | ) or die("Error in command line arguments\n"); 32 | 33 | use JSON qw( decode_json encode_json to_json from_json); 34 | 35 | my $thegroup = ""; 36 | if ($group) { 37 | $thegroup = "--group-id $group"; 38 | } 39 | 40 | #my $zkey = "QV86W53S"; 41 | # print `zotero-cli --group-id $group item --key $zkey | jq '.data | { key, version, $key }'`; 42 | my $str = `zotero-cli $thegroup item --key $item | jq '.data'`; 43 | #print $str; 44 | if ($key) { 45 | if (!$value) { 46 | print &jq("{ $key }", $str); 47 | } else { 48 | $str = &jq("{ key, version }", $str); 49 | $str = &jq(". += { \"$key\": $value }", $str); 50 | say $str; 51 | if ($update) { 52 | open F,">$item.update.json"; 53 | print F $str; 54 | close F; 55 | system "zotero-cli --group-id $group update-item --key $item $item.update.json"; 56 | } 57 | }; 58 | } else { 59 | print $str; 60 | }; 61 | 62 | 63 | sub jq() { 64 | use IPC::Open2; 65 | use open IO => ':encoding(UTF-8)', ':std'; 66 | open2(*README, *WRITEME, "jq", "-M", $_[0]); 67 | binmode(*WRITEME, "encoding(UTF-8)"); 68 | binmode(*README, "encoding(UTF-8)"); 69 | print WRITEME $_[1]; 70 | close(WRITEME); 71 | my $output = join "",; 72 | close(README); 73 | return $output; 74 | } 75 | sub jqs() { 76 | use IPC::Open2; 77 | open2(*README, *WRITEME, "jq", "--slurp", "-M", $_[0]); 78 | binmode(*WRITEME, "encoding(UTF-8)"); 79 | binmode(*README, "encoding(UTF-8)"); 80 | print WRITEME $_[1]; 81 | close(WRITEME); 82 | my $output = join "",; 83 | close(README); 84 | return $output; 85 | } 86 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "defaultSeverity": "error", 3 | "extends": [ 4 | "tslint:recommended" 5 | ], 6 | "jsRules": {}, 7 | "rules": { 8 | "no-console": false, 9 | "member-access": false, 10 | "max-classes-per-file": false, 11 | "new-parens": false, 12 | "no-var-requires": false, 13 | "quotemark": [true, "single", "avoid-escape", "avoid-template"], 14 | "max-line-length": { 15 | "severity": "warning", 16 | "options": [240] 17 | }, 18 | "ordered-imports": false, 19 | "prefer-const": [true, {"destructuring": "all"}], 20 | "member-ordering": [true, { 21 | "order": [ 22 | "public-static-field", 23 | "private-static-field", 24 | "public-instance-field", 25 | "protected-instance-field", 26 | "private-instance-field", 27 | "public-constructor", 28 | "public-instance-method", 29 | "protected-instance-method", 30 | "private-constructor", 31 | "private-instance-method" 32 | ] 33 | }], 34 | "no-magic-numbers": [true, -1, 0, 1, 2, 1000], 35 | "prefer-for-of": true, 36 | "no-duplicate-variable": true, 37 | "no-string-throw": true, 38 | "no-var-keyword": true, 39 | "prefer-object-spread": true, 40 | "triple-equals": [true, "allow-undefined-check", "allow-null-check"], 41 | "indent": [true, "spaces", 2], 42 | "linebreak-style": [true, "LF"], 43 | "eofline": true, 44 | "trailing-comma": [true, {"singleline": "never", "multiline": {"objects": "always", "arrays": "always", "functions": "never", "typeLiterals": "ignore"}}], 45 | "arrow-parens": [true, "ban-single-arg-parens"], 46 | "arrow-return-shorthand": true, 47 | "binary-expression-operand-order": true, 48 | "class-name": true, 49 | "no-irregular-whitespace": true, 50 | "no-trailing-whitespace": [true, "ignore-jsdoc", "ignore-template-strings"], 51 | "no-unnecessary-callback-wrapper": true, 52 | "object-literal-key-quotes": [true, "as-needed"], 53 | "object-literal-shorthand": true, 54 | "one-line": [true, "check-catch", "check-finally", "check-else", "check-open-brace"], 55 | "prefer-template": [true, "allow-single-concat"], 56 | "semicolon": [true, "never"], 57 | "space-before-function-paren": [true, {"anonymous": "never", "method": "never", "constructor": "never", "named": "never", "asyncArrow": "always"}], 58 | "variable-name": [true, "ban-keywords", "check-format", "allow-leading-underscore", "allow-snake-case" ], 59 | "curly": [true, "ignore-same-line"], 60 | "only-arrow-functions": [true, "allow-declarations", "allow-named-functions"], 61 | "radix": false, 62 | "object-literal-sort-keys": false, 63 | "no-empty": [true, "allow-empty-catch"], 64 | "no-conditional-assignment": false, 65 | "one-variable-per-declaration": false 66 | }, 67 | "rulesDirectory": [], 68 | "doc": "https://palantir.github.io/tslint/rules/" 69 | } 70 | -------------------------------------------------------------------------------- /examples/zotShowAllCollections.pl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/perl 2 | use warnings; 3 | use strict; 4 | use open IO => ':encoding(UTF-8)', ':std'; 5 | use utf8; 6 | use feature qw{ say }; 7 | use 5.18.2; 8 | #use String::ShellQuote; 9 | #$string = shell_quote(@list); 10 | use Data::Dumper; 11 | my $home = $ENV{HOME}; 12 | (my $date = `date +'%Y-%m-%d_%H.%M.%S'`) =~ s/\n//; 13 | my $help = ""; 14 | my $string = ""; 15 | my $number = ""; 16 | use Getopt::Long; 17 | my $group = "2129771"; 18 | GetOptions ( 19 | "group=s" => \$group, 20 | "help" => \$help, 21 | "number=f" => \$number, 22 | ) or die("Error in command line arguments\n"); 23 | 24 | 25 | use Encode; 26 | use JSON qw( decode_json encode_json to_json from_json); 27 | my $i=0; 28 | my $newstr = encode('utf-8', `zotero-cli --group-id $group get "/collections?limit=100&start=$i"`); 29 | my $res = &jq('length',$newstr); 30 | my $str = $newstr; 31 | print STDERR "Fetched $res"; 32 | while ($res >= 100) { 33 | $i+=100; 34 | $newstr = encode('utf-8', `zotero-cli --group-id $group get "/collections?limit=100&start=$i"`); 35 | $str .= $newstr; 36 | $res = &jq('length',$newstr); 37 | print STDERR "Fetched $res"; 38 | }; 39 | #print $str; 40 | $str = &jq('.[] | .data | {key, name, parentCollection} ', $str); 41 | $str = &jqs('.',$str); 42 | #say $str; 43 | print "Total: ". &jq('length',$str); 44 | 45 | my $data = decode_json($str); 46 | my %name; 47 | #my %tree; 48 | my %children; 49 | my @top; 50 | 51 | foreach (sort @{$data}) { 52 | #$tree{$_->{key}} = $_->{parentCollection}; 53 | $name{$_->{key}} = $_->{name}; 54 | if ($_->{parentCollection}) { 55 | push @{$children{$_->{parentCollection}}}, $_->{key}; 56 | } else { 57 | push @top, $_->{key}; 58 | }; 59 | }; 60 | 61 | my $depth = ""; 62 | my @tree ; 63 | foreach (sort {$name{$a} cmp $name{$b}} @top) { 64 | &show($_); 65 | }; 66 | 67 | sub show { 68 | my $key = $_[0]; 69 | push @tree, $key; 70 | # say "$depth\+ $key: $name{$key}"; 71 | # say join(".", @tree). " " . $name{$key}; 72 | # say join(".", @tree); 73 | $tree[0] =~ s/PFCKJVIL/location/; 74 | $tree[0] =~ s/SGAGGGLK/featured/; 75 | $tree[0] =~ s/WIWEWXZ8/pubtype/; 76 | $tree[0] =~ s/23WS6R2T/theme/; 77 | $tree[0] =~ s/GQH9J3MJ/ref/; 78 | say "https://docs.edtechhub.org/col/" .$tree[$#tree] . "\t" . 79 | "https://docs.edtechhub.org/lib/?" .$tree[0] . "=" .join(".", @tree[1..$#tree]); 80 | $depth .= "| "; 81 | foreach (sort {$name{$a} cmp $name{$b}} @{$children{$key}}) { 82 | &show($_); 83 | }; 84 | chop($depth); 85 | chop($depth); 86 | pop @tree; 87 | return; 88 | }; 89 | 90 | sub jq() { 91 | use IPC::Open2; 92 | open2(*README, *WRITEME, "jq", "-M", $_[0]); 93 | print WRITEME $_[1]; 94 | close(WRITEME); 95 | my $output = join "",; 96 | close(README); 97 | return $output; 98 | } 99 | sub jqs() { 100 | use IPC::Open2; 101 | open2(*README, *WRITEME, "jq", "--slurp", "-M", $_[0]); 102 | print WRITEME $_[1]; 103 | close(WRITEME); 104 | my $output = join "",; 105 | close(README); 106 | return $output; 107 | } 108 | 109 | 110 | -------------------------------------------------------------------------------- /examples/zotListItemsInCollection.pl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/perl 2 | use warnings; 3 | use strict; 4 | use open IO => ':encoding(UTF-8)', ':std'; 5 | use utf8; 6 | use feature qw{ say }; 7 | use 5.18.2; 8 | use JSON qw( decode_json encode_json to_json from_json); 9 | #use String::ShellQuote; 10 | #$string = shell_quote(@list); 11 | use Data::Dumper; 12 | my $home = $ENV{HOME}; 13 | (my $date = `date +'%Y-%m-%d_%H.%M.%S'`) =~ s/\n//; 14 | my $help = ""; 15 | my $string = ""; 16 | my $number = ""; 17 | use Getopt::Long; 18 | GetOptions ( 19 | "string=s" => \$string, 20 | "help" => \$help, 21 | "number=f" => \$number, 22 | ) or die("Error in command line arguments\n"); 23 | 24 | if (!@ARGV || $help) { 25 | print("Need arguments"); 26 | print "Sorry, no help."; 27 | system("less","$0"); 28 | exit; 29 | }; 30 | 31 | use utf8; 32 | my %map; 33 | say "$0 34 | 35 | This script produces a map, whereby old keys (appearing in EdTechHub.ItemAlsoKnownAs) are mapped to the current key. 36 | 37 | $0 zotero://select/groups/2129771/collections/BY3Q9D8Z 38 | 39 | The script illustrates how output from zotero-cli can be processed in perl. 40 | "; 41 | open MAP,">map-listItemsInCollection.txt"; 42 | foreach my $xkey (@ARGV) { 43 | (my $group,my $key) = ($xkey =~ m/(?:zotero\:\/\/select\/groups\/)?(\d+)(?:\/|\:)(?:collections\/)?([\w\d]+)/); 44 | say "$group:$key"; 45 | #print "zotero-cli --group-id $group get '/collections/$key/collections'"; 46 | #my $json = `zotero-cli --group-id $group get '/collections/$key/collections'`; 47 | my $json = `zotero-cli --group-id $group collection --key $key`; 48 | my $d = decode_json($json); 49 | # say Dumper($d); 50 | say "$key > ${$d}{key} - ${$d}{data}{name} (${$d}{'meta'}{'numItems'}, ${$d}{'meta'}{'numCollections'})"; 51 | say "Getting items..."; 52 | my $json2 = `zotero-cli --group-id $group items --collection '$key'`; 53 | my $items = JSON->new->decode($json2); 54 | say "items: $#{$items}"; 55 | my $i=0; 56 | foreach (sort { ${$a}{data}{title} cmp ${$b}{data}{title}} @{$items}) { 57 | my $x = ""; 58 | my $t = ""; 59 | my $aka = ""; 60 | $t = ${$_}{data}{itemType}; 61 | next if $t eq "note"; 62 | next if $t eq "attachment"; 63 | $i++; 64 | if (${$_}{data}{extra}) { 65 | $x = ${$_}{data}{extra}; 66 | my @a = split /\n/,$x; 67 | foreach (@a) { 68 | if (m/EdTechHub\.ItemAlsoKnownAs\:/) { 69 | $aka = $_; 70 | $aka =~ s/EdTechHub\.ItemAlsoKnownAs\:\s*//; 71 | if (!m/^EdTechHub\.ItemAlsoKnownAs/) { 72 | say("Error in 'extra' - stopping"); 73 | say "$group:".${$_}{key}."\t ($t) -> ".$x; 74 | exit("stopping for safety."); 75 | }; 76 | }; 77 | }; 78 | }; 79 | print "------$i------ 80 | Key/Ty: ${$_}{key}\t${$_}{data}{itemType} 81 | Title: ${$_}{data}{title} 82 | AKA: $aka; 83 | "; 84 | foreach my $y (split("\;",$aka)) { 85 | if ($map{$y} || $y eq "$group:${$_}{key}") { 86 | } else { 87 | say MAP "$y\t$group:${$_}{key}"; 88 | $map{$y} = 1; 89 | }; 90 | }; 91 | if ($aka !~ m/2317526/) { 92 | say "$group:".${$_}{key}."\t".$aka; 93 | say("No reference..."); 94 | }; 95 | }; 96 | }; 97 | close MAP; 98 | 99 | exit(); 100 | -------------------------------------------------------------------------------- /docs/COMMANDS.md: -------------------------------------------------------------------------------- 1 | # Zotero API 2 | 3 | Details for 4 | https://www.zotero.org/support/dev/web_api/v3/basics 5 | 6 | `` below means `/users/` or `/groups/`. 7 | 8 | ## Collection 9 | 10 | | URI | Description | zotero-cli | 11 | |---|---|---| 12 | | <prefix>/collections | Collections in the library | collections | 13 | | <prefix>/collections/top | Top-level collections in the library | collections --top | 14 | | <prefix>/collections/<collectionKey> | A specific collection in the library | collections --key ABC | 15 | | <prefix>/collections/<collectionKey>/collections | Subcollections within a specific collection in the library | | 16 | 17 | ## Items 18 | 19 | | URI | Description | zotero-cli | 20 | |---|---|---| 21 | | <prefix>/items | All items in the library, excluding trashed items | items | 22 | | <prefix>/items/top | Top-level items in the library, excluding trashed items | items --top | 23 | | <prefix>/items/trash | Items in the trash | trash | 24 | | <prefix>/items/<itemKey> | A specific item in the library | item --key ABC | 25 | | <prefix>/items/<itemKey>/children | Child items under a specific item | item --key ABC --children | 26 | | <prefix>/publications/items | Items in My Publications | publications | 27 | | <prefix>/collections/<collectionKey>/items | Items within a specific collection in the library | items --collection ABC| 28 | | <prefix>/collections/<collectionKey>/items/top | Top-level items within a specific collection in the library | items --collection --top | 29 | 30 | ## Searches 31 | (Note: Only search metadata is currently available, not search results.) 32 | 33 | | URI | Description | zotero-cli | 34 | |---|---|---| 35 | |<prefix>/searches | All saved searches in the library | searches | 36 | |<prefix>/searches/ | A specific saved search in the library | | 37 | 38 | ## Tags 39 | | URI | Description | zotero-cli | 40 | |---|---|---| 41 | | <prefix>/tags | All tags in the library | tags | 42 | | <prefix>/tags/<url+encoded+tag> | Tags of all types matching a specific name | | 43 | | <prefix>/items/<itemKey>/tags | Tags associated with a specific item | | 44 | | <prefix>/collections/<collectionKey>/tags | Tags within a specific collection in the library | | 45 | | <prefix>/items/tags | All tags in the library, with the ability to filter based on the items | | 46 | | <prefix>/items/top/tags | Tags assigned to top-level items | | 47 | | <prefix>/items/trash/tags | Tags assigned to items in the trash | | 48 | | <prefix>/items/<collectionKey>/items/tags | Tags assigned to items in a given collection | | 49 | | <prefix>/items/<collectionKey>/items/top/tags | Tags assigned to top-level items in a given collection | | 50 | | <prefix>/publications/items/tags | Tags assigned to items in My Publications | | 51 | 52 | ## Other URLs 53 | | URI | Description | zotero-cli | 54 | |---|---|---| 55 | | /keys/<key> | The user id and privileges of the given API key.Use the DELETE HTTP method to delete the key. This should generally be done only by a client that created the key originally using OAuth. | key | 56 | | /users/<userID>/groups | The set of groups the current API key has access to, including public groups the key owner belongs to even if the key doesn't have explicit permissions for them. | | 57 | 58 | -------------------------------------------------------------------------------- /docs/Examples.md: -------------------------------------------------------------------------------- 1 | # Worked examples 2 | 3 | ## Getting started 4 | 5 | Firstly, you need to have your login details ready. Otherwise you need to supply this with each call: 6 | 7 | ``` 8 | --api-key API_KEY 9 | --config CONFIG 10 | --user-id USER_ID *or* --group-id GROUP_ID 11 | --indent INDENT 12 | ``` 13 | 14 | Remember that you can store this in `zotero-cli.toml` too. 15 | 16 | ## What groups do I have access to? 17 | 18 | If you want to access a group collection but don't know the GROUP_ID, find it like this: 19 | 20 | ``` 21 | zotero-cli.js groups 22 | ``` 23 | 24 | # Collections 25 | 26 | ## Display collections 27 | 28 | Once the login details are set up, and you have the GROUP_ID, e.g. show your collections 29 | 30 | ``` 31 | zotero-cli.js collections --help 32 | zotero-cli.js collections 33 | ``` 34 | 35 | Note down a key (K35DEJSM). Show sub-collections of that collection 36 | 37 | ``` 38 | zotero-cli.js collections --key K35DEJSM 39 | ``` 40 | 41 | ## Adding and removing items to/from a collection 42 | 43 | ``` 44 | zotero-cli.js collection --key K35DEJSM --add ITEM_KEY1 ITEM_KEY2 --remove ITEM_KEY3 ITEM_KEY4 45 | ``` 46 | 47 | ## Adding sub-collections 48 | 49 | ``` 50 | zotero-cli.js collections --key K35DEJSM --create-child "Child subcollection1" "Child subcollection 2" 51 | ``` 52 | 53 | ## Adding collections at the top level 54 | 55 | ``` 56 | zotero-cli.js collections --create-child "Child subcollection1" "Child subcollection 2" 57 | ``` 58 | 59 | # Items 60 | 61 | ## Getting item information 62 | 63 | Use the same key (K35DEJSM). Show some items 64 | 65 | ``` 66 | zotero-cli.js items --help 67 | zotero-cli.js items --top 68 | zotero-cli.js items --collection K35DEJSM 69 | ``` 70 | 71 | ## Item types and item fields (with localised names) 72 | 73 | ``` 74 | zotero-cli.js types 75 | zotero-cli.js fields --type=book 76 | ``` 77 | 78 | ## Updating the collections for an item 79 | 80 | Add or remove item from several collections 81 | 82 | ``` 83 | zotero-cli.js item --key ABC --addtocollection=DEF --removefromcollection=GHI,JKL 84 | ``` 85 | 86 | ## Update an existing item: 87 | 88 | Properties not included in the uploaded JSON are left untouched on the server. 89 | 90 | ``` 91 | zotero-cli.js update-item --key ITEM_KEY UPDATE.json 92 | ``` 93 | 94 | With --replace, you submit the item's complete editable JSON to the server, typically by modifying the downloaded editable JSON — that is, the contents of the data property — directly and resubmitting it. 95 | 96 | ``` 97 | zotero-cli.js update-item --key ITEM_KEY NEW.json 98 | ``` 99 | 100 | ## Item creation 101 | 102 | Here is how you use create-item: 103 | 104 | ``` 105 | zotero-cli.js create-item --template book > book.json 106 | gedit book.json 107 | zotero-cli.js create-item book.json 108 | ``` 109 | 110 | For further options, see `zotero-cli.js create-item --h`. 111 | 112 | # Attachments 113 | 114 | ## Getting attachments 115 | 116 | ``` 117 | zotero-cli.js attachment [-h] --key KEY --save SAVE 118 | ``` 119 | 120 | # Searches 121 | 122 | ## Get searches 123 | 124 | ``` 125 | zotero-cli.js searches 126 | ``` 127 | 128 | ## Create new saved search(s) 129 | 130 | Get the json for existing searches, edit, and create. 131 | 132 | ``` 133 | zotero-cli.js searches > search.json 134 | gedit search.json 135 | zotero-cli.js searches --create search.json 136 | ``` 137 | 138 | # Generic get request 139 | 140 | ``` 141 | zotero-cli.js get /apipath 142 | ``` 143 | -------------------------------------------------------------------------------- /examples/zotProcessCollections.pl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/perl 2 | use warnings; 3 | use strict; 4 | use open IO => ':encoding(UTF-8)', ':std'; 5 | use utf8; 6 | use feature qw{ say }; 7 | use 5.18.2; 8 | use JSON qw( decode_json encode_json to_json from_json); 9 | #use String::ShellQuote; 10 | #$string = shell_quote(@list); 11 | use Data::Dumper; 12 | my $home = $ENV{HOME}; 13 | (my $date = `date +'%Y-%m-%d_%H.%M.%S'`) =~ s/\n//; 14 | my $help = ""; 15 | my $string = ""; 16 | my $number = ""; 17 | use Getopt::Long; 18 | my $verbose = ""; 19 | GetOptions ( 20 | "string=s" => \$string, 21 | "help" => \$help, 22 | "number=f" => \$number, 23 | "verbose" => \$verbose, 24 | ) or die("Error in command line arguments\n"); 25 | 26 | if (!@ARGV || $help) { 27 | print("Need arguments"); 28 | print "Sorry, no help."; 29 | system("less","$0"); 30 | exit; 31 | }; 32 | 33 | use utf8; 34 | 35 | say "$0 36 | 37 | This script produces a map of collections, getting items in the collection as well. 38 | 39 | $0 zotero://select/groups/2129771/collections/BY3Q9D8Z 40 | 41 | The script illustrates how output from zotero-cli can be processed in perl. 42 | "; 43 | 44 | 45 | my %map; 46 | open MAP,">map-processCollections.txt"; 47 | foreach my $xkey (@ARGV) { 48 | # zotero://select/groups/2317526/collections/7D7LEFHR 49 | (my $group, my $key) = ($xkey =~ m/(?:zotero\:\/\/select\/groups\/)?(\d+)(?:\/|\:)(?:collections\/)?([\w\d]+)/); 50 | say "$group:$key"; 51 | &getItems($group,$key); 52 | my $json = `zotero-cli --group-id $group get '/collections/$key/collections'`; 53 | my $d = decode_json($json); 54 | my $i=0; 55 | foreach (sort { ${$a}{data}{name} cmp ${$b}{data}{name} } @{$d}) { 56 | $i++; 57 | #say Dumper(\%{$_}); 58 | #exit; 59 | say "$key > ($i) ${$_}{key} - ${$_}{data}{name} (${$_}{'meta'}{'numItems'}, ${$_}{'meta'}{'numCollections'})"; 60 | &getItems($group,${$_}{key}); 61 | }; 62 | }; 63 | close MAP; 64 | 65 | sub getItems() { 66 | (my $group,my $key) = @_; 67 | say "-- $group:$key"; 68 | my $json2 = `zotero-cli --group-id $group items --collection '$key'`; 69 | #my $json2 = `zotero-cli --group-id $group get '/collections/$key/items'`; 70 | my $items = JSON->new->decode($json2); 71 | my $i=0; 72 | foreach (@{$items}) { 73 | my $t = ${$_}{data}{itemType}; 74 | next if $t eq "note"; 75 | next if $t eq "attachment"; 76 | $i++; 77 | my $x = ""; 78 | my $aka = ""; 79 | if (${$_}{data}{extra}) { 80 | $x = ${$_}{data}{extra}; 81 | my @a = split /\n/,$x; 82 | foreach (@a) { 83 | if (m/EdTechHub\.ItemAlsoKnownAs\:/) { 84 | $aka = $_; 85 | $aka =~ s/EdTechHub\.ItemAlsoKnownAs\:\s*//; 86 | if (!m/^EdTechHub\.ItemAlsoKnownAs/) { 87 | say("Error in 'extra' - stopping"); 88 | say "$group:".${$_}{key}."\t ($t) -> ".$x; 89 | exit("stopping for safety."); 90 | }; 91 | }; 92 | }; 93 | }; 94 | #say " --- ".${$_}{key}." ($t) -> ".$aka; 95 | # say MAP "$group:".${$_}{key}."\t".join("\t", split("\;",$aka)); 96 | print "------$i------ 97 | Key/Ty: ${$_}{key}\t${$_}{data}{itemType} 98 | Title: ${$_}{data}{title} 99 | AKA: $aka; 100 | " if $verbose; 101 | foreach my $y (split("\;",$aka)) { 102 | if ($map{$y} || $y eq "$group:${$_}{key}") { 103 | } else { 104 | if ($y =~ m/\:/) { 105 | say MAP "$y\t$group:${$_}{key}"; 106 | }; 107 | $map{$y} = 1; 108 | }; 109 | }; 110 | if ($aka !~ m/2317526/) { 111 | say "$group:".${$_}{key}."\t".$aka; 112 | say("No reference..."); 113 | }; 114 | }; 115 | }; 116 | 117 | exit(); 118 | -------------------------------------------------------------------------------- /examples/zotLinkToURI.pl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/perl 2 | use warnings; 3 | use strict; 4 | use open IO => ':encoding(UTF-8)', ':std'; 5 | use utf8; 6 | use feature qw{ say }; 7 | use 5.18.2; 8 | use String::ShellQuote; 9 | #use Data::Dumper; 10 | my $home = $ENV{HOME}; 11 | (my $date = `date +'%Y-%m-%d_%H.%M.%S'`) =~ s/\n//; 12 | my $help = ""; 13 | my $number = ""; 14 | use Getopt::Long; 15 | my $key = ""; 16 | my $value = ""; 17 | my $update = ""; 18 | my $group = ""; 19 | my $item = ""; 20 | my $file = ""; 21 | my $remove = ""; 22 | my $link = ""; 23 | my $source = ""; 24 | my $first = ""; 25 | my $attach = ""; 26 | my $viewondocs = ""; 27 | my $linktosource = ""; 28 | my $linksourcetocopy = ""; 29 | GetOptions ( 30 | "help" => \$help, 31 | "number=f" => \$number, 32 | "key=s" => \$key, 33 | "value=s" => \$value, 34 | "update" => \$update, 35 | "group=s" => \$group, 36 | "item=s" => \$item, 37 | "file=s" => \$file, 38 | "remove" => \$remove, 39 | "link" => \$link, 40 | "source=s" => \$source, 41 | "first" => \$first, 42 | "attach" => \$attach, 43 | "viewondocs" => \$viewondocs, 44 | "linktosource" => \$linktosource, 45 | "linksourcetocopy" => \$linksourcetocopy, 46 | ) or die("Error in command line arguments\n"); 47 | 48 | use JSON qw( decode_json encode_json to_json from_json); 49 | 50 | if ($help || !@ARGV) { 51 | say " 52 | 53 | Add a link with URI/title: 54 | 55 | $0 --attach zotero://select/groups/2129771/items/UXTXCBCN URI TITLE 56 | 57 | Add preformatted links: 58 | 59 | $0 --viewondocs zotero://select/groups/2129771/items/UXTXCBCN 60 | $0 --linktosource zotero://select/groups/2129771/items/UXTXCBCN 61 | 62 | NOT IMPLEMENTED: 63 | 64 | $0 --update zotero://select/groups/2129771/items/MXSNWAQN URI 65 | $0 --update zotero://select/groups/2129771/items/MXSNWAQN URI TITLE 66 | $0 --updatetitle zotero://select/groups/2129771/items/MXSNWAQN TITLE 67 | $0 --delete zotero://select/groups/2129771/items/MXSNWAQN 68 | 69 | "; 70 | exit; 71 | }; 72 | 73 | my $groupcollection = $ARGV[0]; 74 | if ($groupcollection =~ m|groups/(\d+)/items/([\d\w]+)|) { 75 | $group = $1; 76 | $item = $2; 77 | } else { 78 | die("Add group/item in zotero://... as first argument."); 79 | }; 80 | 81 | 82 | if ($viewondocs) { 83 | system "$0 --attach $groupcollection \"https://docs.opendeved.net/lib/$item\" \"View on docs.opendeved.net\""; 84 | }; 85 | 86 | 87 | if ($linktosource || $linksourcetocopy || $link) { 88 | my $str = from_json(`zotero-cli --group-id $group item --key $item | jq '.data | .extra' `); 89 | if ($linktosource || $link) { 90 | if ($str =~ m/EdTechHub.Source: (\d+)\:([\w\d]+)/s) { 91 | say "Found source: $1:$2"; 92 | system "$0 --attach $groupcollection \"zotero://select/groups/$1/items/$2\" \"View source item in library $1\""; 93 | } else { 94 | say "Did not find source." 95 | }; 96 | }; 97 | if ($linksourcetocopy || $link) { 98 | if ($str =~ m/EdTechHub.Source: (\d+)\:([\w\d]+)/s) { 99 | say "Found source: $1:$2"; 100 | system "$0 --attach \"zotero://select/groups/$1/items/$2\" $groupcollection \"View copy of this item in library $group\""; 101 | } else { 102 | say "Did not find source." 103 | }; 104 | }; 105 | } 106 | 107 | 108 | my $json = <item-$date.json"; 128 | print F $json; 129 | close F; 130 | print `zotero-cli --group-id $group create-item item-$date.json`; 131 | } 132 | 133 | 134 | sub jq() { 135 | use IPC::Open2; 136 | use open IO => ':encoding(UTF-8)', ':std'; 137 | open2(*README, *WRITEME, "jq", "-M", $_[0]); 138 | binmode(*WRITEME, "encoding(UTF-8)"); 139 | binmode(*README, "encoding(UTF-8)"); 140 | print WRITEME $_[1]; 141 | close(WRITEME); 142 | my $output = join "",; 143 | close(README); 144 | return $output; 145 | } 146 | sub jqs() { 147 | use IPC::Open2; 148 | open2(*README, *WRITEME, "jq", "--slurp", "-M", $_[0]); 149 | print WRITEME $_[1]; 150 | close(WRITEME); 151 | my $output = join "",; 152 | close(README); 153 | return $output; 154 | } 155 | -------------------------------------------------------------------------------- /examples/zotItemTracker.pl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/perl 2 | use warnings; 3 | use strict; 4 | use open IO => ':encoding(UTF-8)', ':std'; 5 | use utf8; 6 | use feature qw{ say }; 7 | use 5.18.2; 8 | use String::ShellQuote; 9 | #use Data::Dumper; 10 | my $home = $ENV{HOME}; 11 | (my $date = `date +'%Y-%m-%d_%H.%M.%S'`) =~ s/\n//; 12 | my $help = ""; 13 | my $number = ""; 14 | use Getopt::Long; 15 | my $key = ""; 16 | my $value = ""; 17 | my $update = ""; 18 | my $group = ""; 19 | my $item = ""; 20 | my $file = ""; 21 | my $remove = ""; 22 | my $link = ""; 23 | my $source = ""; 24 | my $first = ""; 25 | GetOptions ( 26 | "help" => \$help, 27 | "number=f" => \$number, 28 | "key=s" => \$key, 29 | "value=s" => \$value, 30 | "update" => \$update, 31 | "group=s" => \$group, 32 | "item=s" => \$item, 33 | "file=s" => \$file, 34 | "remove" => \$remove, 35 | "link" => \$link, 36 | "source=s" => \$source, 37 | "first" => \$first, 38 | ) or die("Error in command line arguments\n"); 39 | 40 | use JSON qw( decode_json encode_json to_json from_json); 41 | 42 | if ($help) { 43 | say " 44 | $0 zotero://select/groups/261495/items/8F4BAW9N 45 | 46 | "; 47 | exit; 48 | }; 49 | 50 | my $groupcollection = $ARGV[0]; 51 | if ($groupcollection =~ m|groups/(\d+)/items/([\d\w]+)|) { 52 | $group = $1; 53 | $item = $2; 54 | } else { 55 | die("XXX"); 56 | }; 57 | 58 | my $ogroup; 59 | my $oitem; 60 | 61 | if ($first) { 62 | $ogroup = $group; 63 | $oitem = $item; 64 | }; 65 | 66 | 67 | if ($source) { 68 | if ($source =~ m|groups/(\d+)/items/([\d\w]+)|) { 69 | $ogroup = $1; 70 | $oitem = $2; 71 | }; 72 | if ($source =~ m|(\d+)\:([\d\w]+)|) { 73 | $ogroup = $1; 74 | $oitem = $2; 75 | }; 76 | } 77 | 78 | my %a; 79 | my %r; 80 | $a{"$group:$item"} = 0; 81 | my $cont = 1; 82 | 83 | while ($cont == 1) { 84 | foreach (keys %a) { 85 | ($group, $item) = (m|^(\d+)\:([\d\w]+)$|); 86 | $group = $1; 87 | $item = $2; 88 | my @a; 89 | if ($a{$_} == 0) { 90 | @a = &getItems($group,$item); 91 | $a{"$group:$item"} = 1; 92 | }; 93 | foreach (@a) { 94 | if (m|^(\d+)\:([\d\w]+)$|) { 95 | if (!$a{$_}) { 96 | $a{$_} = 0; 97 | }; 98 | #print "$_\t$a{$_}\n"; 99 | }; 100 | }; 101 | }; 102 | $cont = 0; 103 | foreach (keys %a) { 104 | if ($a{$_} == 0) { 105 | $cont = 1; 106 | }; 107 | }; 108 | }; 109 | 110 | my %coll =qw{ 111 | 2129771 ode_PUB 112 | 2405685 ETH_PUB 113 | 2339240 ETH_INT 114 | 261495 ode__ICT 115 | 2083138 DIALRDP 116 | }; 117 | 118 | foreach (qw(.title .dateAdded .dateModified .key)) { 119 | &showKey($_); 120 | }; 121 | 122 | 123 | if ($link) { 124 | my @all = keys %r; 125 | my $a = join " " ,@all; 126 | foreach (keys %r) { 127 | ($group, $item) = (m|^(\d+)\:([\d\w]+)$|); 128 | # system "xdg-open zotero://select/groups/$group/items/$item"; 129 | say `./zoteroUpdateETHIAKA.pl --group $group --item $item $a`; 130 | if ($group eq $ogroup && $item eq $oitem) { 131 | say `./zoteroUpdateExtraAppend.pl --group $group --item $item "EdTechHub.Source: "`; 132 | }; 133 | say `./zoteroUpdateExtraAppend.pl --group $group --item $item "EdTechHub.Source: $ogroup:$oitem"`; 134 | #say `./zoteroUpdateExtraAppend.pl --group $ogroup --item $oitem "EdTechHub.Copy: $group:$item"`; 135 | }; 136 | }; 137 | 138 | 139 | sub showKey() { 140 | foreach (keys %r) { 141 | my $y = " "; 142 | if (m/^(\d+)\:/) { 143 | if ($coll{$1}) { 144 | $y = $coll{$1}; 145 | }; 146 | }; 147 | print "$_ $y\t"; 148 | my $x; 149 | #$x = $r{$_}; 150 | $x = &jq(".data | $_[0] ",$r{$_}); 151 | $x =~ s/\n//; 152 | say "$_[0]: $x"; 153 | }; 154 | }; 155 | 156 | 157 | sub showAll() { 158 | foreach (keys %r) { 159 | my $y = ""; 160 | if (m/^(\d+)\:/) { 161 | if ($coll{$1}) { 162 | $y = $coll{$1}; 163 | }; 164 | }; 165 | say "***--- $_ $y ---"; 166 | my $x; 167 | #$x = $r{$_}; 168 | $x = &jq(".data | {title: .title, dateAdded: .dateAdded, dateModified: .dateModified, key: .key} ",$r{$_}); 169 | say $x; 170 | }; 171 | }; 172 | 173 | sub getItems() { 174 | my $group = $_[0]; 175 | my $item = $_[1]; 176 | say "GET--- $group:$item ---"; 177 | # my $str = `./zoteroUpdateField.pl --group $group --item $item --key extra `; 178 | my $st = `zotero-cli --group $group item --key $item`; 179 | my @ee; 180 | if ($st =~ m/\S/s && $st !~ m/StatusCodeError\: 404/) { 181 | $r{"$group:$item"} = $st; 182 | $st = &jq('.data',$st); 183 | if ($st =~ m/\S/s) { 184 | my $str = &jq(".extra",$st); 185 | #say $str; 186 | my @extra ; 187 | use List::MoreUtils qw(uniq); 188 | if ($str =~ m/\S/s) { 189 | $str =~ s/\n$//s; 190 | $str =~ s/\"$//s; 191 | $str =~ s/^\"//s; 192 | @extra = split(/\\n/,$str); 193 | foreach my $e (@extra) { 194 | if ($e =~ s/^EdTechHub\.ItemAlsoKnownAs\:\s*//) { 195 | $e =~ s/[\;\"]*\s*$//; 196 | my @e = split /\;/,$e; 197 | @ee = sort(uniq(@e)); 198 | $e = "EdTechHub.ItemAlsoKnownAs\: ".join(";",@ee); 199 | #say $e; 200 | }; 201 | }; 202 | }; 203 | # "relations": { 204 | # "owl:sameAs": "http://zotero.org/groups/2405685/items/A8G2S2ZT", 205 | # "dc:replaces": "http://zotero.org/groups/2129771/items/SMBVLED9" 206 | my $n = &jq(".relations.\"owl:sameAs\" ",$st); 207 | if ($n && !$ogroup) { 208 | if ($n =~ m|groups/(\d+)/items/([\d\w]+)|) { 209 | $ogroup = $1; 210 | $oitem = $2; 211 | say "Setting source: $ogroup:$oitem"; 212 | }; 213 | }; 214 | my @str = split /\n/, &jq(".relations | .[] ",$st); 215 | foreach (@str) { 216 | if (m|groups/(\d+)/items/([\d\w]+)|) { 217 | push @ee, "$1:$2"; 218 | }; 219 | }; 220 | }; 221 | } else { 222 | $r{"$group:$item"} = ""; 223 | }; 224 | return @ee; 225 | }; 226 | 227 | 228 | sub jq() { 229 | use IPC::Open2; 230 | use open IO => ':encoding(UTF-8)', ':std'; 231 | open2(*README, *WRITEME, "jq", "-M", $_[0]); 232 | binmode(*WRITEME, "encoding(UTF-8)"); 233 | binmode(*README, "encoding(UTF-8)"); 234 | print WRITEME $_[1]; 235 | close(WRITEME); 236 | my $output = join "",; 237 | close(README); 238 | return $output; 239 | } 240 | sub jqs() { 241 | use IPC::Open2; 242 | open2(*README, *WRITEME, "jq", "--slurp", "-M", $_[0]); 243 | print WRITEME $_[1]; 244 | close(WRITEME); 245 | my $output = join "",; 246 | close(README); 247 | return $output; 248 | } 249 | -------------------------------------------------------------------------------- /examples/zotCreateCollectionsAndTags.pl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/perl 2 | use warnings; 3 | use strict; 4 | use open IO => ':encoding(UTF-8)', ':std'; 5 | use utf8; 6 | use feature qw{ say }; 7 | use 5.18.2; 8 | use String::ShellQuote; 9 | use Data::Dumper; 10 | my $home = $ENV{HOME}; 11 | (my $date = `date +'%Y-%m-%d_%H.%M.%S'`) =~ s/\n//; 12 | my $help = ""; 13 | my $string = ""; 14 | my $number = ""; 15 | use Getopt::Long; 16 | my $groupcollection = "zotero://select/groups/2259720/collections/S4WHJHRY"; 17 | my $groupitem = "zotero://select/groups/2259720/items/I88FJV64"; 18 | 19 | GetOptions ( 20 | "groupitem=s" => \$groupitem, 21 | "groupcollection=s" => \$groupcollection, 22 | "help" => \$help, 23 | "number=f" => \$number, 24 | ) or die("Error in command line arguments\n"); 25 | 26 | use Encode; 27 | use JSON qw(decode_json encode_json to_json from_json); 28 | 29 | #my $new = &createColl("PFCKJVIL","ROMP"); 30 | #say &createColl("$new","ROMP2"); 31 | 32 | my %a; 33 | my $group = "2259720"; 34 | my $item = "S4WHJHRY"; 35 | #$a{"Theme"} = "JVMZDEF2"; 36 | 37 | if ($groupcollection) { 38 | if ($groupcollection =~ m|groups/(\d+)/collections/([\d\w]+)|) { 39 | $group = $1; 40 | $item = $2; 41 | say "Group/collection: $group $item"; 42 | }; 43 | }; 44 | my $indent = ""; 45 | open TAG, ">tags.txt"; 46 | while () { 47 | my $i=-1; 48 | s/\n//; 49 | my @a = split /\;/,$_; 50 | my @b; 51 | my $latest = $item; 52 | my $prefix = "_"; 53 | $indent = ""; 54 | foreach (@a) { 55 | $indent .= "\t"; 56 | $i++; 57 | if ($a{$_}) { 58 | say "$indent $_ -> $a{$_}"; 59 | #push @b, $a{$_}; 60 | if ($prefix eq "_" || $prefix ne "_C:") { 61 | $_ =~ m/^(.)/; 62 | $prefix .= "$1:"; 63 | }; 64 | $latest = $a{$_}; 65 | } else { 66 | $a{$_} = &createColl($latest, $_); 67 | $latest = $a{$_}; 68 | say "$indent $_ -> $a{$_}"; 69 | if ($i == $#a) { 70 | say TAG "$prefix$_ $latest"; 71 | say "$indent\t $prefix$_ $latest"; 72 | } else { 73 | if ($prefix eq "_" || $prefix ne "_C:") { 74 | $_ =~ m/^(.)/; 75 | $prefix .= "$1:"; 76 | }; 77 | }; 78 | #push @b, $a{$_}; 79 | }; 80 | }; 81 | }; 82 | close TAG; 83 | 84 | if ($groupitem) { 85 | if ($groupitem =~ m|groups/(\d+)/items/([\d\w]+)|) { 86 | $group = $1; 87 | $item = $2; 88 | say "Group:item: $group $item"; 89 | system("./zoteroUpdateTags.pl --group $group --item $item --file tags.txt"); 90 | }; 91 | }; 92 | 93 | 94 | sub createColl() { 95 | if ($_[0] && $_[1]) { 96 | my $parent = $_[0]; 97 | my $string = shell_quote($_[1]); 98 | say "$indent\t +create: $parent -> $string"; 99 | my $newstr = encode('utf-8', `zotero-cli --group-id $group collections --key $parent --create-child $string`); 100 | #say $newstr; 101 | $newstr =~ m/key\: '([\w\d]+)'/s; 102 | return $1; 103 | } else { 104 | die("$_[0] && $_[1]"); 105 | }; 106 | }; 107 | 108 | 109 | __DATA__ 110 | Theme;Hardware and modality;Audio and Radio 111 | Theme;Hardware and modality;Video and television 112 | Theme;Hardware and modality;Messaging and phone calls 113 | Theme;Hardware and modality;Web-enabled 114 | Theme;Hardware and modality;Application-based 115 | Theme;Hardware and modality;Offline 116 | Theme;Hardware and modality;Print media 117 | Theme;Hardware and modality;Open and Distance Learning 118 | Theme;Within-country contexts;Urban 119 | Theme;Within-country contexts;Peri-urban 120 | Theme;Within-country contexts;Rural 121 | Theme;Within-country contexts;Low connectivity and/or electricity 122 | Theme;Populations;Refugees and migrants 123 | Theme;Populations;Low-literacy levels individuals 124 | Theme;Populations;Girls 125 | Theme;Populations;Disabled and special educational needs individuals 126 | Theme;Populations;Minority groups 127 | Theme;Populations;Out-of-school populations 128 | Theme;Populations;Parents and caretakers 129 | Theme;Populations;School administrators and senior leadership team 130 | Theme;Populations;Ministries of education 131 | Theme;Populations;Trainee teachers 132 | Theme;Populations;Teaching assistants 133 | Theme;Populations;Support and community workers 134 | Theme;Populations;Teacher trainers 135 | Theme;Populations;Teachers 136 | Theme;Educational level;Early childhood and pre-primary 137 | Theme;Educational level;Primary education 138 | Theme;Educational level;Secondary education 139 | Theme;Educational level;Tertiary/higher education 140 | Theme;Educational level;Technical and vocational education and training 141 | Theme;Educational level;Adult education 142 | Theme;Educational level;Teacher education (pre-service and in-service) 143 | Theme;Focus;Access 144 | Theme;Focus;Assessment 145 | Theme;Focus;Cost analysis 146 | Theme;Focus;Equity 147 | Theme;Focus;Governance 148 | Theme;Focus;Monitoring and evaluation 149 | Theme;Focus;Education financing 150 | Theme;Focus;COVID and reopening of schools 151 | Theme;Focus;System readiness 152 | Theme;Focus;Curriculum and educational content 153 | Theme;Focus;Online and distance teaching and learning 154 | Theme;Focus;Educational data 155 | Language of publication;English 156 | Language of publication;French 157 | Language of publication;Arabic 158 | Language of publication;Portugeese 159 | Language of publication;Chinese 160 | Language of publication;Russian 161 | Language of publication;Spanish 162 | Publication type;Case study 163 | Publication type;Blog post 164 | Publication type;Evidence review 165 | Publication type;Systematic literature review 166 | Publication type;Helpdesk response 167 | Publication type;Country scan 168 | Publication type;Working paper 169 | Publication type;Journal article 170 | Publication type;Impact evaluation 171 | Publication type;Policy paper 172 | Publication type;Thesis 173 | Publication type;Book 174 | Publication type;Book chapter 175 | Publication type;Conference paper 176 | Publication type;Web page or website 177 | Publication type;Reference list 178 | Country;Asia;Western Asia;Abkhazia XABKH 179 | Country;Asia;Southern Asia;Afghanistan AFG 180 | Country;Europe;Southern Europe;Albania ALB 181 | Country;Africa;Northern Africa;Algeria DZA 182 | Country;Europe;Southern Europe;Andorra AND 183 | Country;Africa;Middle Africa;Angola AGO 184 | Country;Americas;Caribbean;Antigua and Barbuda ATG 185 | Country;Americas;South America;Argentina ARG 186 | Country;Asia;Western Asia;Armenia ARM 187 | Country;Asia;Western Asia;Artsakh XARTH 188 | Country;Oceania;Australia and New Zealand;Australia AUS 189 | Country;Europe;Western Europe;Austria AUT 190 | Country;Asia;Western Asia;Azerbaijan AZE 191 | Country;Americas;Caribbean;Bahamas BHS 192 | Country;Asia;Western Asia;Bahrain BHR 193 | Country;Asia;Southern Asia;Bangladesh BGD 194 | Country;Americas;Caribbean;Barbados BRB 195 | Country;Europe;Eastern Europe;Belarus BLR 196 | Country;Europe;Western Europe;Belgium BEL 197 | Country;Americas;Central America;Belize BLZ 198 | Country;Africa;Western Africa;Benin BEN 199 | Country;Asia;Southern Asia;Bhutan BTN 200 | Country;Americas;South America;Bolivia BOL 201 | Country;Europe;Southern Europe;Bosnia and Herzegovina BIH 202 | Country;Africa;Southern Africa;Botswana BWA 203 | Country;Americas;South America;Brazil BRA 204 | Country;Asia;South-estern Asia;Brunei Darussalam BRN 205 | Country;Europe;Eastern Europe;Bulgaria BGR 206 | Country;Africa;Western Africa;Burkina Faso BFA 207 | Country;Africa;Eastern Africa;Burundi BDI 208 | Country;Asia;South-estern Asia;Cambodia KHM 209 | Country;Africa;Middle Africa;Cameroon CMR 210 | Country;Americas;Northern America;Canada CAN 211 | Country;Africa;Western Africa;Cape Verde CPV 212 | Country;Europe;Western Europe;Catalan Republic XCATA 213 | Country;Africa;Middle Africa;Central African Republic CAF 214 | Country;Africa;Middle Africa;Chad TCD 215 | Country;Americas;South America;Chile CHL 216 | Country;Asia;Eastern Asia;China CHN 217 | Country;Americas;South America;Colombia COL 218 | Country;Africa;Eastern Africa;Comoros COM 219 | Country;Africa;Middle Africa;Democratic Republic of the Congo COD 220 | Country;Africa;Middle Africa;Republic of the Congo COG 221 | Country;Africa;Middle Africa;Congo XCO 222 | Country;Americas;Central America;Costa Rica CRI 223 | Country;Africa;Western Africa;Ivory Coast CIV 224 | Country;Europe;Southern Europe;Croatia HRV 225 | Country;Americas;Caribbean;Cuba CUB 226 | Country;Asia;Western Asia;Cyprus CYP 227 | Country;Europe;Eastern Europe;Czech Republic CZE 228 | Country;Europe;Northern Europe;Denmark DNK 229 | Country;Africa;Eastern Africa;Djibouti DJI 230 | Country;Americas;Caribbean;Dominican Republic DOM 231 | Country;Americas;Caribbean;Dominica DMA 232 | Country;Asia;South-estern Asia;Timor-L'este TLS 233 | Country;Americas;South America;Ecuador ECU 234 | Country;Africa;Northern Africa;Egypt EGY 235 | Country;Americas;Central America;El Salvador SLV 236 | Country;Africa;Eastern Africa;Eritrea ERI 237 | Country;Europe;Northern Europe;Estonia EST 238 | Country;Africa;Southern Africa;eSwatini SWZ 239 | Country;Africa;Eastern Africa;Ethiopia ETH 240 | Country;Oceania;Melanesia;Fiji FJI 241 | Country;Europe;Northern Europe;Finland FIN 242 | Country;Europe;Western Europe;France FRA 243 | Country;Africa;Middle Africa;Gabon GAB 244 | Country;Africa;Western Africa;Gambia GMB 245 | Country;Asia;Western Asia;Georgia GEO 246 | Country;Europe;Western Europe;Germany DEU 247 | Country;Africa;Western Africa;Ghana GHA 248 | Country;Europe;Southern Europe;Greece GRC 249 | Country;Americas;Caribbean;Grenada GRD 250 | Country;Americas;Central America;Guatemala GTM 251 | Country;Africa;Western Africa;Guinea-Bissau GNB 252 | Country;Africa;Middle Africa;Equatorial Guinea GNQ 253 | Country;Africa;Western Africa;Guinea GIN 254 | Country;Americas;South America;Guyana GUY 255 | Country;Americas;Caribbean;Haiti HTI 256 | Country;Americas;Central America;Honduras HND 257 | Country;Europe;Eastern Europe;Hungary HUN 258 | Country;Europe;Northern Europe;Iceland ISL 259 | Country;Asia;Southern Asia;India IND 260 | Country;Asia;South-estern Asia;Indonesia IDN 261 | Country;Asia;Southern Asia;Iran IRN 262 | Country;Asia;Western Asia;Iraq IRQ 263 | Country;Europe;Northern Europe;Ireland IRL 264 | Country;Asia;Western Asia;Israel ISR 265 | Country;Europe;Southern Europe;Italy ITA 266 | Country;Americas;Caribbean;Jamaica JAM 267 | Country;Asia;Eastern Asia;Japan JPN 268 | Country;Asia;Western Asia;Jordan JOR 269 | Country;Asia;Central Asia;Kazakhstan KAZ 270 | Country;Africa;Eastern Africa;Kenya KEN 271 | Country;Oceania;Micronesia;Kiribati KIR 272 | Country;Asia;Eastern Asia;North Korea PRK 273 | Country;Asia;Eastern Asia;Korea XKOR 274 | Country;Asia;Eastern Asia;Korea (Republic of) KOR 275 | Country;Europe;Southern Europe;Kosovo XKSVO 276 | Country;Asia;Western Asia;Kurdistan XKRDN 277 | Country;Asia;Western Asia;Kuwait KWT 278 | Country;Asia;Central Asia;Kyrgyzstan KGZ 279 | Country;Asia;South-estern Asia;Laos LAO 280 | Country;Europe;Northern Europe;Latvia LVA 281 | Country;Asia;Western Asia;Lebanon LBN 282 | Country;Africa;Southern Africa;Lesotho LSO 283 | Country;Africa;Western Africa;Liberia LBR 284 | Country;Africa;Northern Africa;Libya LBY 285 | Country;Europe;Western Europe;Liechtenstein LIE 286 | Country;Europe;Northern Europe;Lithuania LTU 287 | Country;Europe;Western Europe;Luxembourg LUX 288 | Country;Africa;Eastern Africa;Madagascar MDG 289 | Country;Africa;Eastern Africa;Malawi MWI 290 | Country;Asia;South-estern Asia;Malaysia MYS 291 | Country;Asia;Southern Asia;Maldives MDV 292 | Country;Africa;Western Africa;Mali MLI 293 | Country;Europe;Southern Europe;Malta MLT 294 | Country;Oceania;Micronesia;Marshall Islands MHL 295 | Country;Africa;Western Africa;Mauritania MRT 296 | Country;Africa;Eastern Africa;Mauritius MUS 297 | Country;Americas;Central America;Mexico MEX 298 | Country;Oceania;Micronesia;Federated States of Micronesia FSM 299 | Country;Europe;Eastern Europe;Republic of Moldova MDA 300 | Country;Europe;Eastern Europe;Pridnestrovian Moldovan Republic XPRMR 301 | Country;Europe;Western Europe;Monaco MCO 302 | Country;Asia;Eastern Asia;Mongolia MNG 303 | Country;Europe;Southern Europe;Montenegro MNE 304 | Country;Africa;Northern Africa;Morocco MAR 305 | Country;Africa;Eastern Africa;Mozambique MOZ 306 | Country;Asia;South-estern Asia;Myanmar MMR 307 | Country;Africa;Southern Africa;Namibia NAM 308 | Country;Oceania;Micronesia;Nauru NRU 309 | Country;Asia;Southern Asia;Nepal NPL 310 | Country;Europe;Western Europe;Netherlands NLD 311 | Country;Oceania;Australia and New Zealand;New Zealand NZL 312 | Country;Americas;Central America;Nicaragua NIC 313 | Country;Africa;Western Africa;Niger NER 314 | Country;Africa;Western Africa;Nigeria NGA 315 | Country;Asia;Western Asia;North Cyprus XNCYP 316 | Country;Europe;Southern Europe;North Macedonia MKD 317 | Country;Europe;Northern Europe;Norway NOR 318 | Country;Asia;Western Asia;Oman OMN 319 | Country;Asia;Southern Asia;Pakistan PAK 320 | Country;Oceania;Micronesia;Palau PLW 321 | Country;Asia;Western Asia;State of Palestine PSE 322 | Country;Americas;Central America;Panama PAN 323 | Country;Oceania;Melanesia;Papua New Guinea PNG 324 | Country;Americas;South America;Paraguay PRY 325 | Country;Americas;South America;Peru PER 326 | Country;Asia;South-estern Asia;Philippines PHL 327 | Country;Europe;Eastern Europe;Poland POL 328 | Country;Europe;Southern Europe;Portugal PRT 329 | Country;Africa;Eastern Africa;Puntland XPTLD 330 | Country;Asia;Western Asia;Qatar QAT 331 | Country;Europe;Eastern Europe;Romania ROU 332 | Country;Europe;Eastern Europe;Russian Federation RUS 333 | Country;Africa;Eastern Africa;Rwanda RWA 334 | Country;Africa;North Africa;Sahrawi Arab Democratic Republic XSADR 335 | Country;Americas;Caribbean;Saint Kitts and Nevis KNA 336 | Country;Americas;Caribbean;Saint Lucia LCA 337 | Country;Americas;Caribbean;Saint Vincent and the Grenadines VCT 338 | Country;Oceania;Polynesia;Samoa WSM 339 | Country;Europe;Southern Europe;San Marino SMR 340 | Country;Africa;Middle Africa;São Tomé and Príncipe STP 341 | Country;Asia;Western Asia;Saudi Arabia SAU 342 | Country;Africa;Western Africa;Senegal SEN 343 | Country;Europe;Southern Europe;Serbia SRB 344 | Country;Africa;Eastern Africa;Seychelles SYC 345 | Country;Africa;Western Africa;Sierra Leone SLE 346 | Country;Asia;South-estern Asia;Singapore SGP 347 | Country;Europe;Eastern Europe;Slovakia SVK 348 | Country;Europe;Southern Europe;Slovenia SVN 349 | Country;Oceania;Melanesia;Solomon Islands SLB 350 | Country;Africa;Eastern Africa;Somalia SOM 351 | Country;Africa;Eastern Africa;Somaliland XSMLD 352 | Country;Africa;Southern Africa;South Africa ZAF 353 | Country;Asia;Eastern Asia;South Korea KOR 354 | Country;Asia;Western Asia;South Ossetia XOSSA 355 | Country;Africa;Eastern Africa;South Sudan SSD 356 | Country;Europe;Southern Europe;Spain ESP 357 | Country;Asia;Southern Asia;Sri Lanka LKA 358 | Country;Africa;Northern Africa;Sudan SDN 359 | Country;Americas;South America;Suriname SUR 360 | Country;Europe;Northern Europe;Sweden SWE 361 | Country;Europe;Western Europe;Switzerland CHE 362 | Country;Asia;Western Asia;Syrian Arab Republic SYR 363 | Country;Asia;Central Asia;Tajikistan TJK 364 | Country;Africa;Eastern Africa;United Republic of Tanzania TZA 365 | Country;Asia;South-estern Asia;Thailand THA 366 | Country;Asia;Southern Asia;Tibet XTIBT 367 | Country;Africa;Western Africa;Togo TGO 368 | Country;Oceania;Polynesia;Tonga TON 369 | Country;Americas;Caribbean;Trinidad and Tobago TTO 370 | Country;Africa;Northern Africa;Tunisia TUN 371 | Country;Asia;Western Asia;Turkey TUR 372 | Country;Asia;Central Asia;Turkmenistan TKM 373 | Country;Oceania;Polynesia;Tuvalu TUV 374 | Country;Africa;Eastern Africa;Uganda UGA 375 | Country;Europe;Eastern Europe;Ukraine UKR 376 | Country;Asia;Western Asia;United Arab Emirates ARE 377 | Country;Europe;Northern Europe;United Kingdom GBR 378 | Country;Americas;Northern America;United States USA 379 | Country;Americas;South America;Uruguay URY 380 | Country;Asia;Central Asia;Uzbekistan UZB 381 | Country;Oceania;Melanesia;Vanuatu VUT 382 | Country;Americas;South America;Holy See VAT 383 | Country;Americas;South America;Venezuela VEN 384 | Country;Asia;South-estern Asia;Viet Nam VNM 385 | Country;Asia;Western Asia;Yemen YEM 386 | Country;Africa;Eastern Africa;Zambia ZMB 387 | Country;Africa;Eastern Africa;Zimbabwe ZWE 388 | 389 | -------------------------------------------------------------------------------- /bin/zotero-cli.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | require('dotenv').config() 4 | require('docstring') 5 | const os = require('os') 6 | 7 | import { ArgumentParser } from 'argparse' 8 | import { parse as TOML } from '@iarna/toml' 9 | import fs = require('fs') 10 | import path = require('path') 11 | 12 | import request = require('request-promise') 13 | import * as LinkHeader from 'http-link-header' 14 | 15 | import Ajv = require('ajv') 16 | const ajv = new Ajv() 17 | 18 | const md5 = require('md5-file') 19 | 20 | function sleep(msecs) { 21 | return new Promise(resolve => setTimeout(resolve, msecs)) 22 | } 23 | 24 | const arg = new class { 25 | integer(v) { 26 | if (isNaN(parseInt(v))) throw new Error(`${JSON.stringify(v)} is not an integer`) 27 | return parseInt(v) 28 | } 29 | 30 | file(v) { 31 | if (!fs.existsSync(v) || !fs.lstatSync(v).isFile()) throw new Error(`${JSON.stringify(v)} is not a file`) 32 | return v 33 | } 34 | 35 | path(v) { 36 | if (!fs.existsSync(v)) throw new Error(`${JSON.stringify(v)} does not exist`) 37 | return v 38 | } 39 | 40 | json(v) { 41 | return JSON.parse(v) 42 | } 43 | } 44 | 45 | class Zotero { 46 | args: any 47 | output: string = '' 48 | parser: any 49 | config: any 50 | zotero: any 51 | base = 'https://api.zotero.org' 52 | headers = { 53 | 'User-Agent': 'Zotero-CLI', 54 | 'Zotero-API-Version': '3', 55 | } 56 | 57 | async run() { 58 | this.output = '' 59 | // global parameters for all commands 60 | this.parser = new ArgumentParser 61 | this.parser.addArgument('--api-key', { help: 'The API key to access the Zotero API.' }) 62 | this.parser.addArgument('--config', { type: arg.file, help: 'Configuration file (toml format). Note that ./zotero-cli.toml and ~/.config/zotero-cli/zotero-cli.toml is picked up automatically.' }) 63 | this.parser.addArgument('--user-id', { type: arg.integer, help: 'The id of the user library.' }) 64 | this.parser.addArgument('--group-id', { type: arg.integer, help: 'The id of the group library.' }) 65 | // See below. If changed, add: You can provide the group-id as zotero-select link (zotero://...). Only the group-id is used, the item/collection id is discarded. 66 | this.parser.addArgument('--indent', { type: arg.integer, help: 'Identation for json output.' }) 67 | this.parser.addArgument('--out', { help: 'Output to file' }) 68 | this.parser.addArgument('--verbose', { action: 'storeTrue', help: 'Log requests.' }) 69 | 70 | const subparsers = this.parser.addSubparsers({ title: 'commands', dest: 'command', required: true }) 71 | // add all methods that do not start with _ as a command 72 | for (const cmd of Object.getOwnPropertyNames(Object.getPrototypeOf(this)).sort()) { 73 | if (typeof this[cmd] !== 'function' || cmd[0] !== '$') continue 74 | 75 | const sp = subparsers.addParser(cmd.slice(1).replace(/_/g, '-'), { description: this[cmd].__doc__, help: this[cmd].__doc__ }) 76 | // when called with an argparser, the command is expected to add relevant parameters and return 77 | // the command must have a docstring 78 | this[cmd](sp) 79 | } 80 | 81 | this.args = this.parser.parseArgs() 82 | 83 | // pick up config 84 | const config: string = [this.args.config, 'zotero-cli.toml', `${os.homedir()}/.config/zotero-cli/zotero-cli.toml`].find(cfg => fs.existsSync(cfg)) 85 | this.config = config ? TOML(fs.readFileSync(config, 'utf-8')) : {} 86 | 87 | if (this.args.user_id || this.args.group_id) { 88 | //Overwriting command line option in config 89 | delete this.config['user-id'] 90 | delete this.config['group-id'] 91 | 92 | this.config['user-id'] = this.args.user_id 93 | this.config['group-id'] = this.args.group_id 94 | 95 | if (!this.config['user-id']) delete this.config['user-id'] 96 | if (!this.config['group-id']) delete this.config['group-id'] 97 | } 98 | 99 | // expand selected command 100 | const options = [].concat.apply([], this.parser._actions.map(action => action.dest === 'command' ? action.choices[this.args.command] : [action])) 101 | for (const option of options) { 102 | if (!option.dest) continue 103 | if (['help', 'config'].includes(option.dest)) continue 104 | 105 | if (this.args[option.dest] !== null) continue 106 | 107 | let value 108 | 109 | // first try explicit config 110 | if (typeof value === 'undefined' && this.args.config) { 111 | value = (this.config[this.args.command] || {})[option.dest.replace(/_/g, '-')] 112 | if (typeof value === 'undefined') value = this.config[option.dest.replace(/_/g, '-')] 113 | } 114 | 115 | // next, ENV vars. Also picks up from .env 116 | if (typeof value === 'undefined') { 117 | value = process.env[`ZOTERO_CLI_${option.dest.toUpperCase()}`] || process.env[`ZOTERO_${option.dest.toUpperCase()}`] 118 | } 119 | 120 | // last, implicit config 121 | if (typeof value === 'undefined') { 122 | value = (this.config[this.args.command] || {})[option.dest.replace(/_/g, '-')] 123 | if (typeof value === 'undefined') value = this.config[option.dest.replace(/_/g, '-')] 124 | } 125 | 126 | if (typeof value === 'undefined') continue 127 | 128 | if (option.type === arg.integer) { 129 | if (isNaN(parseInt(value))) this.parser.error(`${option.dest} must be numeric, not ${value}`) 130 | value = parseInt(value) 131 | 132 | } else if (option.type === arg.path) { 133 | if (!fs.existsSync(value)) this.parser.error(`${option.dest}: ${value} does not exist`) 134 | 135 | } else if (option.type === arg.file) { 136 | if (!fs.existsSync(value) || !fs.lstatSync(value).isFile()) this.parser.error(`${option.dest}: ${value} is not a file`) 137 | 138 | } else if (option.type === arg.json && typeof value === 'string') { 139 | try { 140 | value = JSON.parse(value) 141 | } catch (err) { 142 | this.parser.error(`${option.dest}: ${JSON.stringify(value)} is not valid JSON`) 143 | } 144 | 145 | } else if (option.choices) { 146 | if (!option.choices.includes(value)) this.parser.error(`${option.dest} must be one of ${option.choices}`) 147 | 148 | } else if (option.action === 'storeTrue' && typeof value === 'string') { 149 | const _value = { 150 | true: true, 151 | yes: true, 152 | on: true, 153 | 154 | false: false, 155 | no: false, 156 | off: false, 157 | }[value] 158 | if (typeof _value === 'undefined') this.parser.error(`%{option.dest} must be boolean, not ${value}`) 159 | value = _value 160 | 161 | } else { 162 | // string 163 | } 164 | 165 | this.args[option.dest] = value 166 | } 167 | 168 | if (!this.args.api_key) this.parser.error('no API key provided') 169 | this.headers['Zotero-API-Key'] = this.args.api_key 170 | 171 | if (this.args.user_id === null && this.args.group_id === null) this.parser.error('You must provide exactly one of --user-id or --group-id') 172 | if (this.args.user_id !== null && this.args.group_id !== null) this.parser.error('You must provide exactly one of --user-id or --group-id') 173 | if (this.args.user_id === 0) this.args.user_id = (await this.get(`/keys/${this.args.api_key}`, { userOrGroupPrefix: false })).userID 174 | 175 | 176 | /* 177 | // Could do this here: 178 | if (this.args.group_id) { 179 | this.args.group_id = this.extractGroup(this.args.group_id) 180 | if (!this.args.group_id) { 181 | this.parser.error('Unable to extract group_id from the string provided via --group_id.') 182 | return 183 | } 184 | } 185 | */ 186 | 187 | // using default=2 above prevents the overrides from being picked up 188 | if (this.args.indent === null) this.args.indent = 2 189 | 190 | // call the actual command 191 | try { 192 | await this['$' + this.args.command.replace(/-/g, '_')]() 193 | } catch (ex) { 194 | this.print('Command execution failed: ', ex) 195 | process.exit(1) 196 | } 197 | 198 | if (this.args.out) fs.writeFileSync(this.args.out, this.output) 199 | } 200 | 201 | public print(...args: any[]) { 202 | if (!this.args.out) { 203 | console.log.apply(console, args) 204 | 205 | } else { 206 | this.output += args.map(m => { 207 | const type = typeof m 208 | 209 | if (type === 'string' || m instanceof String || type === 'number' || type === 'undefined' || type === 'boolean' || m === null) return m 210 | 211 | if (m instanceof Error) return `` 212 | 213 | if (m && type === 'object' && m.message) return `` 214 | 215 | return JSON.stringify(m, null, this.args.indent) 216 | 217 | }).join(' ') + '\n' 218 | } 219 | } 220 | 221 | async all(uri, params = {}) { 222 | let chunk = await this.get(uri, { resolveWithFullResponse: true, params }) 223 | let data = chunk.body 224 | 225 | let link = chunk.headers.link && LinkHeader.parse(chunk.headers.link).rel('next') 226 | while (link && link.length && link[0].uri) { 227 | if (chunk.headers.backoff) await sleep(parseInt(chunk.headers.backoff) * 1000) 228 | 229 | chunk = await request({ 230 | uri: link[0].uri, 231 | headers: this.headers, 232 | json: true, 233 | resolveWithFullResponse: true, 234 | }) 235 | data = data.concat(chunk.body) 236 | link = chunk.headers.link && LinkHeader.parse(chunk.headers.link).rel('next') 237 | } 238 | return data 239 | } 240 | 241 | // The Zotero API uses several commands: get, post, patch, delete - these are defined below. 242 | 243 | async get(uri, options: { userOrGroupPrefix?: boolean, params?: any, resolveWithFullResponse?: boolean, json?: boolean } = {}) { 244 | if (typeof options.userOrGroupPrefix === 'undefined') options.userOrGroupPrefix = true 245 | if (typeof options.params === 'undefined') options.params = {} 246 | if (typeof options.json === 'undefined') options.json = true 247 | 248 | let prefix = '' 249 | if (options.userOrGroupPrefix) prefix = this.args.user_id ? `/users/${this.args.user_id}` : `/groups/${this.args.group_id}` 250 | 251 | const params = Object.keys(options.params).map(param => { 252 | let values = options.params[param] 253 | if (!Array.isArray(values)) values = [values] 254 | return values.map(v => `${param}=${encodeURI(v)}`).join('&') 255 | }).join('&') 256 | 257 | uri = `${this.base}${prefix}${uri}${params ? '?' + params : ''}` 258 | if (this.args.verbose) console.error('GET', uri) 259 | 260 | return request({ 261 | uri, 262 | headers: this.headers, 263 | encoding: null, 264 | json: options.json, 265 | resolveWithFullResponse: options.resolveWithFullResponse, 266 | }) 267 | } 268 | 269 | async post(uri, data, headers = {}) { 270 | const prefix = this.args.user_id ? `/users/${this.args.user_id}` : `/groups/${this.args.group_id}` 271 | 272 | uri = `${this.base}${prefix}${uri}` 273 | if (this.args.verbose) console.error('POST', uri) 274 | 275 | return request({ 276 | method: 'POST', 277 | uri, 278 | headers: { ...this.headers, 'Content-Type': 'application/json', ...headers }, 279 | body: data, 280 | }) 281 | } 282 | 283 | async put(uri, data) { 284 | const prefix = this.args.user_id ? `/users/${this.args.user_id}` : `/groups/${this.args.group_id}` 285 | 286 | uri = `${this.base}${prefix}${uri}` 287 | if (this.args.verbose) console.error('PUT', uri) 288 | 289 | return request({ 290 | method: 'PUT', 291 | uri, 292 | headers: { ...this.headers, 'Content-Type': 'application/json' }, 293 | body: data, 294 | }) 295 | } 296 | 297 | async patch(uri, data, version?: number) { 298 | const prefix = this.args.user_id ? `/users/${this.args.user_id}` : `/groups/${this.args.group_id}` 299 | 300 | const headers = { ...this.headers, 'Content-Type': 'application/json' } 301 | if (typeof version !== 'undefined') headers['If-Unmodified-Since-Version'] = version 302 | 303 | uri = `${this.base}${prefix}${uri}` 304 | if (this.args.verbose) console.error('PATCH', uri) 305 | 306 | return request({ 307 | method: 'PATCH', 308 | uri, 309 | headers, 310 | body: data, 311 | }) 312 | } 313 | 314 | async delete(uri, version?: number) { 315 | const prefix = this.args.user_id ? `/users/${this.args.user_id}` : `/groups/${this.args.group_id}` 316 | 317 | const headers = { ...this.headers, 'Content-Type': 'application/json' } 318 | if (typeof version !== 'undefined') headers['If-Unmodified-Since-Version'] = version 319 | 320 | uri = `${this.base}${prefix}${uri}` 321 | if (this.args.verbose) console.error('DELETE', uri) 322 | 323 | return request({ 324 | method: 'DELETE', 325 | uri, 326 | headers, 327 | }) 328 | } 329 | 330 | async count(uri, params = {}) { 331 | return (await this.get(uri, { resolveWithFullResponse: true, params })).headers['total-results'] 332 | } 333 | 334 | show(v) { 335 | this.print(JSON.stringify(v, null, this.args.indent).replace(new RegExp(this.args.api_key, 'g'), '')) 336 | } 337 | 338 | extractKeyAndSetGroup(key) { 339 | // zotero://select/groups/(\d+)/(items|collections)/([A-Z01-9]+) 340 | var out = key; 341 | var res = key.match(/^zotero\:\/\/select\/groups\/(library|\d+)\/(items|collections)\/([A-Z01-9]+)/) 342 | if (res) { 343 | if (res[2] == "library") { 344 | console.log('You cannot specify zotero-select links (zotero://...) to select user libraries.') 345 | return 346 | } else { 347 | // console.log("Key: zotero://-key provided for "+res[2]+" Setting group-id.") 348 | this.args.group_id = res[1] 349 | out = res[3] 350 | }; 351 | } 352 | return out 353 | } 354 | 355 | /// THE COMMANDS /// 356 | // The following functions define key API commands: /keys, /collection, /collections, etc. 357 | 358 | // https://www.zotero.org/support/dev/web_api/v3/basics 359 | // Collections 360 | // /collections Collections in the library 361 | // /collections/top Top-level collections in the library 362 | // /collections/ A specific collection in the library 363 | // /collections//collections Subcollections within a specific collection in the library 364 | 365 | // TODO: --create-child should go into 'collection'. 366 | 367 | async $collections(argparser = null) { 368 | /** Retrieve a list of collections or create a collection. (API: /collections, /collections/top, /collections//collections). Use 'collections --help' for details. */ 369 | 370 | if (argparser) { 371 | argparser.addArgument('--top', { action: 'storeTrue', help: 'Show only collection at top level.' }) 372 | argparser.addArgument('--key', { help: 'Show all the child collections of collection with key. You can provide the key as zotero-select link (zotero://...) to also set the group-id.' }) 373 | argparser.addArgument('--create-child', { nargs: '*', help: 'Create child collections of key (or at the top level if no key is specified) with the names specified.' }) 374 | return 375 | } 376 | 377 | if (this.args.key) { 378 | this.args.key = this.extractKeyAndSetGroup(this.args.key) 379 | if (!this.args.key) { 380 | this.parser.error('Unable to extract group/key from the string provided.') 381 | return 382 | } 383 | } 384 | 385 | if (this.args.create_child) { 386 | const response = await this.post('/collections', 387 | JSON.stringify(this.args.create_child.map(c => { return { name: c, parentCollection: this.args.key } }))) 388 | this.print('Collections created: ', JSON.parse(response).successful) 389 | return 390 | } 391 | 392 | 393 | let collections = null; 394 | if (this.args.key) { 395 | collections = await this.all(`/collections/${this.args.key}/collections`) 396 | } else { 397 | collections = await this.all(`/collections${this.args.top ? '/top' : ''}`) 398 | } 399 | 400 | this.show(collections) 401 | } 402 | 403 | // Operate on a specific collection. 404 | // /collections//items Items within a specific collection in the library 405 | // /collections//items/top Top-level items within a specific collection in the library 406 | 407 | // TODO: --create-child should go into 'collection'. 408 | // DONE: Why is does the setup for --add and --remove differ? Should 'add' not be "nargs: '*'"? Remove 'itemkeys'? 409 | // TODO: Add option "--output file.json" to pipe output to file. 410 | 411 | async $collection(argparser = null) { 412 | /** 413 | Retrieve information about a specific collection --key KEY (API: /collections/KEY or /collections/KEY/tags). Use 'collection --help' for details. 414 | (Note: Retrieve items is a collection via 'items --collection KEY'.) 415 | */ 416 | 417 | if (argparser) { 418 | argparser.addArgument('--key', { required: true, help: 'The key of the collection (required). You can provide the key as zotero-select link (zotero://...) to also set the group-id.' }) 419 | argparser.addArgument('--tags', { action: 'storeTrue', help: 'Display tags present in the collection.' }) 420 | // argparser.addArgument('itemkeys', { nargs: '*' , help: 'Item keys for items to be added or removed from this collection.'}) 421 | argparser.addArgument('--add', { nargs: '*', help: 'Add items to this collection. Note that adding items to collections with \'item --addtocollection\' may require fewer API queries. (Convenience method: patch item->data->collections.)' }) 422 | argparser.addArgument('--remove', { nargs: '*', help: 'Convenience method: Remove items from this collection. Note that removing items from collections with \'item --removefromcollection\' may require fewer API queries. (Convenience method: patch item->data->collections.)' }) 423 | return 424 | } 425 | 426 | if (this.args.key) { 427 | this.args.key = this.extractKeyAndSetGroup(this.args.key) 428 | if (!this.args.key) { 429 | this.parser.error('Unable to extract group/key from the string provided.') 430 | return 431 | } 432 | } 433 | 434 | if (this.args.tags && this.args.add) { 435 | this.parser.error('--tags cannot be combined with --add') 436 | return 437 | } 438 | if (this.args.tags && this.args.remove) { 439 | this.parser.error('--tags cannot be combined with --remove') 440 | return 441 | } 442 | /* 443 | if (this.args.add && !this.args.itemkeys.length) { 444 | this.parser.error('--add requires item keys') 445 | return 446 | } 447 | if (!this.args.add && this.args.itemkeys.length) { 448 | this.parser.error('unexpected item keys') 449 | return 450 | } 451 | */ 452 | if (this.args.add) { 453 | for (const itemKey of this.args.add) { 454 | const item = await this.get(`/items/${itemKey}`) 455 | if (item.data.collections.includes(this.args.key)) continue 456 | await this.patch(`/items/${itemKey}`, JSON.stringify({ collections: item.data.collections.concat(this.args.key) }), item.version) 457 | } 458 | } 459 | 460 | if (this.args.remove) { 461 | for (const itemKey of this.args.remove) { 462 | const item = await this.get(`/items/${itemKey}`) 463 | const index = item.data.collections.indexOf(this.args.key) 464 | if (index > -1) { 465 | item.data.collections.splice(index, 1) 466 | } 467 | await this.patch(`/items/${itemKey}`, JSON.stringify({ collections: item.data.collections }), item.version) 468 | } 469 | } 470 | 471 | this.show(await this.get(`/collections/${this.args.key}${this.args.tags ? '/tags' : ''}`)) 472 | } 473 | 474 | // URI Description 475 | // https://www.zotero.org/support/dev/web_api/v3/basics 476 | // /items All items in the library, excluding trashed items 477 | // /items/top Top-level items in the library, excluding trashed items 478 | 479 | async $items(argparser = null) { 480 | /** 481 | Retrieve list of items from API. (API: /items, /items/top, /collections/COLLECTION/items/top). 482 | Use 'items --help' for details. 483 | By default, all items are retrieved. With --top or limit (via --filter) the default number of items are retrieved. 484 | */ 485 | 486 | let items 487 | 488 | if (argparser) { 489 | argparser.addArgument('--count', { action: 'storeTrue', help: 'Return the number of items.' }) 490 | // argparser.addArgument('--all', { action: 'storeTrue', help: 'obsolete' }) 491 | argparser.addArgument('--filter', { type: arg.json, help: 'Provide a filter as described in the Zotero API documentation under read requests / parameters. For example: \'{"format": "json,bib", "limit": 100, "start": 100}\'.' }) 492 | argparser.addArgument('--collection', { help: 'Retrive list of items for collection. You can provide the collection key as a zotero-select link (zotero://...) to also set the group-id.' }) 493 | argparser.addArgument('--top', { action: 'storeTrue', help: 'Retrieve top-level items in the library/collection (excluding child items / attachments, excluding trashed items).' }) 494 | argparser.addArgument('--validate', { type: arg.path, help: 'json-schema file for all itemtypes, or directory with schema files, one per itemtype.' }) 495 | return 496 | } 497 | 498 | if (this.args.count && this.args.validate) { 499 | this.parser.error('--count cannot be combined with --validate') 500 | return 501 | } 502 | 503 | if (this.args.collection) { 504 | this.args.collection = this.extractKeyAndSetGroup(this.args.collection) 505 | if (!this.args.collection) { 506 | this.parser.error('Unable to extract group/key from the string provided.') 507 | return 508 | } 509 | } 510 | 511 | const collection = this.args.collection ? `/collections/${this.args.collection}` : '' 512 | 513 | if (this.args.count) { 514 | this.print(await this.count(`${collection}/items${this.args.top ? '/top' : ''}`, this.args.filter || {})) 515 | return 516 | } 517 | 518 | const params = this.args.filter || {} 519 | 520 | if (this.args.top) { 521 | // This should be all - there may be more than 100 items. 522 | // items = await this.all(`${collection}/items/top`, { params }) 523 | items = await this.all(`${collection}/items/top`, params ) 524 | } else if (params.limit) { 525 | if (params.limit > 100) { 526 | this.parser.error('You can only retrieve up to 100 items with with params.limit.') 527 | return 528 | } 529 | items = await this.get(`${collection}/items`, { params }) 530 | } else { 531 | items = await this.all(`${collection}/items`, params) 532 | } 533 | 534 | if (this.args.validate) { 535 | if (!fs.existsSync(this.args.validate)) throw new Error(`${this.args.validate} does not exist`) 536 | 537 | const oneSchema = fs.lstatSync(this.args.validate).isFile() 538 | 539 | let validate = oneSchema ? ajv.compile(JSON.parse(fs.readFileSync(this.args.validate, 'utf-8'))) : null 540 | 541 | const validators = {} 542 | // still a bit rudimentary 543 | for (const item of items) { 544 | if (!oneSchema) { 545 | validate = validators[item.itemType] = validators[item.itemType] || ajv.compile(JSON.parse(fs.readFileSync(path.join(this.args.validate, `${item.itemType}.json`), 'utf-8'))) 546 | } 547 | 548 | if (!validate(item)) this.show(validate.errors) 549 | } 550 | 551 | } else { 552 | this.show(items) 553 | } 554 | } 555 | 556 | // https://www.zotero.org/support/dev/web_api/v3/basics 557 | // /items/ A specific item in the library 558 | // /items//children Child items under a specific item 559 | 560 | async $item(argparser = null) { 561 | /** 562 | Retrieve an item (item --key KEY), save/add file attachments, retrieve children. Manage collections and tags. (API: /items/KEY/ or /items/KEY/children). 563 | 564 | Also see 'attachment', 'create' and 'update'. 565 | */ 566 | 567 | if (argparser) { 568 | argparser.addArgument('--key', { required: true, help: 'The key of the item. You can provide the key as zotero-select link (zotero://...) to also set the group-id.' }) 569 | argparser.addArgument('--children', { action: 'storeTrue', help: 'Retrieve list of children for the item.' }) 570 | argparser.addArgument('--filter', { type: arg.json, help: 'Provide a filter as described in the Zotero API documentation under read requests / parameters. To retrieve multiple items you have use "itemkey"; for example: \'{"format": "json,bib", "itemkey": "A,B,C"}\'. See https://www.zotero.org/support/dev/web_api/v3/basics#search_syntax.' }) 571 | argparser.addArgument('--addfile', { nargs: '*', help: 'Upload attachments to the item. (/items/new)' }) 572 | argparser.addArgument('--savefiles', { nargs: '*', help: 'Download all attachments from the item (/items/KEY/file).' }) 573 | argparser.addArgument('--addtocollection', { nargs: '*', help: 'Add item to collections. (Convenience method: patch item->data->collections.)' }) 574 | argparser.addArgument('--removefromcollection', { nargs: '*', help: 'Remove item from collections. (Convenience method: patch item->data->collections.)' }) 575 | argparser.addArgument('--addtags', { nargs: '*', help: 'Add tags to item. (Convenience method: patch item->data->tags.)' }) 576 | argparser.addArgument('--removetags', { nargs: '*', help: 'Remove tags from item. (Convenience method: patch item->data->tags.)' }) 577 | return 578 | } 579 | 580 | 581 | if (this.args.key) { 582 | this.args.key = this.extractKeyAndSetGroup(this.args.key) 583 | if (!this.args.key) { 584 | this.parser.error('Unable to extract group/key from the string provided.') 585 | return 586 | } 587 | } 588 | 589 | const item = await this.get(`/items/${this.args.key}`) 590 | 591 | if (this.args.savefiles) { 592 | let children = await this.get(`/items/${this.args.key}/children`); 593 | await Promise.all(children.filter(item => item.data.itemType === 'attachment').map(async item => { 594 | console.log(`Downloading file ${item.data.filename}`) 595 | fs.writeFileSync(item.data.filename, await this.get(`/items/${item.key}/file`), 'binary') 596 | })) 597 | } 598 | 599 | if (this.args.addfile) { 600 | const attachmentTemplate = await this.get('/items/new?itemType=attachment&linkMode=imported_file', { userOrGroupPrefix: false }) 601 | for (const filename of this.args.addfile) { 602 | if (!fs.existsSync(filename)) { 603 | console.log(`Ignoring non-existing file: ${filename}`); 604 | return 605 | } 606 | 607 | let attach = attachmentTemplate; 608 | attach.title = path.basename(filename) 609 | attach.filename = path.basename(filename) 610 | attach.contentType = `application/${path.extname(filename).slice(1)}` 611 | attach.parentItem = this.args.key 612 | const stat = fs.statSync(filename) 613 | const uploadItem = JSON.parse(await this.post('/items', JSON.stringify([attach]))) 614 | const uploadAuth = JSON.parse(await this.post(`/items/${uploadItem.successful[0].key}/file?md5=${md5.sync(filename)}&filename=${attach.filename}&filesize=${fs.statSync(filename)['size']}&mtime=${stat.mtimeMs}`, '{}', { 'If-None-Match': '*' })) 615 | if (uploadAuth.exists !== 1) { 616 | const uploadResponse = await request({ 617 | method: 'POST', 618 | uri: uploadAuth.url, 619 | body: Buffer.concat([Buffer.from(uploadAuth.prefix), fs.readFileSync(filename), Buffer.from(uploadAuth.suffix)]), 620 | headers: { 'Content-Type': uploadAuth.contentType } 621 | }) 622 | await this.post(`/items/${uploadItem.successful[0].key}/file?upload=${uploadAuth.uploadKey}`, '{}', { 'Content-Type': 'application/x-www-form-urlencoded', 'If-None-Match': '*' }) 623 | } 624 | } 625 | } 626 | 627 | if (this.args.addtocollection) { 628 | let newCollections = item.data.collections 629 | this.args.addtocollection.forEach(itemKey => { 630 | if (!newCollections.includes(itemKey)) { 631 | newCollections.push(itemKey) 632 | } 633 | }) 634 | await this.patch(`/items/${this.args.key}`, JSON.stringify({ collections: newCollections }), item.version) 635 | } 636 | 637 | if (this.args.removefromcollection) { 638 | let newCollections = item.data.collections 639 | this.args.removefromcollection.forEach(itemKey => { 640 | const index = newCollections.indexOf(itemKey) 641 | if (index > -1) { 642 | newCollections.splice(index, 1) 643 | } 644 | }) 645 | await this.patch(`/items/${this.args.key}`, JSON.stringify({ collections: newCollections }), item.version) 646 | } 647 | 648 | if (this.args.addtags) { 649 | let newTags = item.data.tags 650 | this.args.addtags.forEach(tag => { 651 | if (!newTags.find(newTag => newTag.tag === tag)) { 652 | newTags.push({ tag }) 653 | } 654 | }) 655 | await this.patch(`/items/${this.args.key}`, JSON.stringify({ tags: newTags }), item.version) 656 | } 657 | 658 | if (this.args.removetags) { 659 | let newTags = item.data.tags.filter(tag => !this.args.removetags.includes(tag.tag)) 660 | await this.patch(`/items/${this.args.key}`, JSON.stringify({ tags: newTags }), item.version) 661 | } 662 | 663 | const params = this.args.filter || {} 664 | if (this.args.children) { 665 | this.show(await this.get(`/items/${this.args.key}/children`, { params })) 666 | } else { 667 | this.show(await this.get(`/items/${this.args.key}`, { params })) 668 | } 669 | } 670 | 671 | async $attachment(argparser = null) { 672 | /** 673 | Retrieve/save file attachments for the item specified with --key KEY (API: /items/KEY/file). 674 | Also see 'item', which has options for adding/saving file attachments. 675 | */ 676 | 677 | if (argparser) { 678 | argparser.addArgument('--key', { required: true, help: 'The key of the item. You can provide the key as zotero-select link (zotero://...) to also set the group-id.' }) 679 | argparser.addArgument('--save', { required: true, help: 'Filename to save attachment to.' }) 680 | return 681 | } 682 | 683 | if (this.args.key) { 684 | this.args.key = this.extractKeyAndSetGroup(this.args.key) 685 | if (!this.args.key) { 686 | this.parser.error('Unable to extract group/key from the string provided.') 687 | return 688 | } 689 | } 690 | 691 | fs.writeFileSync(this.args.save, await this.get(`/items/${this.args.key}/file`), 'binary') 692 | } 693 | 694 | async $create_item(argparser = null) { 695 | /** 696 | Create a new item or items. (API: /items/new) You can retrieve a template with the --template option. 697 | 698 | Use this option to create both top-level items, as well as child items (including notes and links). 699 | */ 700 | 701 | if (argparser) { 702 | argparser.addArgument('--template', { help: "Retrieve a template for the item you wish to create. You can retrieve the template types using the main argument 'types'." }) 703 | argparser.addArgument('items', { nargs: '*', help: 'Json files for the items to be created.' }) 704 | return 705 | } 706 | 707 | if (this.args.template) { 708 | this.show(await this.get('/items/new', { userOrGroupPrefix: false, params: { itemType: this.args.template } })) 709 | return 710 | } 711 | 712 | if (!this.args.items.length) this.parser.error('Need at least one item to create') 713 | 714 | const items = this.args.items.map(item => JSON.parse(fs.readFileSync(item, 'utf-8'))) 715 | this.print(await this.post('/items', JSON.stringify(items))) 716 | } 717 | 718 | async $update_item(argparser = null) { 719 | /** Update/replace an item (--key KEY), either update (API: patch /items/KEY) or replacing (using --replace, API: put /items/KEY). */ 720 | 721 | if (argparser) { 722 | argparser.addArgument('--key', { required: true, help: 'The key of the item. You can provide the key as zotero-select link (zotero://...) to also set the group-id.' }) 723 | argparser.addArgument('--replace', { action: 'storeTrue', help: 'Replace the item by sumbitting the complete json.' }) 724 | argparser.addArgument('items', { nargs: 1, help: 'Path of item files in json format.' }) 725 | return 726 | } 727 | 728 | if (this.args.key) { 729 | this.args.key = this.extractKeyAndSetGroup(this.args.key) 730 | if (!this.args.key) { 731 | this.parser.error('Unable to extract group/key from the string provided.') 732 | return 733 | } 734 | } 735 | 736 | const originalItem = await this.get(`/items/${this.args.key}`) 737 | for (const item of this.args.items) { 738 | await this[this.args.replace ? 'put' : 'patch'](`/items/${this.args.key}`, fs.readFileSync(item), originalItem.version) 739 | } 740 | } 741 | 742 | // /items/trash Items in the trash 743 | 744 | async $trash(argparser = null) { 745 | /** Return a list of items in the trash. */ 746 | 747 | if (argparser) return 748 | 749 | const items = await this.get('/items/trash') 750 | this.show(items) 751 | } 752 | 753 | 754 | // https://www.zotero.org/support/dev/web_api/v3/basics 755 | // /publications/items Items in My Publications 756 | 757 | async $publications(argparser = null) { 758 | /** Return a list of items in publications (user library only). (API: /publications/items) */ 759 | 760 | if (argparser) return 761 | 762 | const items = await this.get('/publications/items') 763 | this.show(items) 764 | } 765 | 766 | // itemTypes 767 | 768 | async $types(argparser = null) { 769 | /** Retrieve a list of items types available in Zotero. (API: /itemTypes) */ 770 | 771 | if (argparser) return 772 | 773 | this.show(await this.get('/itemTypes', { userOrGroupPrefix: false })) 774 | } 775 | 776 | async $groups(argparser = null) { 777 | /** Retrieve the Zotero groups data to which the current library_id and api_key has access to. (API: /users//groups) */ 778 | if (argparser) return 779 | 780 | this.show(await this.get('/groups')) 781 | } 782 | 783 | async $fields(argparser = null) { 784 | /** 785 | * Retrieve a template with the fields for --type TYPE (API: /itemTypeFields, /itemTypeCreatorTypes) or all item fields (API: /itemFields). 786 | * Note that to retrieve a template, use 'create-item --template TYPE' rather than this command. 787 | */ 788 | 789 | if (argparser) { 790 | argparser.addArgument('--type', { help: 'Display fields types for TYPE.' }) 791 | return 792 | } 793 | 794 | if (this.args.type) { 795 | this.show(await this.get('/itemTypeFields', { params: { itemType: this.args.type }, userOrGroupPrefix: false })) 796 | this.show(await this.get('/itemTypeCreatorTypes', { params: { itemType: this.args.type }, userOrGroupPrefix: false })) 797 | } else { 798 | this.show(await this.get('/itemFields', { userOrGroupPrefix: false })) 799 | } 800 | } 801 | 802 | // Searches 803 | // https://www.zotero.org/support/dev/web_api/v3/basics 804 | 805 | async $searches(argparser = null) { 806 | /** Return a list of the saved searches of the library. Create new saved searches. (API: /searches) */ 807 | 808 | if (argparser) { 809 | argparser.addArgument('--create', { nargs: 1, help: 'Path of JSON file containing the definitions of saved searches.' }) 810 | return 811 | } 812 | 813 | if (this.args.create) { 814 | let searchDef = []; 815 | try { 816 | searchDef = JSON.parse(fs.readFileSync(this.args.create[0], 'utf8')) 817 | } catch (ex) { 818 | console.log('Invalid search definition: ', ex) 819 | } 820 | 821 | if (!Array.isArray(searchDef)) { 822 | searchDef = [searchDef] 823 | } 824 | 825 | await this.post('/searches', JSON.stringify(searchDef)) 826 | this.print('Saved search(s) created successfully.') 827 | return 828 | } 829 | 830 | const items = await this.get('/searches') 831 | this.show(items) 832 | } 833 | 834 | // Tags 835 | 836 | async $tags(argparser = null) { 837 | /** Return a list of tags in the library. Options to filter and count tags. (API: /tags) */ 838 | 839 | if (argparser) { 840 | argparser.addArgument('--filter', { help: 'Tags of all types matching a specific name.' }) 841 | argparser.addArgument('--count', { action: 'storeTrue', help: 'TODO: document' }) 842 | return 843 | } 844 | 845 | let rawTags = null; 846 | if (this.args.filter) { 847 | rawTags = await this.all(`/tags/${encodeURIComponent(this.args.filter)}`) 848 | } else { 849 | rawTags = await this.all('/tags') 850 | } 851 | const tags = rawTags.map(tag => tag.tag).sort() 852 | 853 | if (this.args.count) { 854 | const tag_counts: Record = {} 855 | for (const tag of tags) { 856 | tag_counts[tag] = await this.count('/items', { tag }) 857 | } 858 | this.print(tag_counts) 859 | 860 | } else { 861 | this.show(tags) 862 | } 863 | } 864 | 865 | // Other URLs 866 | // https://www.zotero.org/support/dev/web_api/v3/basics 867 | // /keys/ 868 | // /users//groups 869 | 870 | async $key(argparser = null) { 871 | /** Show details about this API key. (API: /keys ) */ 872 | 873 | if (argparser) return 874 | 875 | this.show(await this.get(`/keys/${this.args.api_key}`, { userOrGroupPrefix: false })) 876 | } 877 | 878 | // Functions for get, post, put, patch, delete. (Delete query to API with uri.) 879 | 880 | async $get(argparser = null) { 881 | /** Make a direct query to the API using 'GET uri'. */ 882 | 883 | if (argparser) { 884 | argparser.addArgument('--root', { action: 'storeTrue', help: 'TODO: document' }) 885 | argparser.addArgument('uri', { nargs: '+', help: 'TODO: document' }) 886 | return 887 | } 888 | 889 | for (const uri of this.args.uri) { 890 | this.show(await this.get(uri, { userOrGroupPrefix: !this.args.root })) 891 | } 892 | } 893 | 894 | async $post(argparser = null) { 895 | /** Make a direct query to the API using 'POST uri [--data data]'. */ 896 | 897 | if (argparser) { 898 | argparser.addArgument('uri', { nargs: '1', help: 'TODO: document' }) 899 | argparser.addArgument('--data', { required: true, help: 'Escaped JSON string for post data' }) 900 | return 901 | } 902 | 903 | this.print(await this.post(this.args.uri, this.args.data)) 904 | } 905 | 906 | async $put(argparser = null) { 907 | /** Make a direct query to the API using 'PUT uri [--data data]'. */ 908 | 909 | if (argparser) { 910 | argparser.addArgument('uri', { nargs: '1', help: 'TODO: document' }) 911 | argparser.addArgument('--data', { required: true, help: 'Escaped JSON string for post data' }) 912 | return 913 | } 914 | 915 | this.print(await this.put(this.args.uri, this.args.data)) 916 | } 917 | 918 | async $delete(argparser = null) { 919 | /** Make a direct delete query to the API using 'DELETE uri'. */ 920 | 921 | if (argparser) { 922 | argparser.addArgument('uri', { nargs: '+', help: 'Request uri' }) 923 | return 924 | } 925 | 926 | for (const uri of this.args.uri) { 927 | const response = await this.get(uri) 928 | await this.delete(uri, response.version) 929 | } 930 | } 931 | } 932 | 933 | (new Zotero).run().catch(err => { 934 | console.error('error:', err) 935 | process.exit(1) 936 | }) 937 | --------------------------------------------------------------------------------