···1515 return zotero.Zotero(library_id="0", library_type="user", local=True, locale=locale)
161617171818+def _normalize_doi(doi):
1919+ """Normalise a DOI for case-insensitive matching.
2020+2121+ Strips common prefixes (https://doi.org/, http://doi.org/, doi:) and converts to lowercase.
2222+ DOIs are case-insensitive per the DOI specification.
2323+ """
2424+ if not doi:
2525+ return ""
2626+2727+ # Strip whitespace
2828+ doi = doi.strip()
2929+3030+ # Strip common prefixes
3131+ prefixes = ["https://doi.org/", "http://doi.org/", "doi:"]
3232+ for prefix in prefixes:
3333+ if doi.lower().startswith(prefix.lower()):
3434+ doi = doi[len(prefix) :]
3535+ break
3636+3737+ # Convert to lowercase for case-insensitive matching
3838+ return doi.lower().strip()
3939+4040+1841@click.group()
1942@click.version_option(version=__version__, prog_name="pyzotero")
2043@click.option(
···391414 err=True,
392415 )
393416 sys.exit(1)
417417+ except Exception as e:
418418+ click.echo(f"Error: {e!s}", err=True)
419419+ sys.exit(1)
420420+421421+422422+@main.command()
423423+@click.argument("dois", nargs=-1)
424424+@click.option(
425425+ "--json",
426426+ "output_json",
427427+ is_flag=True,
428428+ help="Output results as JSON",
429429+)
430430+@click.pass_context
431431+def alldoi(ctx, dois, output_json): # noqa: PLR0912
432432+ """Look up DOIs in the local Zotero library and return their Zotero IDs.
433433+434434+ Accepts one or more DOIs as arguments and checks if they exist in the library.
435435+ DOI matching is case-insensitive and handles common prefixes (https://doi.org/, doi:).
436436+437437+ If no DOIs are provided, shows "No items found" (text) or {} (JSON).
438438+439439+ Examples:
440440+ pyzotero alldoi 10.1234/example
441441+442442+ pyzotero alldoi 10.1234/abc https://doi.org/10.5678/def doi:10.9012/ghi
443443+444444+ pyzotero alldoi 10.1234/example --json
445445+446446+ """
447447+ try:
448448+ locale = ctx.obj.get("locale", "en-US")
449449+ zot = _get_zotero_client(locale)
450450+451451+ # Build a mapping of normalized DOIs to (original_doi, zotero_key)
452452+ click.echo("Building DOI index from library...", err=True)
453453+ doi_map = {}
454454+455455+ # Get all items using everything() which handles pagination automatically
456456+ all_items = zot.everything(zot.items())
457457+458458+ # Process all items
459459+ for item in all_items:
460460+ data = item.get("data", {})
461461+ item_doi = data.get("DOI", "")
462462+463463+ if item_doi:
464464+ normalized_doi = _normalize_doi(item_doi)
465465+ item_key = data.get("key", "")
466466+467467+ if normalized_doi and item_key:
468468+ # Store the original DOI from Zotero and the item key
469469+ doi_map[normalized_doi] = (item_doi, item_key)
470470+471471+ click.echo(f"Indexed {len(doi_map)} items with DOIs", err=True)
472472+473473+ # If no DOIs provided, return empty result
474474+ if not dois:
475475+ if output_json:
476476+ click.echo(json.dumps({}))
477477+ else:
478478+ click.echo("No items found")
479479+ return
480480+481481+ # Look up each input DOI
482482+ found = []
483483+ not_found = []
484484+485485+ for input_doi in dois:
486486+ normalized_input = _normalize_doi(input_doi)
487487+488488+ if normalized_input in doi_map:
489489+ original_doi, zotero_key = doi_map[normalized_input]
490490+ found.append({"doi": original_doi, "key": zotero_key})
491491+ else:
492492+ not_found.append(input_doi)
493493+494494+ # Output results
495495+ if output_json:
496496+ result = {"found": found, "not_found": not_found}
497497+ click.echo(json.dumps(result, indent=2))
498498+ else:
499499+ if found:
500500+ click.echo(f"\nFound {len(found)} items:\n")
501501+ for item in found:
502502+ click.echo(f" {item['doi']} → {item['key']}")
503503+ else:
504504+ click.echo("No items found")
505505+506506+ if not_found:
507507+ click.echo(f"\nNot found ({len(not_found)}):")
508508+ for doi in not_found:
509509+ click.echo(f" {doi}")
510510+394511 except Exception as e:
395512 click.echo(f"Error: {e!s}", err=True)
396513 sys.exit(1)