Showing preview only (2,193K chars total). Download the full file or copy to clipboard to get everything.
Repository: ajslater/codex
Branch: main
Commit: ec9e22f4e447
Files: 768
Total size: 2.0 MB
Directory structure:
gitextract_jcsuoc91/
├── .circleci/
│ └── config.yml
├── .dockerignore
├── .github/
│ └── workflows/
│ └── ci.yml
├── .gitignore
├── .picopt_treestamps.yaml
├── .prettierignore
├── .readthedocs.yaml
├── .shellcheckrc
├── CLAUDE.md
├── Dockerfile
├── LICENSE
├── Makefile
├── NEWS.md
├── README.md
├── bin/
│ ├── benchmark-opds.sh
│ ├── build-choices.sh
│ ├── build-dist.sh
│ ├── ci-download-dist-if-identical.sh
│ ├── clean-pycache.sh
│ ├── collectstatic.sh
│ ├── create-output-dirs.sh
│ ├── delete-files.sh
│ ├── dev-docker.sh
│ ├── dev-module.sh
│ ├── dev-prod-server.sh
│ ├── dev-reverse-proxy.sh
│ ├── dev-server.sh
│ ├── dev-ttabs.sh
│ ├── docker-compose-exit.sh
│ ├── docker-tag-latest.sh
│ ├── fix-docker.sh
│ ├── fix-python.sh
│ ├── fix.sh
│ ├── icons_transform.py
│ ├── kill-codex.sh
│ ├── kill-eslint_d.sh
│ ├── lint-ci.sh
│ ├── lint-complexity.sh
│ ├── lint-darwin.sh
│ ├── lint-docker.sh
│ ├── lint-python.sh
│ ├── lint.sh
│ ├── localize-db.sh
│ ├── localize_library.sql
│ ├── manage.py
│ ├── pm
│ ├── prettier-nginx.sh
│ ├── roman.py
│ ├── sort-ignore.sh
│ ├── test-python.sh
│ ├── uml.sh
│ ├── update-deps-node.sh
│ ├── update-deps-python.sh
│ ├── vendor-diff-package.sh
│ ├── vendor-patch-imports.sh
│ ├── version-node.sh
│ └── version-python.sh
├── cfg/
│ ├── ci.mk
│ ├── codex.mk
│ ├── common.mk
│ ├── django.mk
│ ├── docker.mk
│ ├── docs.mk
│ ├── eslint.config.base.js
│ ├── frontend.mk
│ ├── help.mk
│ ├── node.mk
│ ├── node_root.mk
│ └── python.mk
├── ci/
│ ├── Dockerfile
│ ├── base.Dockerfile
│ ├── builder-base.Dockerfile
│ ├── circleci-step-halt.sh
│ ├── cleanup-repo.py
│ ├── debian.sources
│ ├── dev.Dockerfile
│ ├── dist-builder.Dockerfile
│ ├── docker-bake.hcl
│ ├── docker-build-image.sh
│ ├── docker-compose-exit.sh
│ ├── docker-init.sh
│ ├── docker-push.sh
│ ├── docker-tag-remote-version-as-latest.sh
│ ├── machine-arch.sh
│ ├── machine-env.sh
│ ├── machine-init.sh
│ ├── machine-packages.sh
│ ├── package.Dockerfile
│ ├── python-publish.sh
│ ├── version-checksum.sh
│ ├── version-codex-base.sh
│ ├── version-codex-builder-base.sh
│ ├── version-codex-dist-builder.sh
│ ├── versions-create-env.sh
│ └── versions-env-filename.sh
├── codex/
│ ├── __init__.py
│ ├── applications/
│ │ ├── __init__.py
│ │ ├── lifespan.py
│ │ └── websocket.py
│ ├── asgi.py
│ ├── authentication.py
│ ├── choices/
│ │ ├── __init__.py
│ │ ├── admin.py
│ │ ├── browser.py
│ │ ├── choices_to_json.py
│ │ ├── jobs.py
│ │ ├── notifications.py
│ │ ├── reader.py
│ │ ├── search.py
│ │ └── statii.py
│ ├── librarian/
│ │ ├── README.md
│ │ ├── __init__.py
│ │ ├── bookmark/
│ │ │ ├── __init__.py
│ │ │ ├── bookmarkd.py
│ │ │ ├── latest_version.py
│ │ │ ├── tasks.py
│ │ │ ├── update.py
│ │ │ └── user_active.py
│ │ ├── covers/
│ │ │ ├── __init__.py
│ │ │ ├── coverd.py
│ │ │ ├── create.py
│ │ │ ├── path.py
│ │ │ ├── purge.py
│ │ │ ├── status.py
│ │ │ └── tasks.py
│ │ ├── cron/
│ │ │ ├── __init__.py
│ │ │ └── crond.py
│ │ ├── fs/
│ │ │ ├── README.md
│ │ │ ├── __init__.py
│ │ │ ├── event_batcherd.py
│ │ │ ├── events.py
│ │ │ ├── filters.py
│ │ │ ├── poller/
│ │ │ │ ├── __init__.py
│ │ │ │ ├── events.py
│ │ │ │ ├── poller.py
│ │ │ │ ├── snapshot.py
│ │ │ │ ├── snapshot_diff.py
│ │ │ │ ├── status.py
│ │ │ │ └── tasks.py
│ │ │ ├── status.py
│ │ │ ├── tasks.py
│ │ │ └── watcher/
│ │ │ ├── __init__.py
│ │ │ ├── data.py
│ │ │ ├── dirs.py
│ │ │ ├── events.py
│ │ │ ├── move.py
│ │ │ ├── status.py
│ │ │ ├── tasks.py
│ │ │ └── watcher.py
│ │ ├── librariand.py
│ │ ├── memory.py
│ │ ├── mp_queue.py
│ │ ├── notifier/
│ │ │ ├── __init__.py
│ │ │ ├── notifierd.py
│ │ │ └── tasks.py
│ │ ├── restarter/
│ │ │ ├── __init__.py
│ │ │ ├── restarter.py
│ │ │ ├── status.py
│ │ │ └── tasks.py
│ │ ├── scribe/
│ │ │ ├── __init__.py
│ │ │ ├── importer/
│ │ │ │ ├── __init__.py
│ │ │ │ ├── const.py
│ │ │ │ ├── create/
│ │ │ │ │ ├── __init__.py
│ │ │ │ │ ├── comics.py
│ │ │ │ │ ├── const.py
│ │ │ │ │ ├── covers.py
│ │ │ │ │ ├── folders.py
│ │ │ │ │ ├── foreign_keys.py
│ │ │ │ │ └── link_fks.py
│ │ │ │ ├── delete/
│ │ │ │ │ ├── __init__.py
│ │ │ │ │ ├── comics.py
│ │ │ │ │ ├── covers.py
│ │ │ │ │ └── folders.py
│ │ │ │ ├── failed/
│ │ │ │ │ ├── __init__.py
│ │ │ │ │ ├── create.py
│ │ │ │ │ ├── failed.py
│ │ │ │ │ └── query.py
│ │ │ │ ├── finish.py
│ │ │ │ ├── importer.py
│ │ │ │ ├── init.py
│ │ │ │ ├── link/
│ │ │ │ │ ├── __init__.py
│ │ │ │ │ ├── const.py
│ │ │ │ │ ├── covers.py
│ │ │ │ │ ├── delete.py
│ │ │ │ │ ├── many_to_many.py
│ │ │ │ │ ├── prepare.py
│ │ │ │ │ └── sum.py
│ │ │ │ ├── moved/
│ │ │ │ │ ├── __init__.py
│ │ │ │ │ ├── comics.py
│ │ │ │ │ ├── covers.py
│ │ │ │ │ └── folders.py
│ │ │ │ ├── query/
│ │ │ │ │ ├── __init__.py
│ │ │ │ │ ├── covers.py
│ │ │ │ │ ├── filters.py
│ │ │ │ │ ├── foreign_keys.py
│ │ │ │ │ ├── links.py
│ │ │ │ │ ├── links_fk.py
│ │ │ │ │ ├── links_m2m.py
│ │ │ │ │ ├── update_comics.py
│ │ │ │ │ └── update_fks.py
│ │ │ │ ├── read/
│ │ │ │ │ ├── __init__.py
│ │ │ │ │ ├── aggregate_path.py
│ │ │ │ │ ├── const.py
│ │ │ │ │ ├── extract.py
│ │ │ │ │ ├── folders.py
│ │ │ │ │ ├── foreign_keys.py
│ │ │ │ │ └── many_to_many.py
│ │ │ │ ├── search/
│ │ │ │ │ ├── __init__.py
│ │ │ │ │ ├── prepare.py
│ │ │ │ │ ├── sync_m2m.py
│ │ │ │ │ └── update.py
│ │ │ │ ├── statii/
│ │ │ │ │ ├── __init__.py
│ │ │ │ │ ├── create.py
│ │ │ │ │ ├── delete.py
│ │ │ │ │ ├── failed.py
│ │ │ │ │ ├── link.py
│ │ │ │ │ ├── moved.py
│ │ │ │ │ ├── query.py
│ │ │ │ │ ├── read.py
│ │ │ │ │ └── search.py
│ │ │ │ ├── status.py
│ │ │ │ └── tasks.py
│ │ │ ├── janitor/
│ │ │ │ ├── __init__.py
│ │ │ │ ├── adopt_folders.py
│ │ │ │ ├── cleanup.py
│ │ │ │ ├── failed_imports.py
│ │ │ │ ├── integrity/
│ │ │ │ │ ├── __init__.py
│ │ │ │ │ └── foreign_keys.py
│ │ │ │ ├── janitor.py
│ │ │ │ ├── scheduled_time.py
│ │ │ │ ├── status.py
│ │ │ │ ├── tasks.py
│ │ │ │ ├── update.py
│ │ │ │ └── vacuum.py
│ │ │ ├── lazy_importer.py
│ │ │ ├── priority.py
│ │ │ ├── scribed.py
│ │ │ ├── search/
│ │ │ │ ├── __init__.py
│ │ │ │ ├── const.py
│ │ │ │ ├── handler.py
│ │ │ │ ├── optimize.py
│ │ │ │ ├── prepare.py
│ │ │ │ ├── remove.py
│ │ │ │ ├── status.py
│ │ │ │ ├── sync.py
│ │ │ │ └── tasks.py
│ │ │ ├── status.py
│ │ │ ├── tasks.py
│ │ │ └── timestamp_update.py
│ │ ├── status.py
│ │ ├── status_controller.py
│ │ ├── tasks.py
│ │ ├── telemeter/
│ │ │ ├── __init__.py
│ │ │ ├── scheduled_time.py
│ │ │ ├── stats.py
│ │ │ ├── tasks.py
│ │ │ └── telemeter.py
│ │ ├── threads.py
│ │ └── worker.py
│ ├── middleware.py
│ ├── migrations/
│ │ ├── 0001_init.py
│ │ ├── 0002_auto_20200826_0622.py
│ │ ├── 0003_auto_20200831_2033.py
│ │ ├── 0004_failedimport.py
│ │ ├── 0005_auto_20200918_0146.py
│ │ ├── 0006_update_default_names_and_remove_duplicate_comics.py
│ │ ├── 0007_auto_20211210_1710.py
│ │ ├── 0008_alter_comic_created_at_alter_comic_format_and_more.py
│ │ ├── 0009_alter_comic_parent_folder.py
│ │ ├── 0010_haystack.py
│ │ ├── 0011_library_groups_and_metadata_changes.py
│ │ ├── 0012_rename_description_comic_comments.py
│ │ ├── 0013_int_issue_count_longer_charfields.py
│ │ ├── 0014_pdf_issue_suffix_remove_cover_image_sort_name.py
│ │ ├── 0015_link_comics_to_top_level_folders.py
│ │ ├── 0016_remove_comic_cover_path_librarianstatus.py
│ │ ├── 0017_alter_timestamp_options_alter_adminflag_name_and_more.py
│ │ ├── 0018_rename_userbookmark_bookmark.py
│ │ ├── 0019_delete_queuejob.py
│ │ ├── 0020_remove_search_tables.py
│ │ ├── 0021_bookmark_fit_to_choices_read_in_reverse.py
│ │ ├── 0022_bookmark_vertical_useractive_null_statuses.py
│ │ ├── 0023_rename_credit_creator_and_more.py
│ │ ├── 0024_comic_gtin_comic_story_arc_number.py
│ │ ├── 0025_add_story_arc_number.py
│ │ ├── 0026_comicbox_1.py
│ │ ├── 0027_import_order_and_covers.py
│ │ ├── 0028_telemeter.py
│ │ ├── 0029_comicfts.py
│ │ ├── 0030_nocase_collation_day_month_indexes_status_types.py
│ │ ├── 0031_adminflag_banner.py
│ │ ├── 0032_alter_librarianstatus_preactive.py
│ │ ├── 0033_alter_librarianstatus_status_type.py
│ │ ├── 0034_comicbox2.py
│ │ ├── 0035_fts_optmize.py
│ │ ├── 0036_alter_comic_path_alter_customcover_path_and_more.py
│ │ ├── 0037_redefine_reading_direction_filetype_choices.py
│ │ ├── 0038_settings_tables.py
│ │ └── __init__.py
│ ├── models/
│ │ ├── __init__.py
│ │ ├── admin.py
│ │ ├── base.py
│ │ ├── bookmark.py
│ │ ├── choices.py
│ │ ├── comic.py
│ │ ├── fields.py
│ │ ├── functions.py
│ │ ├── groups.py
│ │ ├── identifier.py
│ │ ├── library.py
│ │ ├── named.py
│ │ ├── paths.py
│ │ ├── query.py
│ │ ├── settings.py
│ │ └── util.py
│ ├── run.py
│ ├── serializers/
│ │ ├── README.md
│ │ ├── __init__.py
│ │ ├── admin/
│ │ │ ├── __init__.py
│ │ │ ├── flags.py
│ │ │ ├── groups.py
│ │ │ ├── libraries.py
│ │ │ ├── stats.py
│ │ │ ├── tasks.py
│ │ │ └── users.py
│ │ ├── auth.py
│ │ ├── browser/
│ │ │ ├── __init__.py
│ │ │ ├── choices.py
│ │ │ ├── filters.py
│ │ │ ├── metadata.py
│ │ │ ├── mixins.py
│ │ │ ├── mtime.py
│ │ │ ├── page.py
│ │ │ ├── saved.py
│ │ │ └── settings.py
│ │ ├── fields/
│ │ │ ├── __init__.py
│ │ │ ├── auth.py
│ │ │ ├── base.py
│ │ │ ├── browser.py
│ │ │ ├── group.py
│ │ │ ├── reader.py
│ │ │ ├── sanitized.py
│ │ │ ├── settings.py
│ │ │ ├── stats.py
│ │ │ └── vuetify.py
│ │ ├── homepage.py
│ │ ├── mixins.py
│ │ ├── models/
│ │ │ ├── __init__.py
│ │ │ ├── admin.py
│ │ │ ├── base.py
│ │ │ ├── bookmark.py
│ │ │ ├── comic.py
│ │ │ ├── groups.py
│ │ │ ├── named.py
│ │ │ └── pycountry.py
│ │ ├── opds/
│ │ │ ├── __init__.py
│ │ │ ├── authentication.py
│ │ │ ├── urls.py
│ │ │ ├── v1.py
│ │ │ └── v2/
│ │ │ ├── __init__.py
│ │ │ ├── facet.py
│ │ │ ├── feed.py
│ │ │ ├── links.py
│ │ │ ├── metadata.py
│ │ │ ├── progression.py
│ │ │ ├── publication.py
│ │ │ └── unused.py
│ │ ├── reader.py
│ │ ├── redirect.py
│ │ ├── route.py
│ │ ├── settings.py
│ │ └── versions.py
│ ├── settings/
│ │ ├── README.md
│ │ ├── __init__.py
│ │ ├── codex.toml.default
│ │ ├── config.py
│ │ ├── hypercorn_migrate.py
│ │ ├── logging.py
│ │ ├── secret_key.py
│ │ ├── servestatic.py
│ │ └── timezone.py
│ ├── signals/
│ │ ├── __init__.py
│ │ ├── django_signals.py
│ │ └── os_signals.py
│ ├── startup/
│ │ ├── __init__.py
│ │ ├── custom_cover_libraries.py
│ │ ├── db.py
│ │ ├── loguru.py
│ │ └── registration.py
│ ├── static_src/
│ │ ├── img/
│ │ │ └── .picopt_treestamps.yaml
│ │ ├── pwa/
│ │ │ └── offline.html
│ │ └── robots.txt
│ ├── templates/
│ │ ├── README.md
│ │ ├── headers-icons.html
│ │ ├── headers-script-globals.html
│ │ ├── index.html
│ │ ├── opds_v1/
│ │ │ ├── index.xml
│ │ │ └── opensearch_v1.xml
│ │ └── pwa/
│ │ ├── headers.html
│ │ ├── manifest.webmanifest
│ │ ├── serviceworker-register.js
│ │ └── serviceworker.js
│ ├── urls/
│ │ ├── __init__.py
│ │ ├── api/
│ │ │ ├── __init__.py
│ │ │ ├── admin.py
│ │ │ ├── auth.py
│ │ │ ├── browser.py
│ │ │ ├── reader.py
│ │ │ ├── root.py
│ │ │ └── v3.py
│ │ ├── app.py
│ │ ├── const.py
│ │ ├── converters.py
│ │ ├── opds/
│ │ │ ├── __init__.py
│ │ │ ├── authentication.py
│ │ │ ├── binary.py
│ │ │ ├── root.py
│ │ │ ├── v1.py
│ │ │ └── v2.py
│ │ ├── pwa.py
│ │ ├── root.py
│ │ └── spectacular.py
│ ├── util.py
│ ├── version.py
│ ├── views/
│ │ ├── README.md
│ │ ├── __init__.py
│ │ ├── admin/
│ │ │ ├── __init__.py
│ │ │ ├── api_key.py
│ │ │ ├── auth.py
│ │ │ ├── flag.py
│ │ │ ├── group.py
│ │ │ ├── library.py
│ │ │ ├── permissions.py
│ │ │ ├── stats.py
│ │ │ ├── tasks.py
│ │ │ └── user.py
│ │ ├── auth.py
│ │ ├── bookmark.py
│ │ ├── browser/
│ │ │ ├── __init__.py
│ │ │ ├── annotate/
│ │ │ │ ├── __init__.py
│ │ │ │ ├── bookmark.py
│ │ │ │ ├── card.py
│ │ │ │ └── order.py
│ │ │ ├── bookmark.py
│ │ │ ├── breadcrumbs.py
│ │ │ ├── browser.py
│ │ │ ├── choices.py
│ │ │ ├── const.py
│ │ │ ├── cover.py
│ │ │ ├── download.py
│ │ │ ├── filters/
│ │ │ │ ├── __init__.py
│ │ │ │ ├── bookmark.py
│ │ │ │ ├── field.py
│ │ │ │ ├── filter.py
│ │ │ │ ├── group.py
│ │ │ │ └── search/
│ │ │ │ ├── __init__.py
│ │ │ │ ├── field/
│ │ │ │ │ ├── __init__.py
│ │ │ │ │ ├── column.py
│ │ │ │ │ ├── expression.py
│ │ │ │ │ ├── filter.py
│ │ │ │ │ ├── optimize.py
│ │ │ │ │ └── parse.py
│ │ │ │ ├── fts.py
│ │ │ │ └── parse.py
│ │ │ ├── group_mtime.py
│ │ │ ├── metadata/
│ │ │ │ ├── __init__.py
│ │ │ │ ├── annotate.py
│ │ │ │ ├── const.py
│ │ │ │ ├── copy_intersections.py
│ │ │ │ └── query_intersections.py
│ │ │ ├── mtime.py
│ │ │ ├── order_by.py
│ │ │ ├── page_in_bounds.py
│ │ │ ├── paginate.py
│ │ │ ├── params.py
│ │ │ ├── saved_settings.py
│ │ │ ├── settings.py
│ │ │ ├── title.py
│ │ │ └── validate.py
│ │ ├── const.py
│ │ ├── download.py
│ │ ├── error.py
│ │ ├── exceptions.py
│ │ ├── frontend.py
│ │ ├── healthcheck.py
│ │ ├── lazy_import.py
│ │ ├── mixins.py
│ │ ├── opds/
│ │ │ ├── __init__.py
│ │ │ ├── auth.py
│ │ │ ├── authentication/
│ │ │ │ ├── __init__.py
│ │ │ │ └── v1.py
│ │ │ ├── binary.py
│ │ │ ├── const.py
│ │ │ ├── error.py
│ │ │ ├── feed.py
│ │ │ ├── metadata.py
│ │ │ ├── opensearch/
│ │ │ │ ├── __init__.py
│ │ │ │ └── v1.py
│ │ │ ├── settings.py
│ │ │ ├── start.py
│ │ │ ├── urls.py
│ │ │ ├── user_agent.py
│ │ │ ├── v1/
│ │ │ │ ├── __init__.py
│ │ │ │ ├── const.py
│ │ │ │ ├── entry/
│ │ │ │ │ ├── __init__.py
│ │ │ │ │ ├── entry.py
│ │ │ │ │ └── links.py
│ │ │ │ ├── facets.py
│ │ │ │ ├── feed.py
│ │ │ │ └── links.py
│ │ │ └── v2/
│ │ │ ├── __init__.py
│ │ │ ├── const.py
│ │ │ ├── feed/
│ │ │ │ ├── __init__.py
│ │ │ │ ├── feed_links.py
│ │ │ │ ├── groups.py
│ │ │ │ ├── links.py
│ │ │ │ └── publications.py
│ │ │ ├── href.py
│ │ │ ├── manifest.py
│ │ │ └── progression.py
│ │ ├── public.py
│ │ ├── pwa.py
│ │ ├── reader/
│ │ │ ├── __init__.py
│ │ │ ├── arcs.py
│ │ │ ├── books.py
│ │ │ ├── page.py
│ │ │ ├── params.py
│ │ │ ├── reader.py
│ │ │ └── settings.py
│ │ ├── settings.py
│ │ ├── template.py
│ │ ├── timezone.py
│ │ ├── util.py
│ │ └── version.py
│ └── websockets/
│ ├── README.md
│ ├── __init__.py
│ ├── consumers.py
│ ├── listener.py
│ └── mp_queue.py
├── compose.yaml
├── docs/
│ ├── DOCKER.md
│ ├── WINDOWS.md
│ ├── requirements.txt
│ ├── style.material.css
│ ├── style.mkdocs.css
│ └── style.readthedocs.css
├── eslint.config.js
├── frontend/
│ ├── .gitignore
│ ├── .npmrc
│ ├── .prettierignore
│ ├── .remarkignore
│ ├── .shellcheckrc
│ ├── Makefile
│ ├── README.md
│ ├── bin/
│ │ ├── dev-server.sh
│ │ ├── fix.sh
│ │ ├── kill-eslint_d.sh
│ │ ├── lint-darwin.sh
│ │ ├── lint.sh
│ │ ├── roman.py
│ │ ├── sort-ignore.sh
│ │ ├── update-deps-node.sh
│ │ └── version-node.sh
│ ├── cfg/
│ │ ├── codex-frontend.mk
│ │ ├── common.mk
│ │ ├── help.mk
│ │ └── node.mk
│ ├── jsconfig.json
│ ├── package.json
│ ├── src/
│ │ ├── admin.vue
│ │ ├── api/
│ │ │ └── v3/
│ │ │ ├── admin.js
│ │ │ ├── auth.js
│ │ │ ├── base.js
│ │ │ ├── browser.js
│ │ │ ├── common.js
│ │ │ ├── notify.js
│ │ │ ├── reader.js
│ │ │ └── vuetify-items.js
│ │ ├── app.vue
│ │ ├── browser.vue
│ │ ├── comic-name.js
│ │ ├── components/
│ │ │ ├── admin/
│ │ │ │ ├── admin-header.vue
│ │ │ │ ├── browser-link.vue
│ │ │ │ ├── create-update-dialog/
│ │ │ │ │ ├── create-update-button.vue
│ │ │ │ │ ├── create-update-dialog.vue
│ │ │ │ │ ├── create-update-inputs-mixin.js
│ │ │ │ │ ├── duration-input.vue
│ │ │ │ │ ├── group-create-update-inputs.vue
│ │ │ │ │ ├── library-create-update-inputs.vue
│ │ │ │ │ ├── relation-picker.vue
│ │ │ │ │ ├── server-folder-picker.vue
│ │ │ │ │ └── user-create-update-inputs.vue
│ │ │ │ ├── drawer/
│ │ │ │ │ ├── admin-menu.vue
│ │ │ │ │ ├── admin-settings-button-progress.vue
│ │ │ │ │ ├── admin-settings-drawer.vue
│ │ │ │ │ ├── admin-settings-panel.vue
│ │ │ │ │ ├── status-list-item.vue
│ │ │ │ │ └── status-list.vue
│ │ │ │ ├── group-chip.vue
│ │ │ │ ├── status-helpers.js
│ │ │ │ ├── tabs/
│ │ │ │ │ ├── admin-table.vue
│ │ │ │ │ ├── custom-covers-panel.vue
│ │ │ │ │ ├── datetime-column.vue
│ │ │ │ │ ├── delete-row-dialog.vue
│ │ │ │ │ ├── failed-imports-panel.vue
│ │ │ │ │ ├── flag-descriptions.json
│ │ │ │ │ ├── flag-tab.vue
│ │ │ │ │ ├── group-tab.vue
│ │ │ │ │ ├── job-tab.vue
│ │ │ │ │ ├── library-tab.vue
│ │ │ │ │ ├── library-table.vue
│ │ │ │ │ ├── relation-chips.vue
│ │ │ │ │ ├── stats-tab.vue
│ │ │ │ │ ├── stats-table.vue
│ │ │ │ │ ├── tabs.vue
│ │ │ │ │ └── user-tab.vue
│ │ │ │ └── use-now-timer.js
│ │ │ ├── anchors.scss
│ │ │ ├── auth/
│ │ │ │ ├── auth-form-mixin.js
│ │ │ │ ├── auth-menu.vue
│ │ │ │ ├── auth-token.vue
│ │ │ │ ├── change-password-dialog.vue
│ │ │ │ └── login-dialog.vue
│ │ │ ├── banner.vue
│ │ │ ├── book-cover.scss
│ │ │ ├── book-cover.vue
│ │ │ ├── browser/
│ │ │ │ ├── browser-header.vue
│ │ │ │ ├── card/
│ │ │ │ │ ├── browser-card-menu.vue
│ │ │ │ │ ├── card.vue
│ │ │ │ │ ├── controls.vue
│ │ │ │ │ ├── order-by-caption.vue
│ │ │ │ │ └── subtitle.vue
│ │ │ │ ├── drawer/
│ │ │ │ │ ├── browser-settings-covers.vue
│ │ │ │ │ ├── browser-settings-drawer.vue
│ │ │ │ │ ├── browser-settings-group.vue
│ │ │ │ │ ├── browser-settings-misc.vue
│ │ │ │ │ ├── browser-settings-panel.vue
│ │ │ │ │ └── browser-settings-saved.vue
│ │ │ │ ├── empty.vue
│ │ │ │ ├── filter-warning-snackbar.vue
│ │ │ │ ├── main.vue
│ │ │ │ └── toolbars/
│ │ │ │ ├── breadcrumbs/
│ │ │ │ │ ├── breadcrumbs.vue
│ │ │ │ │ └── browser-toolbar-breadcrumbs.vue
│ │ │ │ ├── browser-toolbar-title.vue
│ │ │ │ ├── nav/
│ │ │ │ │ ├── browser-nav-button.vue
│ │ │ │ │ └── browser-toolbar-nav.vue
│ │ │ │ ├── search/
│ │ │ │ │ ├── browser-toolbar-search.vue
│ │ │ │ │ ├── search-combobox.vue
│ │ │ │ │ ├── search-help-text.vue
│ │ │ │ │ └── search-help.vue
│ │ │ │ ├── select-many/
│ │ │ │ │ └── browser-toolbar-select-many.vue
│ │ │ │ └── top/
│ │ │ │ ├── browser-toolbar-top.vue
│ │ │ │ ├── filter-by-select.vue
│ │ │ │ ├── filter-sub-menu.vue
│ │ │ │ ├── order-by-select.vue
│ │ │ │ ├── order-reverse-button.vue
│ │ │ │ ├── search-button.vue
│ │ │ │ ├── toolbar-button.vue
│ │ │ │ └── top-group-select.vue
│ │ │ ├── cancel-button.vue
│ │ │ ├── clipboard.vue
│ │ │ ├── close-button.vue
│ │ │ ├── codex-list-item.vue
│ │ │ ├── confirm-dialog.vue
│ │ │ ├── confirm-footer.vue
│ │ │ ├── download-button.vue
│ │ │ ├── empty.vue
│ │ │ ├── mark-read-button.vue
│ │ │ ├── metadata/
│ │ │ │ ├── expand-button.vue
│ │ │ │ ├── metadata-activator.vue
│ │ │ │ ├── metadata-body.vue
│ │ │ │ ├── metadata-chip.vue
│ │ │ │ ├── metadata-controls.vue
│ │ │ │ ├── metadata-cover.vue
│ │ │ │ ├── metadata-dialog.vue
│ │ │ │ ├── metadata-header.vue
│ │ │ │ ├── metadata-ratings.vue
│ │ │ │ ├── metadata-tags.vue
│ │ │ │ ├── metadata-text.vue
│ │ │ │ ├── table.scss
│ │ │ │ └── tags-table.vue
│ │ │ ├── pagination-nav-button.vue
│ │ │ ├── pagination-slider.vue
│ │ │ ├── pagination-toolbar.vue
│ │ │ ├── placeholder-loading.vue
│ │ │ ├── reader/
│ │ │ │ ├── book-change-activator.vue
│ │ │ │ ├── book-change-drawer.vue
│ │ │ │ ├── books-window.vue
│ │ │ │ ├── change-column.scss
│ │ │ │ ├── drawer/
│ │ │ │ │ ├── download-panel.vue
│ │ │ │ │ ├── keyboard-shortcuts-panel.vue
│ │ │ │ │ ├── keyboard-shortcuts-table.vue
│ │ │ │ │ ├── reader-settings-controls.vue
│ │ │ │ │ ├── reader-settings-drawer.vue
│ │ │ │ │ ├── reader-settings-panel.vue
│ │ │ │ │ ├── reader-settings-reader.vue
│ │ │ │ │ └── reader-settings-scope.vue
│ │ │ │ ├── empty.vue
│ │ │ │ ├── pager/
│ │ │ │ │ ├── horizontal-pages.vue
│ │ │ │ │ ├── page/
│ │ │ │ │ │ ├── page-error.vue
│ │ │ │ │ │ ├── page-img.vue
│ │ │ │ │ │ ├── page-loading.vue
│ │ │ │ │ │ └── page.vue
│ │ │ │ │ ├── page-change-link.vue
│ │ │ │ │ ├── pager-full-pdf.vue
│ │ │ │ │ ├── pager-horizontal.vue
│ │ │ │ │ ├── pager-vertical.vue
│ │ │ │ │ ├── pager.vue
│ │ │ │ │ ├── pdf-doc.vue
│ │ │ │ │ └── scale-for-scroll.vue
│ │ │ │ └── toolbars/
│ │ │ │ ├── nav/
│ │ │ │ │ ├── reader-book-change-nav-button.vue
│ │ │ │ │ ├── reader-nav-button.vue
│ │ │ │ │ └── reader-toolbar-nav.vue
│ │ │ │ └── top/
│ │ │ │ ├── reader-arc-select.vue
│ │ │ │ └── reader-toolbar-top.vue
│ │ │ ├── scale-button.vue
│ │ │ ├── settings/
│ │ │ │ ├── button.vue
│ │ │ │ ├── docs-footer.vue
│ │ │ │ ├── opds-dialog.vue
│ │ │ │ ├── opds-url.vue
│ │ │ │ ├── settings-drawer.vue
│ │ │ │ └── version-footer.vue
│ │ │ ├── submit-footer.vue
│ │ │ ├── toolbar-select.vue
│ │ │ └── unauthorized.vue
│ │ ├── datetime.js
│ │ ├── http-error.vue
│ │ ├── main.js
│ │ ├── platform.js
│ │ ├── plugins/
│ │ │ ├── drag-scroll.js
│ │ │ ├── router.js
│ │ │ └── vuetify.js
│ │ ├── reader.vue
│ │ ├── route.js
│ │ ├── stores/
│ │ │ ├── admin.js
│ │ │ ├── auth.js
│ │ │ ├── browser-select-many.js
│ │ │ ├── browser.js
│ │ │ ├── common.js
│ │ │ ├── metadata.js
│ │ │ ├── reader.js
│ │ │ ├── socket.js
│ │ │ └── store.js
│ │ └── util.js
│ ├── tests/
│ │ └── unit/
│ │ └── reader-nav-button.test.js
│ └── vite.config.js
├── mkdocs.yml
├── mock_comics/
│ ├── __init__.py
│ ├── bigbook.py
│ ├── mock_comics.py
│ └── mock_comics.sh
├── nginx/
│ └── default.conf
├── package.json
├── pyproject.toml
├── tests/
│ ├── README.md
│ ├── __init__.py
│ ├── files/
│ │ ├── comicbox-2-example.cbz
│ │ ├── comicbox-2-update.cbz
│ │ ├── comicbox.example.yaml
│ │ └── comicbox.update.yaml
│ ├── importer/
│ │ ├── __init__.py
│ │ ├── test_basic.py
│ │ ├── test_update_all.py
│ │ └── test_update_none.py
│ ├── nginx-local-codex.conf
│ ├── test_asgi.py
│ └── test_models.py
└── vulture_ignorelist.py
================================================
FILE CONTENTS
================================================
================================================
FILE: .circleci/config.yml
================================================
executors:
amd64-medium-executor:
machine:
image: ubuntu-2404:current
resource_class: medium
arm64-medium-executor:
machine:
image: ubuntu-2404:current
resource_class: arm.medium
orbs:
discord: antonioned/discord@0.1.0
advanced-checkout: vsco/advanced-checkout@1.1.0
jobs:
build-base-amd64: &build-base
executor: amd64-medium-executor
steps:
- advanced-checkout/shallow-checkout
- run:
command: . ./ci/machine-init.sh base
name: Update packages & docker, produce env file.
- run:
command: ./ci/docker-build-image.sh codex-base clean
name: Build Base Image
- run:
command: ./ci/docker-build-image.sh codex-builder-base clean
name: Build Builder Base Image
- persist_to_workspace:
paths:
- ./.env-*
- ./.venv
root: .
- discord/status:
fail_only: true
webhook: "${DISCORD_STATUS_WEBHOOK}"
failure_message: "**${CIRCLE_USERNAME}**'s build: **${CIRCLE_JOB}** failed."
build-base-arm64:
<<: *build-base
executor: arm64-medium-executor
test-and-build-dist-amd64:
executor: amd64-medium-executor
steps:
- advanced-checkout/shallow-checkout
- attach_workspace:
at: .
- run:
command: ./ci/machine-init.sh dist-builder
name: Update packages & docker, produce env file.
- run:
command: ./ci/docker-build-image.sh codex-dist-builder pull
name: Build Builder Base Image
- run:
command: ./ci/docker-compose-exit.sh codex-lint
name: Lint
- run:
command: ./ci/docker-compose-exit.sh codex-frontend-test
name: "Frontend: Test"
- store_test_results:
path: test-results/jest
- store_artifacts:
path: frontend/coverage
- run:
command: ./ci/docker-compose-exit.sh codex-frontend-build
name: "Frontend: Build"
- run:
command: ./ci/docker-compose-exit.sh codex-backend-test
name: "Backend: Test"
- store_test_results:
path: test-results/pytest
- store_artifacts:
path: test-results/coverage
- run:
command: ./ci/docker-compose-exit.sh codex-build-dist
name: Build Distribution
- run:
command: sudo chown -R circleci:circleci dist
name: chown dist
- persist_to_workspace:
paths:
- ./dist
root: .
- discord/status:
fail_only: false
webhook: "${DISCORD_STATUS_WEBHOOK}"
failure_message: "**${CIRCLE_USERNAME}**'s build: **${CIRCLE_JOB}** failed."
build-amd64: &build
executor: amd64-medium-executor
steps:
- advanced-checkout/shallow-checkout
- attach_workspace:
at: .
- run:
command: ./ci/machine-init.sh
name: Update packages & docker, produce env file.
- run:
command: ./ci/docker-build-image.sh codex-arch
name: Build Codex Runnable Image
- discord/status:
fail_only: true
webhook: "${DISCORD_STATUS_WEBHOOK}"
failure_message: "**${CIRCLE_USERNAME}**'s build: **${CIRCLE_JOB}** failed."
success_message: "**${CIRCLE_USERNAME}**'s build: **${CIRCLE_JOB}** built."
build-arm64:
<<: *build
executor: arm64-medium-executor
deploy:
executor: amd64-medium-executor
steps:
- advanced-checkout/shallow-checkout
- attach_workspace:
at: .
- run:
command: ./ci/machine-init.sh
name: Update packages & docker, produce env file.
- run:
command: ./ci/docker-push.sh
name: Push multi-arch images to Docker Hub
- run:
command: echo Disabled PyPI push # ./ci/python-publish.sh
name: Publish Codex Package to PyPI
- discord/status:
fail_only: false
webhook: "${DISCORD_STATUS_WEBHOOK}"
failure_message: "**${CIRCLE_USERNAME}**'s build: **${CIRCLE_JOB}** failed."
success_message: "**${CIRCLE_USERNAME}**'s build: **${CIRCLE_JOB}** deployed."
version: 2.1
workflows:
main:
jobs:
- build-base-amd64: &filters-all
filters:
branches:
only:
- main
- build-base-arm64: &filters-release
filters:
branches:
only:
- main
- test-and-build-dist-amd64:
<<: *filters-all
requires:
- build-base-amd64
- build-amd64:
<<: *filters-release
requires:
- build-base-amd64
- test-and-build-dist-amd64
- build-arm64:
<<: *filters-release
requires:
- build-base-arm64
- test-and-build-dist-amd64
- deploy:
<<: *filters-release
requires:
- build-amd64
- build-arm64
================================================
FILE: .dockerignore
================================================
__pycache__
!dist
!docker/debian.sources
.*cache
.circleci
.claude
.coverage*
.docker-token
.DS_Store
.env*
.eslintcache
.ghrc-token
.git
.mypy_cache
.picopt_timestamp
.picopt_treestamps.yaml
.pypi-token
.pytest_cache
.ropeproject
.ruff_cache
.venv*
*.egg-info
*.py[co]
*~
*Dockerfile
cache
codex/static
codex/static_build
comics
config
dev*
docker-compose*
docker*
MANIFEST
mock_comics
monkeytype.sqlite3
NEWS
node_modules
test-results
TODO.md
update-builder-requirement.sh
version.sh
================================================
FILE: .github/workflows/ci.yml
================================================
name: CI
on:
push:
branches: [main]
pull_request:
types: [opened, synchronize]
branches: [main, develop]
env:
IMAGE: ghcr.io/${{ github.repository }}
BUILDER_IMAGE: ghcr.io/${{ github.repository }}/cache:ci
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
# ---------------------------------------------------------------------------
# Lint, test, and build the Python wheel (amd64 only)
# ---------------------------------------------------------------------------
test:
name: Lint, Test & Build Dist
runs-on: ubuntu-24.04
if: |
github.ref_name == 'main' && github.event_name == 'push' ||
github.base_ref == 'main' ||
(github.base_ref == 'develop' && github.head_ref == 'pre-release')
permissions:
contents: read
packages: write
checks: write
steps:
- name: Checkout
uses: actions/checkout@v6
with:
# Only fetch full history for the main push gate
fetch-depth: ${{ (github.ref_name == 'main' && github.event_name == 'push') && 0 || 1 }}
- name: Check for Redundant Tests & Existing Dist
id: gate
if: github.ref_name == 'main' && github.event_name == 'push'
env:
GH_REPO: ${{ github.repository }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: bin/ci-download-dist-if-identical.sh
- name: Set up Docker Buildx
if: steps.gate.outputs.dist_found != 'true'
uses: docker/setup-buildx-action@v4
- name: Log in to GHCR
if: steps.gate.outputs.dist_found != 'true'
uses: docker/login-action@v4
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build dist-builder image
id: build-ci
if: steps.gate.outputs.dist_found != 'true'
uses: docker/build-push-action@v7
with:
context: .
target: codex-ci
load: true
tags: codex-ci:ci
cache-from: type=registry,ref=${{ env.BUILDER_IMAGE }}
cache-to: type=registry,ref=${{ env.BUILDER_IMAGE }},mode=max
- name: Start Services
id: start-services
if: steps.build-ci.outcome == 'success'
run: |
mkdir -p dist test-results
docker compose up -d ci
- name: Lint
id: lint
if: steps.start-services.outcome == 'success'
run: docker exec codex-ci make lint
- name: Build for Tests & Dist
id: build-for-tests
if: steps.lint.outcome == 'success'
run: docker exec codex-ci make build-choices build-frontend collectstatic
- name: Tests
id: tests
if: steps.build-for-tests.outcome == 'success'
run: docker exec codex-ci make test-frontend django-check test-python -o build-choices
- name: Upload Test Results
id: upload-tests
if: "!cancelled() && steps.tests.outcome != 'skipped'"
uses: actions/upload-artifact@v7
with:
name: test-results
path: "**/test-results/pytest/*.xml"
- name: Publish Test Report
if: "!cancelled() && steps.upload-tests.outcome == 'success'"
uses: mikepenz/action-junit-report@v6
with:
report_paths: "**/test-results/pytest/*.xml"
- name: Build Distribution
id: build-dist
if: steps.tests.outcome == 'success'
run: docker exec codex-ci make build-only
- name: Stop Services
if: steps.start-services.outcome == 'success'
run: |
touch .env.package # hack because down is too promiscuous
docker compose down ci
- name: Upload dist artifact
if: "!cancelled() && steps.gate.outputs.dist_found == 'true' || steps.build-dist.outcome =='success'"
uses: actions/upload-artifact@v7
with:
name: python-dist
path: dist/
retention-days: 2
- name: Discord notification (Test Results)
if: "!cancelled()"
uses: sarisia/actions-status-discord@v1
with:
webhook: ${{ secrets.DISCORD_WEBHOOK }}
status: ${{ job.status }}
title: Lint & Test
description: "Tests for ${{ github.ref_name }} #${{ github.run_number }} ${{ job.status == 'success' && 'passed' || 'FAILED' }}"
color: ${{ job.status == 'success' && '0x28a745' || '0xd73a49' }}
url: "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
# ---------------------------------------------------------------------------
# Build per-arch Docker images natively
# ---------------------------------------------------------------------------
build:
name: Build Image (${{ matrix.arch }})
needs: test
if: github.event_name == 'push' ||
(github.base_ref == 'develop' && github.head_ref == 'pre-release')
strategy:
matrix:
include:
- arch: amd64
runner: ubuntu-24.04
- arch: arm64
runner: ubuntu-24.04-arm
runs-on: ${{ matrix.runner }}
permissions:
contents: read
packages: write
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4
- name: Download dist artifact
uses: actions/download-artifact@v8
with:
name: python-dist
path: dist/
- name: Set Build Context
run: |
VERSION=$(grep -Po '(?<=^version = ")[^"]+' pyproject.toml)
echo "CODEX_VERSION=${VERSION}" >> "$GITHUB_ENV"
echo "CODEX_WHEEL=$(find dist -name '*.whl' -print0 | xargs basename)" >> "$GITHUB_ENV"
- name: Log in to GHCR
uses: docker/login-action@v4
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v7
with:
context: .
platforms: linux/${{ matrix.arch }}
push: true
build-args: |
CODEX_VERSION=${{ env.CODEX_VERSION }}
CODEX_WHEEL=${{ env.CODEX_WHEEL }}
tags: ${{ env.IMAGE }}:${{ env.CODEX_VERSION }}-${{ matrix.arch }}
cache-from: type=gha,scope=build-${{ matrix.arch }}
cache-to: type=gha,scope=build-${{ matrix.arch }},mode=max
# ---------------------------------------------------------------------------
# Create multi-arch manifest and publish
# ---------------------------------------------------------------------------
deploy:
name: Deploy
needs: build
runs-on: ubuntu-24.04
permissions:
contents: read
packages: write
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Setup Docker Buildx
uses: docker/setup-buildx-action@v4
- name: Setup uv
uses: astral-sh/setup-uv@v8.0.0
- name: Download dist artifact
uses: actions/download-artifact@v8
with:
name: python-dist
path: dist/
- name: Log in to GHCR
uses: docker/login-action@v4
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Create and push manifest
run: |
VERSION=$(uv version --short)
TAG_ARGS=("-t" "${IMAGE}:${VERSION}")
# Positive regex check for Final versions (e.g., 1.2.3)
if [[ $VERSION =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
TAG_ARGS+=("-t" "${IMAGE}:latest")
fi
docker buildx imagetools create \
--annotation "index:org.opencontainers.image.title=Codex" \
--annotation "index:org.opencontainers.image.description=Codex Comic Server" \
--annotation "index:org.opencontainers.image.version=${VERSION}" \
--annotation "index:org.opencontainers.image.authors=AJ Slater <aj@slater.net>" \
--annotation "index:org.opencontainers.image.url=https://codex-reader.app" \
--annotation "index:org.opencontainers.image.source=https://github.com/ajslater/codex" \
--annotation "index:org.opencontainers.image.licenses=GPL-3.0-only" \
"${TAG_ARGS[@]}" \
"${IMAGE}:${VERSION}-amd64" \
"${IMAGE}:${VERSION}-arm64"
- name: Publish to PyPI
run: uv publish dist/*
env:
UV_PUBLISH_TOKEN: ${{ secrets.PYPI_TOKEN }}
- name: Discord notification (Deploy Results)
if: "!cancelled()"
uses: sarisia/actions-status-discord@v1
with:
webhook: ${{ secrets.DISCORD_WEBHOOK }}
status: ${{ job.status }}
title: Deploy
description: "Deploy ${{ github.ref_name }} #${{ github.run_number }} ${{ job.status == 'success' && 'succeeded' || 'FAILED' }}"
color: ${{ job.status == 'success' && '0x28a745' || '0xd73a49' }}
url: "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
================================================
FILE: .gitignore
================================================
__pycache__/
__pypackages__/
__snapshots__
.*cache
.claude
.coverage
.coverage.*
.coverage*
.dmypy.json
.docker-token
.DS_Store
.eggs/
.env
.env-*
.env.package
.env.pushover
.eslintcache
.ghrc-token
.hypothesis/
.installed.cfg
.ipynb_checkpoints
.mypy_cache/
.nox/
.npm
.pypi-token
.pypirc
.pyre/
.pytest_cache/
.Python
.python-version
.ropeproject
.ruff_cache/
.scrapy
.spyderproject
.spyproject
.tox/
.uv-publish-env
.venv*
.webassets-cache
*.cover
*.egg
*.egg-info/
*.log
*.manifest
*.mo
*.pot
*.py,cover
*.py[cod]
*.sage.py
*.so
*.spec
*~
*$py.class
build
build/
celerybeat-schedule
celerybeat.pid
codex/static_build/
codex/static/
comics
config
coverage.xml
develop-eggs/
dist
dist/
dmypy.json
docs/_build/
docs/site
downloads/
eggs/
env.bak/
env/
ENV/
frontend/components.d.ts
frontend/coverage
frontend/src/choices/
htmlcov/
instance/
ipython_config.py
jspm_packages/
lib/
lib64/
local_settings.py
MANIFEST
monkeytype.sqlite3
node_modules
node_modules/
nosetests.xml
parts/
pip-delete-this-directory.txt
pip-log.txt
pip-wheel-metadata/
profile_default/
sdist/
share/python-wheels/
target/
test-results
TODO.md
var/
venv.bak/
venv/
wheels/
================================================
FILE: .picopt_treestamps.yaml
================================================
config:
bigger: false
convert_to: []
formats:
- GIF
- JPEG
- PNG
- WEBP
ignore: []
keep_metadata: true
recurse: true
symlinks: true
.: 1657150080.593595
================================================
FILE: .prettierignore
================================================
__pycache__
.*cache
.*cache/
.circleci
.claude
.git
.mypy_cache
.pytest_cache
.ruff_cache
.venv
.venv*
*Dockerfile
cache
codex/_vendor
codex/static
codex/static_build
codex/static_src/img/*.svg
codex/templates/**/*.html
codex/templates/**/*.xml
codex/templates/pwa/manifest.webmanifest
comics
config
dist
frontend
node_modules
package-lock.json
test-results
tests/**/*.json
tests/**/*.xml
tests/**/*.yaml
tests/**/*.yml
uv.lock
================================================
FILE: .readthedocs.yaml
================================================
build:
os: ubuntu-24.04
tools:
python: "3"
mkdocs:
configuration: mkdocs.yml
python:
install:
- requirements: docs/requirements.txt
version: 2
================================================
FILE: .shellcheckrc
================================================
external-sources=true
================================================
FILE: CLAUDE.md
================================================
# CLAUDE.md Codex
## Project Overview
Codex is a comic archive web server: Django 6 backend, Vue 3 frontend, SQLite
database, WebSocket status updates. Users browse and read comics (CBZ, CBR, PDF)
through a responsive web UI. A background librarian daemon watches the
filesystem for changes and manages metadata import, cover generation, and search
indexing.
## Commands
Commands are from @\~/.claude/python-devenv.md
### Local Commands
Non exhaustive list of commands specific to this repository
```bash
make install # Install all dependencies (Python + Node)
make build-frontend # Vite production build
make collectstatic # Django collectstatic
make build-only # Build Python wheel (no frontend)
make build-choices # Generate choices JSON from Django enums
```
## Architecture
### Backend (`/codex/`)
Django app served by Granian (ASGI). Key subsystems:
- **`models/`** — ORM models: Comic, Series, Publisher, Imprint, Library,
Bookmark, Identifier. SQLite with WAL mode.
- **`views/`** — DRF ViewSets organized by feature: `browser/` (comic listings),
`reader/` (page serving), `admin/` (CRUD), `opds/` (syndication).
- **`urls/`** — API at `/api/v3/`. Sub-routers: `/auth/`, `/c/` (reader),
`/<group>/` (browser), `/admin/`.
- **`serializers/`** — DRF serializers for browser, reader, and admin responses.
- **`librarian/`** — Multiprocessing background daemon with dedicated threads:
- `CoverThread` — Generate/cache comic covers
- `ScribeThread` — Metadata import, FTS5 search index sync
- `LibraryPollerThread` / `LibraryWatcherThread` — Filesystem monitoring
(polling + inotify)
- `CronThread` — Scheduled tasks (auto-import, cleanup)
- `BookmarkThread` — Persist user reading positions
- `NotifierThread` — Broadcast status via WebSockets
- **`websockets/`** — Django Channels consumers. Groups: `ALL` (everyone),
`ADMIN` (staff). Broadcasts librarian task progress.
- **`settings/`** — Config loaded from `/config/codex.toml` (TOML). Env vars:
`DEBUG`, `CODEX_CONFIG_DIR`, `TIMEZONE`.
- **`applications/`** — ASGI app layers (HTTP + WebSocket routing, lifespan).
- **`run.py`** — Granian server entry point.
### Frontend (`/frontend/`)
Vue 3 + Vite + Vuetify 4 SPA.
- **`src/stores/`** — Pinia stores: `browser`, `reader`, `auth`, `metadata`,
`socket`, `admin`.
- **`src/api/v3/`** — HTTP client (xior) with automatic CSRF token injection.
- **`src/components/`** — Organized by view: `browser/`, `reader/`, `admin/`,
`metadata/`, `settings/`.
- **`src/plugins/`** — Vue Router, Vuetify, drag-scroll.
- **Routes:** `/` (home), `/:group/:pks/:page` (browser), `/c/:pk/:page`
(reader), `/admin` (dashboard).
- **Build output:** Vite builds to `/codex/static_build/`, then `collectstatic`
copies to `/codex/static/`.
### Docker (`/Dockerfile`)
Multi-stage build with targets:
1. **`runtime-base`** — Slim Debian with runtime libs only
2. **`builder`** — Python 3.14 + Node 24 + build tools
3. **`codex-ci`** — Builder + full source + dev deps (used in CI for
lint/test/build)
4. **`wheel-installer`** — Installs compiled wheel, strips binaries
5. **`final`** (default) — Minimal production image. Exposes port 9810, volumes
`/comics` and `/config`.
### CI (`.github/workflows/ci.yml`)
Single workflow with three jobs: `test` -> `build` -> `deploy`. The `test` job
builds the `codex-ci` Docker target, runs lint/test/build inside it via
`docker exec`. The `build` job creates per-arch production images (amd64 +
arm64). The `deploy` job creates a multi-arch manifest and publishes to GHCR +
PyPI.
### Makefile Structure
`/Makefile` includes fragments from `/cfg/*.mk`. These are managed by the
sibling `cfg` boilerplate system. Key fragments: `codex.mk`, `django.mk`,
`frontend.mk`, `python.mk`, `docker.mk`, `ci.mk`.
### Key Libraries
- **Backend:** Django 6, Channels 4.2, DRF 3.16, Granian 2.7, comicbox (comic
parsing), Pillow, django-cachalot, loguru
- **Frontend:** Vue 3.5, Vite 8, Vuetify 4, Pinia 3, xior
- **Database:** SQLite + WAL + FTS5 full-text search
## Project-Specific Conventions
- Released as both a Python wheel (PyPI) and Docker image (GHCR).
- Config file is TOML (`codex.toml`), not env vars.
- Browser API groups comics by: publisher, series, folder, arc, volume — the
`group` URL param selects which.
- Choices/enums are shared between frontend and backend via generated JSON
(`make build-choices`).
- The `compose.yaml` `ci` service mirrors the CI Docker build for local testing.
## Linting & Testing
Uses @\~/.claude/python-devenv.md
- **Python:** pytest with Django test runner. Results in `test-results/pytest/`.
- **Frontend:** vitest. Results in `test-results/`.
- **Lint:** Ruff (Python), ESLint + Prettier (JS/Vue), shellcheck, hadolint
(Dockerfile).
================================================
FILE: Dockerfile
================================================
###############################################################################
# Multi-stage Dockerfile for Codex CI and production
#
# Targets:
# codex-ci – CI image with all deps + source (lint, test, build wheel)
# final – Slim production image (default)
#
# Usage:
# CI: docker build --target codex-ci -t codex-ci:ci .
# docker run codex-ci:ci make lint
# Prod: docker build --build-arg CODEX_WHEEL=codex-X.Y.Z-py3-none-any.whl \
# --build-arg CODEX_VERSION=X.Y.Z .
###############################################################################
# ---- Stage 1: runtime-base (slim, no build tools) --------------------------
FROM ghcr.io/ajslater/python-debian:3.14.4-slim-trixie_0 AS runtime-base
COPY ci/debian.sources /etc/apt/sources.list.d/
# hadolint ignore=DL3008
RUN apt-get clean \
&& apt-get update \
&& apt-get install --no-install-recommends -y \
curl \
libimagequant0 \
libjpeg62-turbo \
libopenjp2-7 \
libssl3 \
libyaml-0-2 \
libtiff6 \
libwebp7 \
ruamel.yaml.clib \
unrar \
zlib1g \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*
# ---- Stage 2: builder (build tools + Node for compilation) -----------------
FROM nikolaik/python-nodejs:python3.14-nodejs24 AS builder
# nodejs25 blocked on bug https://github.com/nodejs/node/issues/60303
COPY ci/debian.sources /etc/apt/sources.list.d/
# hadolint ignore=DL3008
RUN apt-get clean \
&& apt-get update \
&& apt-get install --no-install-recommends -y \
bash \
build-essential \
cmake \
git \
libimagequant0 \
libjpeg62-turbo \
libopenjp2-7 \
libssl3 \
libyaml-0-2 \
libtiff6 \
libwebp7 \
python3-dev \
ruamel.yaml.clib \
unrar \
zlib1g \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /app
# hadolint ignore=DL3013,DL3042
RUN pip3 install --no-cache --upgrade pip
# ---- Stage 3: codex-ci (all deps + source for CI) -------------------------
FROM oven/bun:latest AS bun-source
FROM builder AS codex-ci
# hadolint ignore=DL3008
RUN apt-get clean \
&& apt-get update \
&& apt-get install --no-install-recommends -y \
shellcheck \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*
COPY --from=bun-source /usr/local/bin/bun /usr/local/bin/bun
COPY --from=bun-source /usr/local/bin/bunx /usr/local/bin/bunx
WORKDIR /app
# Python deps (cacheable when lockfiles unchanged)
COPY pyproject.toml uv.lock ./
# hadolint ignore=DL3042
RUN PIP_CACHE_DIR=$(pip3 cache dir) PYMUPDF_SETUP_PY_LIMITED_API=0 \
uv sync --no-install-project --no-dev --group lint --group test
# Root Node deps (eslint, prettier, etc.)
COPY package.json bun.lock ./
RUN bun install
# Frontend Node deps
WORKDIR /app/frontend
COPY frontend/package.json frontend/bun.lock ./
RUN bun install
# Full source
WORKDIR /app
COPY . .
VOLUME /app/codex/static_build
VOLUME /app/codex/static
VOLUME /app/dist
VOLUME /app/test-results
VOLUME /app/frontend/src/choices
# ---- Stage 4: wheel-installer (compile native extensions) ------------------
FROM builder AS wheel-installer
ARG CODEX_WHEEL
COPY dist/${CODEX_WHEEL} /tmp/${CODEX_WHEEL}
# hadolint ignore=DL3059,DL3013
RUN PYMUPDF_SETUP_PY_LIMITED_API=0 pip3 install --no-cache-dir /tmp/${CODEX_WHEEL}
# Slim down /usr/local before it gets copied to the final image
# hadolint ignore=DL3059
RUN set -eux \
# Remove pip, setuptools, wheel — not needed at runtime
&& pip3 uninstall -y pip setuptools wheel 2>/dev/null || true \
&& rm -rf /usr/local/bin/pip* \
# Strip debug symbols from shared libraries (~30-50% size reduction on .so files)
&& find /usr/local -name '*.so' -exec strip --strip-unneeded {} + 2>/dev/null || true \
&& find /usr/local -name '*.so.*' -exec strip --strip-unneeded {} + 2>/dev/null || true \
# Remove Python bytecode caches (regenerated on first import)
&& find /usr/local -type d -name '__pycache__' -exec rm -rf {} + 2>/dev/null || true \
&& find /usr/local -name '*.pyc' -delete 2>/dev/null || true \
# Remove the stdlib test suite (~30MB) — safe, never needed at runtime
&& rm -rf /usr/local/lib/python*/test \
&& rm -rf /usr/local/lib/python*/idlelib \
&& rm -rf /usr/local/lib/python*/ensurepip \
# Remove type stubs — only used by type checkers
&& find /usr/local -name '*.pyi' -delete 2>/dev/null || true \
# Remove the installed wheel
&& rm -f /tmp/${CODEX_WHEEL}
# ---- Stage 5: final (production image) ------------------------------------
FROM runtime-base AS final
ARG CODEX_VERSION
LABEL org.opencontainers.image.title="Codex" \
org.opencontainers.image.description="Codex Comic Server" \
org.opencontainers.image.version="${CODEX_VERSION}" \
org.opencontainers.image.authors="AJ Slater <aj@slater.net>" \
org.opencontainers.image.url="https://codex-reader.app" \
org.opencontainers.image.source="https://github.com/ajslater/codex" \
org.opencontainers.image.licenses="GPL-3.0-only"
RUN mkdir -p /comics && touch /comics/DOCKER_UNMOUNTED_VOLUME
RUN mkdir -p /home/abc/.config/comicbox \
&& chown -R abc /home/abc/.config \
&& chmod 777 /home/abc/.config /home/abc/.config/comicbox
COPY --from=wheel-installer /usr/local /usr/local
VOLUME /comics
VOLUME /config
EXPOSE 9810
CMD ["/usr/local/bin/codex"]
================================================
FILE: LICENSE
================================================
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU General Public License is a free, copyleft license for
software and other kinds of works.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
the GNU General Public License is intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users. We, the Free Software Foundation, use the
GNU General Public License for most of our software; it applies also to
any other work released this way by its authors. You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
To protect your rights, we need to prevent others from denying you
these rights or asking you to surrender the rights. Therefore, you have
certain responsibilities if you distribute copies of the software, or if
you modify it: responsibilities to respect the freedom of others.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must pass on to the recipients the same
freedoms that you received. You must make sure that they, too, receive
or can get the source code. And you must show them these terms so they
know their rights.
Developers that use the GNU GPL protect your rights with two steps:
(1) assert copyright on the software, and (2) offer you this License
giving you legal permission to copy, distribute and/or modify it.
For the developers' and authors' protection, the GPL clearly explains
that there is no warranty for this free software. For both users' and
authors' sake, the GPL requires that modified versions be marked as
changed, so that their problems will not be attributed erroneously to
authors of previous versions.
Some devices are designed to deny users access to install or run
modified versions of the software inside them, although the manufacturer
can do so. This is fundamentally incompatible with the aim of
protecting users' freedom to change the software. The systematic
pattern of such abuse occurs in the area of products for individuals to
use, which is precisely where it is most unacceptable. Therefore, we
have designed this version of the GPL to prohibit the practice for those
products. If such problems arise substantially in other domains, we
stand ready to extend this provision to those domains in future versions
of the GPL, as needed to protect the freedom of users.
Finally, every program is threatened constantly by software patents.
States should not allow patents to restrict development and use of
software on general-purpose computers, but in those that do, we wish to
avoid the special danger that patents applied to a free program could
make it effectively proprietary. To prevent this, the GPL assures that
patents cannot be used to render the program non-free.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Use with the GNU Affero General Public License.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU Affero General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the special requirements of the GNU Affero General Public License,
section 13, concerning interaction through a network will apply to the
combination as such.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If the program does terminal interaction, make it output a short
notice like this when it starts in an interactive mode:
<program> Copyright (C) <year> <name of author>
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, your program's commands
might be different; for a GUI interface, you would use an "about box".
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU GPL, see
<https://www.gnu.org/licenses/>.
The GNU General Public License does not permit incorporating your program
into proprietary programs. If your program is a subroutine library, you
may consider it more useful to permit linking proprietary applications with
the library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License. But first, please read
<https://www.gnu.org/licenses/why-not-lgpl.html>.
================================================
FILE: Makefile
================================================
SHELL := /usr/bin/env bash
include cfg/codex.mk
include cfg/django.mk
include cfg/frontend.mk
include cfg/python.mk
include cfg/docker.mk
include cfg/ci.mk
include cfg/docs.mk
include cfg/node.mk
include cfg/node_root.mk
include cfg/common.mk
include cfg/help.mk
.PHONY: all
================================================
FILE: NEWS.md
================================================
# 📜 Codex News
<img src="codex/img/logo.svg" style="
height: 128px;
width: 128px;
border-radius: 128px;
" />
## v1.10.12
- Fixes
- Fix an occasional bug linking folders when importing.
- Fix updating comics crash by batching comic updates.
- Fix possible import crashes in delete & linking.
- Fix overzealous lazy importer importing tags from books that already had
tags.
- Fix reader settings clearing anomalies. Add a global reader settings clear
button.
- Fix unnecessary redirect in OPDS when viewing comic in extended metadata.
- Features
- Codex identifies itself in the HTTP Server: header.
- Codex identifies itself in the OPDS v1 <generator> tag.
- Adjusted importer batching variable defaults for greater throughput
## v1.10.11
- Fixes
- Custom covers were not importing.
- Custom group covers were not counted in admin library view.
## v1.10.10
- Fixes
- Fix creating non-extant reader global settings.
## v1.10.9
- Fixes
- Fix browser settings not clearing with clear button.
- Fix search settings not clearing on OPDS start page.
## v1.10.8
- Fixes
- Reset settings on OPDS start page more consistently.
- More logging for webserver errors.
- There's a jump in version numbers because the production build process was
broken.
## v1.10.4-7 - Broken releases
## v1.10.3
- Fixes
- Poll all libraries button was not polling all libraries.
- Fix OPDS v2 manifest series link.
- Force OPDS v2 start link to reset filters and order
- Library server folder picker shows folder menu more consistently.
- Features
- Select many mode in browser for browser actions.
## v1.10.2
- Fixes
- Fix many OPDS v2 links leading to the wrong views.
- Fix admin update user validation.
## v1.10.1 - Mistaken release
## v1.10.0 Rust modules & Settings Features
- 🚨 Big Changes 🚨
- The docker repo has changed to ghcr.io/ajslater/codex
- Django 6 requires python >=3.12.
- Unified configuration:
- Your hypercorn.toml config will be migrated automatically to
codex.toml. Options previously only configurable with environment
variables may now be also be specified in codex.toml.
- Some of the environment variable names have changed, but old values
are still respected for compatibility.
- Replaced the thumbnail filename algorithm. Thumbnails will regenerate.
- Fixes
- Fix old dynamic covers displaying when database had changed due to
imports.
- Fix erroneous "Bookmark" filter appearing in filter menu when a read state
is selected.
- Fix reader close button receiving nonsensical route.
- Fix job status messages sometimes not being updated correctly.
- Fix check latest version of Codex bug.
- Tags page sometimes had erroneous links went nowhere.
- Fixed some foreign key integrity fixing bugs.
- Features
- Browser can save and load named views.
- Reader Settings are now available for Series, Parent Folder and Story Arc
- The Admin Jobs tab is an enhanced tasks tab with start and stop buttons
and job progress.
- Browser Pane refreshes with a click/touch pull control.
- Performance Improvements
- Codex now uses the Granian http server instead of Hypercorn.
- The file watcher and poller now use one thread each no matter how many
libraries you have.
- Shaved about 80MB off the compressed docker image and 15MB off the python
wheel.
## v1.9.24
- Fixes
- Fix OPDS v2 numberOfItems pagination sum.
## v1.9.23
- Fixes
- Fix admin tasks tab crash
- Fix Library folder picker showing bad data
## v1.9.22
- Fixes
- Fix many batch jobs not running. Including polling and importing.
## v1.9.21
- Fixes
- Fix OPDS v2 manifest crash
## v1.9.20
- Features
- Browser filters now show selected filters more clearly in the menus.
- Fix filtering on credits when clicking on metadata chips.
- OPDS v2 credits and subjects now link to views filtered on the tag.
## v1.9.19
- Fixes
- Fix PDF date parsing bug on import.
- Fix comic names with null volume_to field.
- Provide volume name in comic title for OPDS v2 as there's no volume view
available.
- Fix OPDS v2 folder as collection naming.
- Dev Notes
- Vuetify 4.0 frontend
## v1.9.18
- Fixes
- Fix search indexing the universes tag.
## v1.9.17 Broken build
## v1.9.16
- Fixes:
- Fix broken Reader close button.
- Fix OPDS v2 progression crash
## v1.9.15
- Fixes:
- Fix OPDS v2 progression crash.
- Adjust OPDS v2 progression position field to be 1 based, while Codex page
is 0 based.
## v1.9.14
- Fixes:
- Fix bad href validation in OPDS v2 leading to null links and therefore
invalid OPDS.
## v1.9.13
- Change:
- Codex only accepts auth tokens prefixed with the Bearer header.
- Fixes:
- Fix crash on API schema generation.
- Fix scribe daemon not shutting down properly.
- Fix Swagger API docs crash.
- Fix full text search barfing on single quoted terms.
- Return 401 and authorization v1 json to OPDS instead of 403 on auth
failure more often.
- Features:
- OPDS order by filename.
## v1.9.12
- Fixes:
- Fix accepting OPDS v2 progression correct Content-Type.
- Fix parsing OPDS v2 progression position.
- Fix bookmark update crash.
## v1.9.11
- Fixes:
- Fix OPDS v1 pagination.
## v1.9.10
- Fixes
- Fix Remote-User authorization for proxy setting http headers.
- Fix order links not appearing in OPDS v1
- Feature:
- Allow "Bearer" header for token authentication in addition to "Token"
## v1.9.9
- Fixes
- Access control bug would let users see comics in include groups they were
not a part of.
- Remove redundant top links from OPDS v2 facets group block.
- Fixed a status crash.
- Features
- Remote-User HTTP header SSO support activated by an environment variable.
See docs.
- HTTP Header Auth Tokens are now accepted as authorization. Useful for OPDS
and possibly some SSO setups.
## v1.9.8
- Bad release. Does not start.
## v1.9.7
- Fixes
- Fix OPDS v2 manifest/reading crash.
- Fix OPDS v2 search.
## v1.9.6
- Fixes
- Fix OPDS feed crash.
## v1.9.5
- Fixes
- Fix settings not resetting to default on OPDS start pages.
- Isolate OPDS settings from web settings so they don't reset each other.
## v1.9.4
- Fixes
- Fix unnecessary redirects in OPDS v1 & v2 search.
## v1.9.3
- Fixes
- Unnecessary redirect going to OPDS v1 start page.
- Fixed start page link in OPDS v2.0
## v1.9.2
- Fixes
- OPDS 2.0 homepage preview sections were showing the wrong queries.
- OPDS 2.0 progression bookmarking was broken. Stump doesn't seem to be
transmitting progression data yet, though.
- Features
- OPDS 1.0 gains a homepage similar to OPDS v2
- OPDS start page URLS are now different. See the OPDS popup dialog.
## v1.9.1
- Fixes
- OPDS v2 fix crash on faceted views.
## v1.9.0 - OPDS 2.0 Redesign
- Fixes
- Fix crash in OPDS v2 feed.
- Features
- Redesigned OPDS v2 to work with Stump & Readest Clients
- Docker healthcheck endpoint. See docs.
## v1.8.22
- Fixes
- Fix reading books crash that crept into the last release.
## v1.8.21
- Fixes
- Fix Close Book button linking to parent of desired route.
## v1.8.20
- Fixes
- Fix dragging when zoomed in in reader when using mouse left button.
- Fix weird spacing and borders on browser and reader toolbars.
- Attempt to make Close Book route in reader more accurate.
## v1.8.19
- Fixes
- Do not sanitize Library and Comic path names. Fixes an import crash on
paths with dangerous looking strings in them.
- Fix a crash when moving comics.
## v1.8.18
- Fixes
- Filter and Search parameters were being applied after clearing.
- Features
- Read metadata from PDF embedded files.
## v1.8.17
- Features
- Python 3.14 support.
- Fixes
- Fix for codex sometimes shutting when asked to restart.
## v1.8.16
- Don't support Python 3.14 until dependencies do.
- Docker image is now based on trixie debian
## v1.8.15
- Performance
- Replace axios with xior AJAX library.
## v1.8.14
- Fixes
- Fix settings not saving in reader and sometimes in browser.
- Fix shutdown and restart signals on Windows.
## v1.8.13
- Fixes
- Fix import crash when metadata import is disabled.
## v1.8.12
- Fixes
- Fix publishers and series tags disappearing. Use Library Force Update to
restore them.
- Fix credit role order in tags screen.
## v1.8.11
- Fixes
- Restarting Codex with Codex would still crash. More comprehensive fix.
## v1.8.10
- Fixes
- Restarting Codex with Codex would crash, so Auto-Update restarts were
broke.
- Sorry non-docker homies! Thanks @professionaltart.
- Fix universes not being search indexed (very rare).
- More reliable re-render for browser page slider and navigation buttons.
## v1.8.9
- Fixes
- Attempt to fix not saving new stat values if update doesn't update comic
metadata.
- Attempt to fix import bug where update metadata is disposed of before it
can be applied.
- Attempt to fix browser page slider not updating to match the current
folder.
## v1.8.8
- Fixes
- Fix import crash on some Metron tagged comics with a primary identifier
source.
## v1.8.7
- Fixes
- Make Full Text Search table smaller. Some irrelevant fields and data were
causing exponential bloat, crippling large libraries.
- Fix search by field for string fields, identifiers and issue.
- Fix overflow on browser card titles.
## v1.8.6
- Fixes
- Scale search index sync batch size with memory. Large search sync query
was preventing search sync of large databases on small machines.
## v1.8.5
- Fixes
- Better fix for persistent settings. Old fix could lead to settings that
never clear.
## v1.8.4
- Fixes
- Fix not importing page_count when import metadata is off, leads to
unreadable books.
- Fix persistent settings.
## v1.8.3
- Fixes
- Fix searching by string like fields in the search bar like 'path'.
- Fix minor search update exception when there's nothing search related to
update on import.
## v1.8.2
- Fixes
- Force docker logs (all stdout logs) to colorize output.
## v1.8.1
- Fixes
- Fix minor search update exception.
## v1.8.0
- Features
- Support
[MetronInfo.xml v1.0 Schema](https://metron-project.github.io/docs/category/metroninfo)
for comic metadata.
- Support CB7 (7zip) comic archives.
- Search Indexing now also done in the import phase which is faster than
syncing from the db:
- 20k comics both imported and indexed in 9 minutes on my old macbook at
a rate of 30 comics per second. Half a million comics might take less
than 5 hours on the same machine.
- However this update replaces the search index and Codex will sync the
search index when it starts.
- Fewer database updates when comics change metadata or don't change
metadata at all means fewer and faster update imports.
- New Tags:
- Add Universes. Main Characters, and Main Team marked with a ★.
- Metron tags can provide links to web databases for each tag.
- New Import Metadata on Demand Admin Flag.
- Add manual Import Abort task for admins.
- Browser tab titles show route and page title, no longer show custom
banner.
- Logging format changed.
- Fixes
- Reading Order in reader was not always defaulting to your last Browser Top
Group.
- Fixed filtering on Critical Rating and FileType
- Fixed searching on Critical Rating field.
- Bug displaying Age Rating, Original Format, Scan Info, Tagger in metadata.
- Fixed database locking crashes.
- Attempt to fix major version change CSRF errors by forcing logout if CSRF
errors occur.
- Dev
- Uses Comicbox 2.0 see
[Comicbox NEWS for all details](https://github.com/ajslater/comicbox/blob/main/NEWS.md)
## v1.7.15
- Fixes
- Fix unable to change top group to folders or story arcs if on root browse
page.
## v1.7.14
- Fixes
- Fix Reader jumping back into the same book when changing to a new book.
## v1.7.13
- Fixes
- Fix Reader Reading Order dropdown unable to change reading order.
- Fix loading root browser pages greater than 1 as a new route.
- Missing books error screen had no toolbars, could not close it.
- Fix docker configuration for Synology by creating a comicbox config
directory.
- Wait to load browser and reader pages until login tried with a cookie.
- Features
- Experimental `CODEX_BROWSER_MAX_OBJ_PER_PAGE` env variable. Default
is 100.
## v1.7.12
- Fixes
- Crash downloading comics when user is in a group.
- User last active activity was often not recorded.
- Features
- Animate Pages option toggle in reader settings
## v1.7.11
- Fixes
- Fix importing of story arc tags
## v1.7.10
- Fixes
- Fix occasional import link tags crash.
## v1.7.9
- Fixes
- Tags page layout fixes.
## v1.7.8
- Features
- Tags page layout changes with more notes from @beville.
- Fixes
- Tags for simple comic fields were incorrectly taken from the first comic
in a group.
- Filter menu wasn't populating for top folder.
- Librarian status tasks would appear out of order.
## v1.7.7
- Fixes
- Fix Librarians Statuses not loading in UI.
- Tags screen crash with untagged series or volumes.
- Fix tags screen Mark Read confirmation names.
- Order Tags Contributors by relevance not alphabetically.
## v1.7.6
- Features
- Always Show Filenames setting for browser cards.
- Tags page layout changes with notes from @beville.
- Fixes
- Fix import crash on files with escapable html characters or utf-8 chars in
the filename.
- Fix page range crash in reader.
- Fix overzealous escaping of html in metadata strings.
- Fix group tags not aggregated properly in metadata screen for folders and
story arcs.
## v1.7.5
- Fixes
- More granular websocket notifications means the web UI does fewer more
targeted data updates.
- Prevent deactivating and deprivileging the logged in user.
- Fix resetting active flag when resetting admin user with environment
variable.
- Fix crash in auto-update when unable to determine current Codex version.
## v1.7.4
- Fixes
- Fix setting bookmarks and book settings bug.
- Sanitize HTML out of imported comic metadata fields and admin inputs.
## v1.7.3
- Features
- Browser Order By Child Count.
- Customized site title option in Admin Flags.
- Codex logs are now compressed when rotated.
- Support OPDS v2.0 Progression (streaming) draft proposal from Aldiko.
- Fixes
- Browser Group Mark Read/Unread obeys browser filters.
- Fixed bad redirect when to deep linking into browser Folders or Story
Arcs.
- Fixed order by name for issues disregarding volume names in browser and
reader
- Fix display of name and filename on browser cards.
- Fix batching import of Contributors to prevent crash on large imports.
- Fixed an OPDS Metadata crash.
## v1.7.2
- Features
- Download entire groups of comics from the browser and metadata screens.
- Fixes
- Fix reader page API sometimes crashing.
## v1.7.1
- Features
- Always display filename for comics on browser cards in Folder View
- Detect .jxl extension (JPEG XL) as a comic page.
- Fixes
- Fix ignoring MacOS resource forks in archives.
- Don't redirect to issue view on first search in Folder View
## v1.7.0
- Features
- Search
- Use SQLite Full Text Search v5 for search engine.
- Search syntax has changed. See the help popup at the end of the search
bar.
- Use faster db column lookups for some search bar queries. Thanks
@bmfrosty.
- You may remove the directory `config/whoosh_index`.
- Integrity Checks
- Faster more comprehensive db integrity checks run every night instead of
at startup.
- Integrity checks can run on startup with environment variables documented
in README.
- Fixes
- Actually fix browser opening reader at correct bookmark.
- Also fixes progress calculation on browser cards.
- Fixed crashes when the upstream codex version is not accessible.
- Fixed possible race conditions with nightly maintenance.
## v1.6.19
- Fixes
- Fix browser opening reader at correct bookmark.
- Fix for browser triple tap bug for android tablet browsers in desktop
mode.
- Fix populating arcs in reading order menu in reader.
- Fix submitting old arc to reader API.
- Fix Version API blocking. Add check version admin task.
- Fix Library "Poll Every" validation.
- Fix Metadata dialog not scrolling sometimes.
- Fix file extension for downloaded PDF pages.
## v1.6.18
- Yanked. Broken Reader.
## v1.6.17
- Features
- Admin Action buttons now responsive to view size.
- Fixes
- Auto update wasn't comparing versions well.
- Possible fix for initializing admin flags crash.
## v1.6.16
- Fixes
- Import may have been marking mounted drive's comics modified
inappropriately.
- Import crash when moving comics.
- Relink deep orphan folders in the db instead of recreating them.
- Do not adopt orphan folders deleted from the filesystem.
- Admin Tab change password for user broke.
- More robust ui cache busting on library update.
- Fix minor error on metadata text boxes with null values.
## v1.6.15
- Fixes
- Fix more Metadata links to browser groups not computing and resolving
properly.
## v1.6.14
- Fixes
- Fix Metadata links to browser groups not resetting topGroup properly.
## v1.6.13
- Fixes
- Admin Panel Link was showing in the admin panel, not in the browser or
reader.
## v1.6.12
- Features
- Native Windows installation instructions in the README thanks to
@professionaltart.
- Anonymously send stats to improve Codex. See admin/flags for description
and opt-out.
- Fixes
- Detect iOS devices in Desktop Mode for proper iOS tap behavior.
## v1.6.9, v1.6.10, v1.6.11
- Yanked. Bad network behavior. Broken javascript.
## v1.6.8
- Fixes
- Fix OPDS streaming in lazy metadata mode for Chunky-like readers which
require a page count.
## v1.6.7
- Fixes
- OPDS authorization for some readers
- Remove superfluous debug exception trace on timezone endpoint.
## v1.6.6
- Fixes
- User creation in admin panel broke.
- There was confusing UI on admin panel unauthorized screen.
## v1.6.5
- Fixes:
- Fix logout button not working.
## v1.6.4
- Fixes:
- Reader crash loading reader order arcs.
- OPDS datetimes now uniformly served in iso format.
- Fix browser filter menus clearing and loading irregularities.
- Fix parsing negative issue numbers in filenames.
- Log common non-ComicBookInfo archive comments with less alarm.
- Removed
- LOGLEVEL=VERBOSE deprecated for a long time. Use LOGLEVEL=DEBUG
## v1.6.3
- Features
- Reader inherits the last browser view, with filters, as it's default
reading arc.
- When browser page is less than 1, redirect to parent. When 1 and empty,
show empty page.
- Fix
- The cover api was not accepting http basic (opds) authentication.
## v1.6.2
- Fixes
- Fix pagination with more than 100 comics in the browser.
## v1.6.1
- Features
- Add a retry button on book load error and page load error pages.
- Fixes
- Fix unable to login if anonymous users prohibited.
- Fix filter crash.
- Metadata was showing incorrect groups for individual comics
## v1.6.0
- Features
- Custom Covers for Folders, Publishers, Imprints, Series and Story Arcs.
- Browser setting to choose Dynamic or First group covers. Thanks @Thakk.
- Breadcrumbs in the browser
- Reader can read by Volumes as well as by Series, Folder and StoryArc.
- More compact UI controls.
- Metadata tags can click to browse filtered on that tag.
- Experimental API throttling support. Search the README for "throttle".
- Add websocket updates for anonymous sessions
- Speed and caching optimizations.
- Fixes
- OPDS http basic authorization fixed.
- Groups with the same name in different cases collapse into one group in
the browser.
- Order By respects browser show groups settings.
- Fixed re-import of urls and identifiers.
- Fixed cleanup of some foreign keys when no longer used.
- Clean up all orphan folders on startup instead of first pass
- Fix creating bookmarks.
- Update browser sessions for user when users finish a book.
## v1.5.19
- Fixes
- Metadata crash on folders.
## v1.5.18
- Fixes
- Ignore comic pages from dotfiles and macOS resource forks.
## v1.5.17
- Fixes
- Fix background color of browser card controls since vuetify update.
## v1.5.16
- Fixes
- Fix creating and updating exclude groups.
- More Web & url tags parsed from metadata.
## v1.5.15
- Fixes
- OPDS streaming broken for some clients (Chunky) without metadata.
- OPDS redirects for empty pages or 404's were crashing.
- OPDS uses filename fallback for title if missing metadata.
## v1.5.14
- Features
- Relative folder path is now searchable if Folder View enabled.
- More granular caching hopefully for better performance.
- Fixes
- OPDS redirects were crashing.
- Null search was crashing metadata for single comics
- Fix a breakage with fast file static file serving.
- Change browser order by to something sensible when search cleared.
## v1.5.13
- Fixes
- Fix root folder for library sometimes not created on import.
- Fix redirect loop in browser when all members of a group deleted.
- Fix browser pagination buttons not advancing.
- Fix OPDS v2 crash
- Fix browser throbber not appearing when making query.
## v1.5.12
- Fixes
- Fix Folder browser offset pagination bug for folder with books and no
folders.
## v1.5.11
- Fixes
- Fix erroneous Folder View page out of bounds redirect.
## v1.5.10
- Fixes
- Folder view was not showing all the books on mixed folder & book pages.
- Shutdown and Restart admin tasks were not working.
## v1.5.9
- Fixes
- Crash when reading comics in folder view introduced in v1.5.8
## v1.5.8
- Fixes
- No search results was returning every comic instead of no comics.
- issue: field searching returned no results.
- issue_number, community_rating, & critical rating no longer require two
digits of precision.
- Excess books included in reader arc/folder/series.
- Features
- Even Lazier import when Import Metadata Admin flag turned off.
- issue: field search now combines numeric and suffix parts.
## v1.5.7
- Fixes
- Pagination crash with more than 100 folders.
- Experimental fix for Synology Docker CHOWN_PYTHON_SITE_PACKAGES=1
## v1.5.6
- Fixes
- Fix sqlite limit crash when importing > \~1000 web urls.
## v1.5.5
- Fixes
- Attempt to fix import crash processing too much metadata at once. Allow
undocumented env variable to manipulate this: CODEX_FILTER_BATCH_SIZE
(default: 900)
- Fix search engine update crash for large collections.
## v1.5.4
- Fixes
- Django 5 broke root_path prefixing from the asgi server. Work around it.
## v1.5.3
- Fixes
- Mouse horizontal scroll broken on Firefox.
## v1.5.2
- Fixes
- OPDS titles were showing as "Unknown" for comics with tagged volumes.
- OPDS v2 was crashing.
- Cover displayed for group browser with Name ordering was inconsistent.
- Enable mouse drag horizontal scrolling in reader zoom mode.
## v1.5.1
- Fixes
- OPDS v1 was not rendering any data.
## v1.5.0
- **Warning**
- The main database path has changed from `db.sqlite3` to `codex.sqlite3`
- This version forces a rebuild of the search index (not the main database)
- Fixes
- Some integrity checks weren't running on startup.
- The metadata page would sometimes crash for Admins.
- Moving a comic to a subfolder would crash.
- Moving a deep subfolders would crash.
- Moving a comic to the root folder would send the comic to the phantom
zone.
- Updating comics would sometimes not delete removed tags.
- Series & Volumes no longer updated too often on import.
- Admin Actions was polling all libraries when one selected.
- OPDS was showing repeated titles.
- Vertical scroller tracking and updating improved.
- Page filenames are now sorted case insensitively which should improve
order.
- Features
- Admin Exclude groups compliment the existing Include groups.
- New metadata tags: Monochrome, Tagger, GTIN, Review, Identifiers, &
Reading Direction. Available when comics are re-imported (Force Update
recommended).
- Identifiers metadata tag replaces the "Web" tag.
- Reads ComicInfo.xml and other formats from the PDF keywords field. You can
write ComicInfo.xml to PDFs with comictagger.
- Reading Direction reader setting replaces Reader's vertical & horizontal
views.
- Supports the MetronInfo metadata format (rare).
- Filesystem events filtered to only the ones Codex handles.
- Double-click to zoom on pages in reader.
- Read PDF with browser in a new tab link.
- Experimental checkbox for caching entire comic or PDF in the browser.
- Admin Flag for disabling most metadata import.
- Dev
- Using comicbox v1 for metadata import.
## v1.4.3
- Fixes
- Crash on undecodable characters in metadata.
- Search terms weren't applying to filter choices population.
- Fix name ordering. Show series & volume in browser cards if it affects
name ordering.
- Shrink reader page change boxes to let toolbar activate on corner clicks.
- Dev
- Big lint update.
## v1.4.2
- Fixes
- Groups were not aggregating children properly when searched.
- Search could break Folder View.
- Changing the browser 'Order By' would sometimes not apply.
- Attempt to fix stale books appearing on reader load.
## v1.4.1
- Fixes
- A bug that prevented folder view from displaying under some circumstances.
## v1.4.0
- Features
- Story Arc Top Group in Web & OPDS Browsers
- Support multiple Story Arcs per comic.
- Supports Mylar CSV StoryArc / StoryArcNumber extension to ComicInfo.xml
- Show only filter options that affect the current browse level.
- Reader has a Series/Folder/Story Arc order selector.
- Reader shows filename instead of metadata title if you've been browsing in
File View
- Downloads now use the original filename from disk.
- Fix
- Folder was view displayed but crashed in OPDS even if disabled by admin.
## v1.3.14
- Features
- Better metadata extraction for PDFs.
- Support for ComicInfo StoryArcNumber, Review and GTIN tags.
- Order by Story Arc Number
- Do not detect .cbr files if unrar is not on the path.
- Display filename for comics in browser file view.
- Fixes
- Import of ComicInfo Tags metadata.
- Never removed old missing metadata when updated.
- Error on moving folders.
- Fix saving last route between sessions.
- Better error messages if unrar is not on the path.
- Removed
- Remove support for unrar.cffi
## v1.3.13
- Fixes
- Group cover sometimes showing wrong cover for order.
- Rare import crash.
## v1.3.12
- Features
- OPDS 2 Last Read link.
- Fixes
- Books without bookmarks could break parts of the reader.
- Remove clipboard UI hints when clipboard isn't available.
## v1.3.11
- Features
- Last Read Order By option for web and OPDS.
- Some Order By options now have a default descending order.
- OPDS 1 special top links limited to 100 entries.
- Fix
- OPDS 1 links did not include filters or order information.
- OPDS 1 page streaming broke.
## v1.3.10
- Fixes
- Crash when reading from folder view.
## v1.3.9
- Features
- Experimental OPDS 2.0 Support.
- Create all comic covers admin task.
- Faster Metadata pages for web and OPDS.
- Fixes
- Two pages mode broken.
- Credits not imported bug.
- Failed imports not removed when file removed bug.
## v1.3.8
- Fixes
- Fix Basic Authentication not enabled for OPDS Cover, Page, and Download
views.
- Tune low memory algorithm slightly lower for memory constrained systems.
- Dev
- Use makefile and moved most scripts into bin.
## v1.3.7
- Feature
- Metadata page links to groups to browse to.
- Fixes
- Crash when moving comics.
- Container memory limits weren't detected for Linux kernels before 4.5
- Reader
- Horizontal Reader was slow for comics with high page counts.
- Vertical scroller was not tracking pages in fitTo Width or Orig modes.
- Validation error detecting child and parent library paths incorrectly.
- Dev
- Django 4.2
## v1.3.6
- Fixes
- Much lower memory tuning. Environment variables control tuning.
- Possible fix for vertical scroller page tracking for tall images.
## v1.3.5
- Fixes
- OPDS sorting and filtering broke.
- Fixed Download URLs for clients that ignore headers like Chunky.
- Update Search Index now checks for more missing entries.
## v1.3.4
- Fixes
- Number out of range errors for issue when search indexing.
- Total child pages of folders and groups sometimes overcounted, displaying
half unread folders.
- Reader: Vertical Scroll
- Remove black bottom margin from images.
- Was loading every page in a comic at once.
- Page tracking did not work with images larger than viewport width.
## v1.3.3
- Fixes
- Number out of range errors when search indexing.
- Possible Search Index Remove Stale and Abort jobs not scheduled properly.
- OPDS missing entry ids rejected by Panels reader.
- Downloads had an extra period in the suffix.
## v1.3.2
- Fixes
- Reader Fit To settings broken
- Possible files marked modified too often.
## v1.3.1
- Fixes
- An import crash in create foreign keys.
- Admin table dates were always in UTC so sometime off by a day.
## v1.3.0
### I remember... my whole life. Everything
- Features
- Codex stable in 1GB RAM environments. Faster with more.
- Codex uses unrar-cffi if available. Not required.
- Browser
- Navigate to top button.
- Filter by File Type.
- OPDS
- Top links display only at catalog root.
- Extended metadata moved to alternate links.
- Admin
- Search Indexer Remove Stale Records task much faster.
- Comic import speedups.
- Fancier sortable admin tables.
- Removed `max_db_ops` config variable.
- Fixes
- Reader vertical scroll lost its place in Fit To Width or Orig mode.
- OPDS downloaded files all had the same name.
- Search Index
- More robust against bad data.
- Some search fields were case sensitive.
- Admin
- Graceful shutdown when Docker container stops.
- Codex was backing up on every startup.
- Status for batched imports (large imports or low memory) now reflects
total instead of single batch.
## v1.2.9
- Features
- Vertical scroll option for reader.
- Faster search index removes.
- Admin Users tab shows last user activity date.
- OPDSE PSE 1.2 extension for Panels `pse:lastReadDate`
- Fixes
- Fixed next and previous book keyboard shortcuts.
- Improved OPDS acquisition page performance by removing more "categories"
metadata.
## v1.2.8
- Features
- Search Index
- Improved search indexing times.
- Admin Flag to adjust nightly full optimization.
- OPDS
- "Newest Issues" Link replaced by "Recently Added" after user feedback.
- Fixes
- Volume tags were often not scanned. Recommend using Force Reimport on all
libraries.
- OPDS
- Fix navigation links not inheriting view settings of current page.
- Removed populating categories in OPDS to experiment with performance
issues.
- Fix OPDS pse lastRead tag.
- Block library polling during database updates, fixes reindexing.
## v1.2.7
- Fixes
- Trap final search index commit errors and try again without merging
segments.
- Fix moving folders assigned no parent folder, displaying them in root.
## v1.2.6
- Fixes
- Impose memory limits on search index writers.
- Impose items before write limits search index writer.
- Sort comics by path for the reader navigation when in Folder View.
- Remove inappropriate vertical scroll bars from page images.
## v1.2.5
- Features
- In Folder View the reader navigates by folder instead of series.
- Fixes
- OPDS crash on missing 24 hour time setting input required.
## v1.2.4
- Features
- User configurable 24 hour time format.
- Reader
- Displays covers as one page even in two page mode.
- Read in Reverse mode.
- Keymaps for adjusting page by one page in two page mode.
- Previous and Next book navigation buttons and keymaps.
- Fixes
- OPDS:
- Fix acquisition feed timeouts on large libraries by removing most m2m
fields that populated OPDS categories
- Fix pagination
- Show series name in comic title.
- Experiment: don't show top links or entry facets on pages > 1
- Reader:
- Two pages mode would skip pages.
- Next/prev book goes to correct page for Right To Left tagged books.
- Fix occasional error setting reader settings.
- Fixed noop poll event happening on comic cover creation.
## v1.2.3
- Fixes
- Prevent search indexing starting over if it encounters errors.
- Fix download buttons.
- Fix admin settings drawer obscuring small screens.
- Fix scroll bars showing inapproporately on admin tables.
- Fix OPDS authors having 'i' appended.
## v1.2.2
- Fixes
- Fix all items removed from search index after update.
- Speedups to cleaning up search engine ghosts.
## v1.2.1
- Fixes
- Crash on building a fresh database.
- Fixed an importer crash when it tried to wait for changing files.
- Disabling Library Poll prevented manual polling.
- More explicit Poll Every hints in edit dialog.
- Repository link didn't open a new window.
## v1.2.0
### What kind of Heaven uses bounty hunters?
- Features
- Faster and more robust PDF support. Codex no longer depends on the poppler
library.
- LOGLEVEL=VERBOSE deprecated in favor of DEBUG
- Stats page API accessible via API key as well as admin login.
- Fixes
- Some Librarian Status messages would appear never to finish.
- Development
- The multiprocessing method is now S P A W N 💀 on all platforms.
- Websockets are now handled by customized Django channels
- aioprocessing Queue communicates between librarian and channels.
## v1.1.6
- Fixes
- Fix rare deletion and recreation of all comics when inodes changed.
## v1.1.5
- Features
- Admin Stats tab
- Libraries can have a poll delay longer than 1 day.
- Fixes
- Crash when removing comics.
- Admin Create & Update dialogs would get stuck open on submit.
- Delete expired and corrupt sessions every night.
- More liberal touch detection for more devices.
## v1.1.4
- Fixes
- Multiprocessing speedup for large search engine indexing jobs
- Writes search engine data in segments.
- Search engine segment combiner optimizer runs nightly (and manually).
## v1.1.3
- Fixes
- Fix some OPDS browsers unable to read comics.
## v1.1.2
- Fixes
- Fix unable to initialize database on first run
## v1.1.0
### Whoosh
- Features
- Switch to Whoosh Search Engine.
- You may delete `config/xapian_index`.
- May run on Windows now?
- Moved db backups to `config/backups`.
- Backup database before migrations.
- Removed
- Do not store search history for combobox across sessions.
- Fixes
- Fix Admin Library folder picker.
- Uatu does a better job of ignoring device changes.
- Don't pop out of folder mode on searches.
- Fix showing error on unable to load comic image.
## v1.0.3
- Features
- Force update all failed imports admin task.
- Fixes
- Fix moving folders to subfolder orphans folders bug.
- Fix id does not exist redirect loop.
## v1.0.2
- Features
- Support for Deflate64 zip compression algorithm.
- Fixes
- Fix Failed Imports not retrying import when updated.
- Make db updates more durable and possibly problem comics paths in log.
- Discard orphan websocket connections from the connection pool.
- Fix Admin Status drawer closing at wrong time.
## v1.0.1
- Features
- Justify order-by field in browser cards.
- Fixes
- Fixed next book change drawer opening settings drawer.
- Fixed zero padding on browser card issue numbers.
## v1.0.0
### Vue 3
- Features
- Removed old django admin pages.
- Shutdown task for admins.
- Configure logging with environment variables. See README.
- Fixes
- Fix displaying error in login dialog.
- Fix saving community & critical rating filters to session
- Fix fit to screen not enlarging pages smaller than screen.
- Developer
- Frontend is now Vuetify 3 over Vue 3. Using options API.
## v0.14.5
- Fixes
- Fix crash on decoding some comics metadata charset encoding.
## v0.14.4
- Fixes
- Fix login not available when AdminFlag Enable Non Users was unset.
- Fix server PicklingError logging bug.
## v0.14.3
- Fixes
- Fix root_path configuration
## v0.14.2
- Fixes
- Fix Librarian process hanging due to logging deadlock.
- Fix reader keyboard shortcut help.
- Fix book change drawer appearing in the middle of books.
## v0.14.1
- Fixes
- Resolve ties in browser ordering with default comic ordering.
- Always close book change drawer before reader opens.
## v0.14.0
### Sliding Pages
- Features
- Animated sliding pages on reader.
- Comic & PDF pages display loading, rendering and password errors.
- Fixes
- Filters with compound names were not loading choices.
- Show only usable filters for current view as filter choices.
- Allow filtering by None values when None values exist.
- Handle an iOS bug with downloading pages and comics inside a PWA.
- Fixed PDF failure to render on load and after changing settings.
- Login & Change Password dialogs no longer activate Reader shortcuts by
accident.
## v0.13.0
### Admin Panel
- Features
- Single Page Admin Panel.
- Users may now change their own passwords.
- OPDS
- Use facets for known User Agents that support them. Default to using entry
links.
- Gain a Newest Issues facet, a Start top link and a Featured / Oldest
Unread link.
- More metadata tags.
- Special thanks to @beville for UX research and suggestions
- HTTP Basic auth only used for OPDS.
- Frontend components do lazy loading, should see some speedups.
- Fixes
- Fixed imprints & volume levels not displaying sometimes.
- Fix large images & downloads for some OPDS clients.
- Developer
- API v3 is more restful.
- /api/v3/ displays API documentation.
- Vite replaces Vue CLI.
- Pina replaces Vuex.
- Vitest replaces Jest.
- Django livereload server and debug toolbar removed.
## v0.12.2
- Fixes
- Fix OPDS downloading & streaming for Chunky Comic Reader.
- Hack in facets as nav links for Panels & Chunky OPDS readers.
## v0.12.1
- Fixes
- Disable article ignore on name sort in folder view.
- Fix browser navigation bug with issues top group.
## v0.12.0
### Syndication
- Features
- OPDS v1, OPDS Streaming & OPDS Search support.
- Codex now accepts HTTP Basic authentication.
- If you run Codex behind a proxy that accepts HTTP Basic credentials that
are different than those for Codex, be sure to disable authorization
forwarding.
- Larger browser covers.
- Sort by name ignores leading articles in 11 languages.
- Fixes
- Use defusexml to load xml metadata for safety.
- Removed process naming. My implementation was prone to instability.
## v0.11.0
### Task Monitor
- Features
- Librarian tasks in progress appear in the settings side drawer for
adminstratiors.
- Covers are now created on demand by the browser, rather than on import.
- Browser Read filter.
- Fixes
- Bookmark progress bar updates in browser after closing book.
- Metadata web links fix.
## v0.10.10
- Features
- Reader nav toolbar shows position in series.
- Fixes
- Fix inability to log in when Enable Non Users admin flag is unset.
- Simplify Admin Library delete confirmation page to prevent OOM crash.
- Move controls away from iphone notch and home bar.
## v0.10.9
- Fixes
- Fix null bookmark and count fields in metadata
- Fix indeterminate finished state when children have bookmark progress.
- Fix maintenance running inappropriately on first run. Crashed xapian
database.
- Fix reader metadata keymap
- Features
- Progressive Web App support
- Reader "Shrink to" settings replaced by "Fit to"
- Special Thanks
- To ToxicFrog, who's been finding most of these bugs I'm fixing for a
while.
## v0.10.8
- Fixes
- Fixed reader nav clicks always showing the toolbars.
- Attempt to fix unwanted browser toolbars when treated as mobile app
- Wait half a second before displaying reader placeholder spinner.
- Fix metadata missing search query.
- Fix metadata cache busting.
- Features
- Accessibility enhancements for screen readers.
## v0.10.7
- Features
- Browser tries to scroll to closed book to keep your place.
- Fixes
- Fixed missing lower click area on browser cards.
- Fixed session bookmark interfering with logged in user bookmark.
## v0.10.6
- Broken docker container
## v0.10.5
- Features
- Reader shrink to screen setting becomes fit to screen and embiggens small
images.
- Reader changing to the next book now has visual feedback and requires two
clicks.
- Fixes
- Removed vertical scrollbars when Reader shrunk to height.
- Don't disturb the view when top group changes from higher to lower.
## v0.10.4
- Fixes
- Fix double tap for non-iOS touch devices.
- Features
- Shrink to Screen reader setting.
- Reader throbber if a page takes longer than a quarter second to load.
## v0.10.3
- Fixes
- Fix PDF going blank when settings change.
- Remove vestigial browser scrollbars when they're not needed. Thanks to
ToxicFrog.
- Fix cover cleanup maintenance task.
## v0.10.2
- Fixes
- URLS dictate view over top group. Fixes linking into views.
- Fix possible cover generation memory leak.
- Build a deadfall trap for search indexer zombies. Use Offspring's brains
as bait.
## v0.10.1
- Fixes
- Linked old top level comics orphaned by library folders migration.
## v0.10.0
### Portable Document Format
- Features
- PDF support. Optional poppler-utils binary package needed to generate PDF
cover thumbnails.
- CBT support. Tarball comic archives.
- Alphanumeric issue support. Requires rescanning existing comics.
- Individual top level folders for each library.
- Don't duplicate folder name in filename sort.
- Fixes
- Comic file suffixes now matched case insensitively.
- Finished comics count as 100% complete for bookmark aggregation.
- Mark all folder descendant comics un/read recursively instead of immediate
children.
- Don't leak library root paths in Folder View for non-admins in the API.
- Fixed aggregation bug showing inaccurate data when viewing group metadata.
- More accurate Name sorting.
- Fixed default start page for RTL comics.
- Disabled reading links for empty comics.
- Shield radiation from Venus to reduce zombie incidents.
## v0.9.14
- Fixes
- Fix comicbox config crash.
- Use codex config namespace (\~/.config/codex) so codex doesn't interfere
with standalone comicbox configs.
- Comic issue numbers display to two decimal points instead of using ½
glyphs.
- Features
- Filename order by option. Disabled if the "Enable Folder View" Admin Flag
is off.
## v0.9.13
- Fixes
- Fix root_path configuration for running codex in url sub-paths
- Parse new filename patterns for metadata.
- Slightly faster comic cover generation.
## v0.9.12
- Fixes
- Fix setting global reader settings.
- Fixed reader settings not applying due to caching.
- Bust reader caches when library updates.
- Reader titles smaller and wrap on mobile.
- Fixed deep linking into reader.
- Features
- Disable reader prev/next touch swiping for phone sized browsers.
## v0.9.11
- Fixes
- Fixed covers not creating on import.
- Covers update in browser when updated on disk.
- Create missing covers on startup.
- Bust browser cache when library updates.
- Reader settings were not applying in some cases.
- Fixed crash updating latest codex software version from the internet.
- Fixed crash loading admin page.
- Features
- Codex processes show names in ps and thread names on Linux.
- Add Poll libraries action to FailedImports Admin Panel.
- Space and shift-space previous and next reader shortcuts.
- Reader settings UI redesigned to be clearer.
## v0.9.10
Yanked. Crash loading admin page.
## v0.9.9
- Fixes
- Fixed combining CBI credits with other format credits
- Failed imports notification appears only for new failed imports.
- Features
- Update search index daily.
- Clean up orphan comic covers every night.
## v0.9.8
- Fixes
- Fixed search index update crash while database is still updating.
- Fixed issues larger than 99 bug.
- Fixed issue not imported due to metadata cleaning bug.
- Fixed crash updating search index while library was still updating.
- Thread error trapping and diagnostics to root out zombie process issue.
- Sort numeric terms in filter menus numerically not alphabetically.
- Fixed comic name display wrapping in browser.
- Features
- More comprehensive metadata sanitizing before import.
- Reduced time checking to see if files have finished writing before import.
- Uniform format for metadata parsing logging.
- Credits sorted by last name.
## v0.9.7
- Fixes
- Coerce decimal values into valid ranges and precision before importing.
- Features
- Clean up unused foreign keys once a day instead of after every import.
- Clean up unused foreign keys librarian job available in admin panel.
## v0.9.6
- Fixes
- Don't open browser when a library changes when reading a comic.
- Fixed crash creating illegal dates on import.
- Features
- Replace description field with more common ComicInfo comments field.
- Log files now rotate by size instead of daily.
- Log path for failed imports and cover creation.
## v0.9.5
- Fixed
- Use an allow list for importing metadata to prevent crashes.
## v0.9.4
- Fixes
- Fixed crash when importing comments metadata.
## v0.9.3
- Fixes
- Import credits data for CBI and CIX tagged comics.
- More liberal metadata decimal parsing.
## v0.9.2
- Fixes
- Fix rare migration bug for Cover Artist role.
## v0.9.1
- Fixes
- Fix to library group integrity checker
## v0.9.0
### Private Libraries
- Features
- Libraries may have access restricted to certain user groups.
- The "Critical Rating" tag is now a decimal value.
- The "Community Rating" tag replaced "User Rating" tag, a decimal value.
- Cover Credits role replaced by "Cover Artist".
- Reader has a "Download Page" button.
- Metadata dialog highlights filtered items.
- Metadata dialog is faster.
- Admin Queue Job for creating missing comic covers.
## v0.8.0
### Search
- Features
- Metadata search field in browser
- Settings dialogs replaced with side drawers
- Changed some keyboard shortcuts in reader.
- "group by" renamed to "top group".
- Admin panel gained a Queue Jobs page.
- Fixes
- Browser does a better job of remembering your last browser view on first
load.
- Reader's "close book" button now does a better job returning you to your
last browser view.
- Metadata panel cleanup and fix some missing fields.
- Binary Dependencies
- Codex now requires the Xapian library to run as a native application
- Drop Support
- The linux/armhf platform is no longer published for Docker.
- License
- Codex is GPLv3
## v0.7.5
- Fixes
- Fix integrity cleanup check for old comic_folder relations that prevented
migrations.
## v0.7.4
- Fixes
- Fix integrity cleanup check for more types of integrity errors that may
have prevented clean db migrations.
- Fix last filter, group, sort not loading properly for some new views.
## v0.7.3
- Fixes
- Fix crash updating latest version.
- Fix a folder not found crash in folder view.
- Features
- Database indexing speedups.
## v0.7.2
- Fixes
- Fix another integrity check bug
## v0.7.1
- Fixes
- Fix and integrity check crash that happened with an older databases.
- Features
- Added `CODEX_SKIP_INTEGRITY_CHECK` env var.
## v0.7.0
### Feels Snappier
- Database Migration
- v0.7.0 changes the database schema. Databases run with v0.7.0+ will not
run on previous versions of codex.
- Features
- Big speed up to importing comics for large imports.
- Speed up creating comic covers on large imports.
- Admin Panel options for polling (formerly "scanning") and watching events
have changed names.
- Admin Panel task added to regenerate all comic covers.
- Browser Admin Menu option added for polling all Libraries on demand.
- Comics with no specified Publishers, Imprints and Series no longer have
induced default names for these but have no name like Volumes.
- Codex repairs database integrity on startup.
- Codex backs up the database every night.
- Autodetect server timezone (for logging).
- Use TZ and TIMEZONE environment variables to explicitly set server
timezone.
- Added `VERBOSE` logging level to help screen out bulk `DEBUG` messages
from dependencies.
- Truncated logging messages for easier reading.
- Fixes
- Fixed metadata screen displaying incorrect information.
- Now compatible with python 3.10.
## v0.6.8
- Fixes
- Fixes some import bugs with filename parsing when there are no tags
- Fixed two page view toggle hotkey
- Features
- Browser now tells you what kind of items you're looking at.
- Reader swiping navigation
- Reader keyboard shortcut help dialog
- Tentative linux/armhf support. No way for me to test this
- Vacuum the sqlite database once a day to prevent bloat
- Corrupt database rebuild procedure. See README.
## v0.6.7
- Dark admin pages and fix template overrides.
## v0.6.6
- Automate multi-arch builds
## v0.6.5
- Build Docker images for amd64 & arm64 manually.
## v0.6.4
- Fix reader bug that only displayed first page
## v0.6.3
- Add LOGLEVEL environment variable.
- Set to DEBUG to see everything.
- Removed DEV environment variable.
- Possible fix for newly imported covers not displaying.
## v0.6.2
- Fixes
- Fixed intermittent Librarian startup crash in docker.
- Fixed DEBUG environment variable to be able to run in production.
- Dev
- Added DEV environment variable for dev environment.
## v0.6.1
- Fixes
- Fix librarian startup crash. Prevented admin actions from happening.
## v0.6.0
### Better Filtering and Sorting
- Features
- New Filters
- New sort options: Updated Time and Maturity Rating
- New frontend URL scheme
- New API
- Added time to the browse card when sorting by time fields
- Browser pagination footer now remains fixed on the page
- Browser pagination footer is now a slider to handle larger collections
- Notifications now appear in reader as well as browser
- On comic import failure, log the path as well as the reason
- Codex version information moved to Browser > Settings
- Fixes
- Fixed a bug importing Story Arc Series Groups and Genres. Requires
re-import to correct.
- Fixed a bug with sorting that grouped improperly and showed the wrong
covers for reverse sorts.
- Scanning notifications on login not disappearing bug squashed
- Fixed a bug where the browser settings menu wouldn't close when opening a
dialog
## v0.5.18
- Fixes
- Fix filters not changing display bug
## v0.5.17
- Fixes
- Fix root_path not parsing bug
## v0.5.16
- Fixes
- Fix broken startup when parsing json shared between front and back end
## v0.5.15
- Features
- Metadata popup is now faster.
- Metadata popup now shows created_at, updated_at and path (if admin).
- Removed numeric and common password validators. Made the minimum length 4.
## v0.5.14
- Features
- Metadata view for browse containers. Also observes filters.
- Covers now regenerate on re-import.
- Fixes
- Fix scanning notification
- Fix unable to delete libraries bug
## v0.5.13
- Features
- Admin Flag for automatically updating codex
- Force updates from the admin panel with an Admin Flag action
- Snackbar for notifying about failed imports
## v0.5.12
- Features
- Admin page for failed imports
- Snackbar tells admins when scans are happening
- Report the latest version available in the browser footer tooltip
- Admin flag for disabling codex for non-users
## v0.5.11
- Features
- Browser rows now adapt to browse tile size
- Browser covers for containers now match data we're sorting by
- Serve static files faster
- Fixes
- Reader fix settings for all comics were not setting properly
- Fix bookmarks for sessions that aren't logged in
## v0.5.10
- Fixes
- Fix filtering bugs
- Fix mark read/unread bugs
- Fix reader settings not setting properly
- Fix reader images positioning
- Fix minor crash closing books with uninitialized browser app
## v0.5.9
- Fixes
- Fix sorting for folder view
- Fix import bugs
- Features
- Display sort key value in browse tile
- Display standard image for missing covers
- Slightly more helpful 404 page
## v0.5.8
- Upload mistake with 0.5.7. This is just a version bump.
## v0.5.7
- Fixes
- Fix import crashes
- Remove scan locks on startup
- Features
- Allow credits with an empty role
- Pagination of large browse results
- Center comic pages better
- Add download link to browser menu
- Log to files as well as console
## v0.5.6
- Fixes
- Websocket path security wasn't handling leading slashes well. Skip it.
## v0.5.5
- Fixes
- Revert to whitenoise 5.1.0 which works with subpaths
## v0.5.4
- Fixes
- Fix crash on start if all static dirs do not exist.
## v0.5.3
- Fixes
- Fixed login bug introduces in v0.5.3 (thanks hubcaps)
- Fixed filtering bug introduced in v0.5.2
- Features
- Versioned API
- Toast popup for admins indicating libraries are scanning.
- Periodic frontend refresh during long scans.
- Codex version displayed in browser footer
## v0.5.2
- Features
- Lazy load filter choices when the menu opens
- Documentation moved into admin panel
- Fixes
- Fix multiprocessing for Windows
## v0.5.1
- Minor bugfixes.
## v0.5.0
### First useful working version
- Productionized alpha release
## v0.4.0
### Polished UI
- Polished VueJS frontend
## v0.3.0
### I'm a frontend developer!
- Single Page VueJS frontend PoC without much styling
## v0.2.0
### It's alive
- Working application with all initial features
- Django frontend
## v0.1.0
### Hello world
- Proof of concept.
================================================
FILE: README.md
================================================
# Codex
A comic archive browser and reader.
<img src="/img/logo.svg" style="
height: 128px;
width: 128px;
border-radius: 128px;
" />
## 🚨 Announcement 🚨
### Docker
The Docker image has moved to
[ghcr.io/ajslater/codex](https://github.com/ajslater/codex/pkgs/container/codex).
A final docker.io image has been released on dockerhub.
## ✨ Features
- Codex is a web server.
- Full text search of comic metadata and bookmarks.
- Filter and sort on all comic metadata and unread status per user.
- Browse a tree of Publishers, Imprints, Series, Volumes, or your own folder
hierarchy, or by tagged Story Arc.
- Read comics in a variety of aspect ratios and directions that fit your screen.
- Watches the filesystem and automatically imports new or changed comics.
- Anonymous browsing and reading or reigistered users only, to your preference.
- Per user bookmarking & settings, even before you make an account.
- Private Libraries accessible only to certain groups of users.
- Reads CBZ, CBR, CBT, and PDF formatted comics.
- Syndication with OPDS 1 & 2, streaming, search and authentication.
- Add custom covers to Folders, Publishers, Imprints, Series, and Story Arcs.
- Remote-User HTTP header SSO support.
- Runs in 1GB of RAM, faster with more.
- GPLv3 Licenced.
### Examples
- _Filter by_ Story Arc and Unread, _Order by_ Publish Date to create an event
reading list.
- _Filter by_ Unread and _Order by_ Added Time to see your latest unread comics.
- _Search by_ your favorite character to find their appearances across different
comics.
## 👀 Demonstration
You may browse a [live demo server](https://demo.codex-reader.app/) to get a
feel for Codex.
## 📜 News
Codex has a [NEWS file](NEWS.md) to summarize changes that affect users.
## 🕸️ HTML Docs
[HTML formatted docs are available here](https://codex-comic-reader.readthedocs.io)
## 📦 Installation
### Install & Run with Docker
Run the official
[Docker Image](https://github.com/ajslater/codex/pkgs/container/codex) at
ghcr.io/ajslater/codex.
Read the [Docker instructions](docs/DOCKER.md)
You'll then want to read the [Administration](#administration) section of this
document.
### Install & Run on HomeAssistant server
If you have a [HomeAssistant](https://www.home-assistant.io/) server, Codex can
be installed with the following steps :
- Add the `https://github.com/alexbelgium/hassio-addons` repository by
[clicking here](https://my.home-assistant.io/redirect/supervisor_add_addon_repository/?repository_url=https%3A%2F%2Fgithub.com%2Falexbelgium%2Fhassio-addons)
- Install the addon :
[click here to automatically open the addon store, then install the addon](https://my.home-assistant.io/redirect/supervisor)
- Customize addon options, then then start the add-on.
### Install & Run as a Native Application
You can also run Codex as a natively installed python application with pip.
#### Binary Dependencies
You'll need to install the appropriate system dependencies for your platform
before installing Codex.
##### Linux Dependencies
###### Debian Dependencies
...and Ubuntu, Mint, MX, Window Subsystem for Linux, and others.
```sh
apt install build-essential libimagequant0 libjpeg-turbo8 libopenjp2-7 libssl libyaml-0-2 libtiff6 libwebp7 python3-dev python3-pip sqlite3 unrar zlib1g
```
Versions of packages like libjpeg, libssl, libtiff may differ between flavors
and versions of your distribution. If the package versions listed in the example
above are not available, try searching for ones that are with `apt-cache` or
`aptitude`.
```sh
apt-cache search libjpeg-turbo
```
###### Alpine Dependencies
```sh
apk add bsd-compat-headers build-base jpeg-dev libffi-dev libwebp openssl-dev sqlite yaml-dev zlib-dev
```
##### Install unrar Runtime Dependency on non-debian Linux
Codex requires unrar to read CBR formatted comic archives. Unrar is often not
packaged for Linux, but here are some instructions:
[How to install unrar in Linux](https://www.unixtutorial.org/how-to-install-unrar-in-linux/)
Unrar as packaged for Alpine Linux v3.14 seems to work on Alpine v3.15+
##### macOS Dependencies
Using [Homebrew](https://brew.sh/):
```sh
brew install jpeg libffi libyaml libzip openssl python sqlite unrar webp
```
#### Installing Codex on Linux on ARM (AARCH64) with Python 3.13
Pymupdf has no pre-built wheels for AARCH64 so pip must build it and the build
fails on Python 3.13 without this environment variable set:
```sh
PYMUPDF_SETUP_PY_LIMITED_API=0 pip install codex
```
You will also have to have the `build-essential` and `python3-dev` or equivalent
packages installed on on your Linux.
#### Windows Installation
Windows users are encouraged to use Docker to run Codex, but it will also run
natively on the Windows Subsystem for Linux.
Installation instructions are in the
[Native Windows Dependencies Installation Document](docs/WINDOWS.md).
#### Run Codex Natively
Once you have installed codex, the codex binary should be on your path. To start
codex, run:
```sh
codex
```
### Use Codex
Once installed and running you may navigate to <http://localhost:9810/>
## 👑 Administration
### Navigate to the Admin Panel
- Click the hamburger menu ☰ to open the browser settings drawer.
- Log in as the 'admin' user. The default administrator password is also
'admin'.
- Navigate to the Admin Panel by clicking on its link in the browser settings
drawer after you have logged in.
### Change the Admin password
The first thing you should do is log in as the admin user and change the admin
password.
- Navigate to the Admin Panel as described above.
- Select the Users tab.
- Change the admin user's password using the small lock button.
- You may also change the admin user's name with the edit button.
- You may create other users and grant them admin privileges by making them
staff.
### Add Comic Libraries
The second thing you will want to do is log in as an Administrator and add one
or more comic libraries.
- Navigate to the Admin Panel as described above.
- Select the Libraries tab in the Admin Panel
- Add a Library with the "+ LIBRARY" button in the upper left.
### Reset the admin password
If you forget all your superuser passwords, you may restore the original default
admin account by running codex with the `CODEX_RESET_ADMIN` environment variable
set.
```sh
CODEX_RESET_ADMIN=1 codex
```
or, if using Docker:
```sh
docker run -e CODEX_RESET_ADMIN=1 -v host-parent-dir/config:/config ajslater/codex
```
### Private Libraries
In the Admin Panel you may configure private libraries that are only accessible
to specific groups.
A library with _no_ groups is accessible to every user including anonymous
users.
A library with _any_ groups is accessible only to users who are in those groups.
Use the Groups admin panel to create groups and the Users admin panel to add and
remove users to groups.
#### Include and Exclude Groups
Codex can make groups for libraries that exclude groups of users or exclude
everyone and include only certain groups of users.
### PDF Metadata
Codex reads PDF metadata from the filename, PDF metadata fields and also many
formats of common complex comic metadata if they are embedded in the PDF
`keywords` field.
If you decide to include PDFs in your comic library, I recommend taking time to
rename your files so Codex can find some metadata. Codex recognizes several file
naming schemes. This one has good results:
`{series} v{volume} #{issue} {title} ({year}) {ignored}.pdf`
Complex comic metadata, such as ComicInfo.xml, can be also be embedded in the
keywords field by using the [comicbox](https://github.com/ajslater/comicbox)
command line tool. Codex will read this data because it relies on comicbox
internally. Not many people use comicbox or embedded metadata in PDFs in this
fashion, so you likely won't find it unless you've added it yourself.
### 🗝️ API with Key Access
Codex has a limited number of API endpoints available with API Key Access. The
API Key is available on the admin/stats tab.
## 🎛️ Configuration
### Config Dir
The default config directory is `config/` directly under the working directory
you run codex from. You may specify an alternate config directory with the
environment variable `CODEX_CONFIG_DIR`.
The config directory contains a file named `codex.toml` where you can specify
ports and bind addresses. If no `codex.toml` is present Codex copies a default
one to that directory on startup. e.g.
```toml
[server]
host = "0.0.0.0"
port = 9810
url_path_prefix = ""
```
The config directory also holds the main sqlite database, a Django cache and
comic book cover thumbnails.
### Environment Variables
Environment variables override values set in the TOML config file.
#### General
- `TIMEZONE` or `TZ` will explicitly set the timezone in long format (e.g.
`"America/Los Angeles"`). This is useful inside Docker because codex cannot
automatically detect the host machine's timezone.
- `DEBUG_TRANSFORM` will show verbose information about how the comicbox library
reads all archive metadata sources and transforms it into a the comicbox
schema.
- `CODEX_CONFIG_DIR` will set the path to codex config directory. Defaults to
`$CWD/config`
##### Server
- `GRANIAN_HOST` the IP or hostname to serve Codex from. Defaults to "0.0.0.0",
all interfaces.
- `GRANIAN_PORT` the port to serve Codex from. Defaults to 9810.
- `GRANIAN_WORKERS` Number of worker processes. 1 recommended for containerized
environments.
- `GRANIAN_HTTP` HTTP protocol to use. "auto", "1" or "2". Defaults to "auto".
Generally you want to serve codex from behind nginx or traefik which will
handle the protocol, even HTTP 3, so this should stay on "auto".
- `GRANIAN_WEBSOCKETS` Enable websockets. Required for codex live updates.
Default true.
- `GRANIAN_URL_PATH_PREFIX` HTTP path prefix for codex (e.g. "/codex" for
reverse proxy sub-path). Defaults to "".
##### Repair
- `CODEX_RESET_ADMIN=1` will reset the admin user and its password to defaults
when codex starts.
- `CODEX_FIX_FOREIGN_KEYS=1` will check for and try to repair illegal foreign
keys on startup.
- `CODEX_INTEGRITY_CHECK=1` will perform database integrity check on startup.
- `CODEX_FTS_INTEGRITY_CHECK=1` will perform an integrity check on the full text
search index.
- `CODEX_FTS_REBUILD=1` will rebuild the full text search index.
#### Logging
- `LOGLEVEL` will change how verbose codex's logging is. Valid values are
`CRITICAL`, `ERROR`, `WARNING`, `SUCCESS`, `INFO`, `DEBUG`, and the overly
noisy `TRACE`. The default is `INFO`.
- `CODEX_LOG_DIR` sets a custom directory for saving logfiles. Defaults to
`$CODEX_CONFIG_DIR/logs`
- `CODEX_LOG_RETENTION` how long to keep logs. Defaults to "6 months".
- `CODEX_LOG_TO_FILE=0` will not log to files.
- `CODEX_LOG_TO_CONSOLE=0` will not log to the console.
##### Browser
- `CODEX_BROWSER_MAX_OBJ_PER_PAGE` the maximum number of objects per page.
Defaults to 100.
#### Throttling
Codex contains some experimental throttling controls. The value supplied to
these variables will be interpreted as the maximum number of allowed requests
per minute. For example, the following settings would limit each described group
to 2 queries per second.
- `CODEX_THROTTLE_ANON=30` Anonymous users
- `CODEX_THROTTLE_USER=30` Authenticated users
- `CODEX_THROTTLE_OPDS=30` The OPDS v1 & v2 APIs (Panels uses this for search)
- `CODEX_THROTTLE_OPENSEARCH=30` The OPDS v1 Opensearch API
#### Authentication
- `CODEX_AUTH_REMOTE_USER` will allow unauthenticated logins with the
Remote-User HTTP header. This can be very insecure if not configured properly.
Please read the Remote-User docs devoted to it below.
### Reverse Proxy
[nginx](https://nginx.org/) is often used as a TLS terminator and subpath proxy.
Here's an example nginx config with a subpath named '/codex'.
```nginx
# HTTP
proxy_set_header Host $http_host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Host $server_name;
proxy_set_header X-Forwarded-Port $server_port;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Scheme $scheme;
# Websockets
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade" location /codex {
proxy_pass http://codex:9810;
# Codex reads http basic authentication.
# If the nginx credentials are different than codex credentials use this line to
# not forward the authorization.
proxy_set_header Authorization "";
}
```
Specify a reverse proxy sub path (if you have one) in `config/codex.toml`
```toml
[server]
url_path_prefix = "/codex"
```
#### Nginx Reverse Proxy 502 when container refreshes
Nginx requires a special trick to refresh dns when linked Docker containers
recreate. See this
[nginx with dynamix upstreams](https://tenzer.dk/nginx-with-dynamic-upstreams/)
article.
#### Single Sign On and Third Party Authentication
##### OAuth & OIDC
Codex is not an OIDC client at this time. However the following Remote-User and
Token Authentication methods may assist other services in providing Single Sign
On.
##### Remote-User Authentication
Remote-User authentication tells Codex to accept a username from the webserver
and assume that authentication has already been done. This is very insecure if
you haven't configured an authenticating reverse proxy in front of Codex.
Here's a snipped for configuring nginx with tinyauth to provide this header.
This snipped it incomplete and assumes that the rest of nginx tinyauth config
has been done:
```nginx
auth_request_set $tinyauth_remote_user $upstream_http_remote_user;
proxy_set_header Remote-User $tiny_auth_user;
```
⚠️ Only turn on the `CODEX_AUTH_REMOTE_USER` environment variable if your
webserver sets the `Remote-User` header itself every time for the Codex
location, overriding any malicious client that might set it themselves. ⚠️
##### HTTP Token Authentication
You can also configure your proxy to add token authentication to the headers.
Codex will read “Bearer” prefixed authorization tokens. The token is unique for
each user and may be found in the Web UI sidebar. You must configure your proxy
or single sign on software to send this token.
```nginx
set user_token 'user-token-taken-from-web-ui';
proxy_set_header Authorization "Bearer $user_token";
```
### Restricted Memory Environments
Codex can run with as little as 1GB available RAM. Large batch jobs –like
importing and indexing tens of thousands of comics at once– will run faster the
more memory is available to Codex. The biggest gains in speed happen when you
increase memory up to about 6GB. Codex batch jobs do get faster the more memory
it has above 6GB, but with diminishing returns.
If you must run Codex in an admin restricted memory environment you might want
to temporarily give Codex a lot of memory to run a very large import job and
then restrict it for normal operation.
## 📖 Use
### 👤 Sessions & Accounts
Once your administrator has added some comic libraries, you may browse and read
comics. Codex will remember your preferences, bookmarks and progress in the
browser session. Codex destroys anonymous sessions and bookmarks after 60 days.
To preserve these settings across browsers and after sessions expire, you may
register an account with a username and password. You will have to contact your
administrator to reset your password if you forget it.
### ᯤ OPDS
Codex supports OPDS syndication and OPDS streaming. You may find the OPDS url in
the side drawer. It should take the form:
`http(s)://host.tld(:9810)(/path_prefix)/opds/v1.2/`
or
`http(s)://host.tld(:9810)(/path_prefix)/opds/v2.0/`
#### OPDS v1 Clients
- iOS
- [Panels](https://panels.app/)
- [PocketBooks](https://pocketbook.ch/)
- [KYBook 3](http://kybook-reader.com/)
- [Chunky Comic Reader](https://apps.apple.com/us/app/chunky-comic-reader/id663567628)
- Android
- [Moon+](https://play.google.com/store/apps/details?id=com.flyersoft.moonreader)
- [Librera](https://play.google.com/store/apps/details?id=com.foobnix.pdf.reader)
Kybook 3 does not seem to support http basic authentication, so Codex users are
not supported.
#### OPDS v2 Clients
OPDS 2.0 is a newer protocol that is only just starting to be supported by new
clients.
- [Stump (Alpha Test)](https://www.stumpapp.dev/guides/mobile/app)
- [Readest](https://readest.com/) (No page streaming yet, download only)
#### OPDS Authentication
##### OPDS Login
The few clients that implement the OPDS 1.0 Authentication spec present the user
with a login screen for interactive authentication.
##### HTTP Basic
Some OPDS clients allow configuring HTTP Basic authentication in their OPDS
server settings. If the don't, you will have to add your username and password
to the URL. In that case the OPDS url will look like:
`http(s)://username:password@codex-server.tld(:9810)(/path_prefix)/opds/v1.2/`
##### HTTP Token
Some clients allow adding a unique login token to the HTTP headers. Codex will
read "Bearer" prefixed authorization tokens. The token is unique for each user
and may be found in the Web UI sidebar.
#### Supported OPDS Specifications
##### OPDS v1
- [OPDS 1.2](https://specs.opds.io/opds-1.2.html)
- [OPDS-PSE 1.2](https://github.com/anansi-project/opds-pse/blob/master/v1.2.md)
- [OPDS Authentication 1.0](https://drafts.opds.io/authentication-for-opds-1.0.html)
##### OPDS v2
- [OPDS 2.0 (draft)](https://drafts.opds.io/opds-2.0.html)
- [OPDS 2.0 Digital Visual Narratives Profile (DiViNa)](https://github.com/readium/webpub-manifest/blob/master/profiles/divina.md)
- [OPDS 2.0 Authentication (proposal)](https://github.com/opds-community/drafts/discussions/43)
- [OPDS 2.0 Progression (proposal)](https://github.com/opds-community/drafts/discussions/67)
##### OpenSearch v1
- [OpenSearch 1.1 (draft)](https://github.com/dewitt/opensearch)
## [🩺 Troubleshooting](#troubleshooting)
### 📒 Logs
Codex collects its logs in the `config/logs` directory. Take a look to see what
th e server is doing.
You can change how much codex logs by setting the `LOGLEVEL` environment
variable. By default this level is `INFO`. To see more verbose messages, run
codex like:
```sh
LOGLEVEL=DEBUG codex
```
### Watching Filesystem Events with Docker
Codex tries to watch for filesystem events to instantly update your comic
libraries when they change on disk. But these native filesystem events are not
translated between macOS & Windows Docker hosts and the Docker Linux container.
If you find that your installation is not updating to filesystem changes
instantly, you might try enabling polling for the affected libraries and
decreasing the `poll_every` value in the Admin console to a frequency that suits
you.
### Emergency Database Repair
If the database becomes corrupt, Codex includes a facility to rebuild the
database. Place a file named `rebuild_db` in your Codex config directory like
so:
```sh
touch config/rebuild_db
```
Shut down and restart Codex.
The next time Codex starts it will back up the existing database and try to
rebuild it. The database lives in the config directory as the file
`config/db.sqlite3`. If this procedure goes kablooey, you may recover the
original database at `config/backups/codex.sqlite3.before-rebuild`. Codex will
remove the `rebuild_db` file.
### Warnings to Ignore
#### StreamingHttpResponse Iterator Warning
```pycon
packages/django/http/response.py:517: Warning: StreamingHttpResponse must consume synchronous iterators in order to serve them asynchronously. Use an asynchronous iterator instead.
```
This is a known warning and does not represent anything bad happening. It's an
artifact of the Django framework slowly supporting asynchronous server endpoints
and unfortunately isn't practical to remove yet.
## 📚Alternatives to Codex
- [Kavita](https://www.kavitareader.com/) has light metadata filtering/editing,
supports comics, eBooks, and features for manga.
- [Komga](https://komga.org/) has light metadata editing and duplicate page
elimination.
- [Ubooquity](https://vaemendis.net/ubooquity/) reads both comics and eBooks.
## 🔧 Popular comic utilities
- [Mylar](https://github.com/mylar3/mylar3) is the best comic book manager which
also has a built in reader.
- [Comictagger](https://github.com/comictagger/comictagger) is a comic metadata
editor. It comes with a command line and desktop GUI. It will tag identified
comics from online database sources.
- [Metron Tagger](https://github.com/Metron-Project/metron-tagger) is a command
line comic metadata editor. It will tag identified comics from online database
sources.
- [Comicbox](https://github.com/ajslater/comicbox) is a powerful command line
comic metadata editor and multi metadata format synthesizer. It is what Codex
uses under the hood to read comic metadata.
## 🤝 Contributing
### 🐛 Bug Reports
Issues and feature requests are best filed on the
[Github issue tracker](https://github.com/ajslater/codex/issues).
## 💬 Support
I and other Codex users answer questions on the
[Codex Comic Server Discord](https://discord.gg/CU5kKxv7kg)
### 🛠 Develop
Codex's git repo is mirrored on [Github](https://github.com/ajslater/codex/)
Codex is a Django Python webserver with a VueJS front end.
`/codex/codex/` is the main django app which provides the webserver and
database.
`/codex/frontend/` is where the vuejs frontend lives.
Most of Codex development is now controlled through the Makefile. Type `make`
for a list of commands.
## 🔗 Links
- [Docker Image](https://github.com/ajslater/codex/pkgs/container/codex)
- [PyPi Package](https://pypi.org/project/codex/)
- [GitHub Project](https://github.com/ajslater/codex/)
## 🙏🏻 Thanks
- Thanks to [Aurélien Mazurie](https://pypi.org/user/ajmazurie/) for allowing me
to use the PyPi name 'codex'.
- To [ProfessionalTart](https://github.com/professionaltart) for providing
native Windows installation instructions.
- Thanks to the good people of
[#mylar](https://github.com/mylar3/mylar3#live-support--conversation) for
continuous feedback and comic ecosystem education.
## 😊 Enjoy

================================================
FILE: bin/benchmark-opds.sh
================================================
#!/usr/bin/env bash
# benchmark opds url times
set -euo pipefail
BASE_URL="http://localhost:9810"
OPDS_BASE="/opds/v1.2"
timeit() {
echo "${1}":
TEST_PATH="${OPDS_BASE}${2}"
echo -e "\t$TEST_PATH"
URL="${BASE_URL}${TEST_PATH}"
/usr/bin/time -h curl -S -s -o /dev/null "$URL"
}
timeit "Recently Added:" "/s/0/1?orderBy=created_at&orderReverse=True"
#timeit "All Series" "/r/0/1?topGroup=s"
================================================
FILE: bin/build-choices.sh
================================================
#!/usr/bin/env bash
# Build json choices for frontend using special script.
set -euo pipefail
THIS_DIR="$(dirname "$0")/.."
cd "$THIS_DIR" || exit 1
export PYTHONPATH="${PYTHONPATH:-}:$THIS_DIR"
CHOICES_DIR=frontend/src/choices
# rm -rf "${CHOICES_DIR:?}"/* # breaks vite build
export UV_NO_DEV=1
uv run codex/choices/choices_to_json.py "$CHOICES_DIR"
================================================
FILE: bin/build-dist.sh
================================================
#!/usr/bin/env bash
# Build script for producing a codex python package
set -euxo pipefail
cd "$(dirname "$0")"
export BUILD=1
make collectstatic
./bin/pm check
echo "*** build and package application ***"
PIP_CACHE_DIR=$(pip3 cache dir)
export PIP_CACHE_DIR
uv build
================================================
FILE: bin/ci-download-dist-if-identical.sh
================================================
#!/usr/bin/env bash
# Download last dist artifacts if current code is identical to the merge source.
set -euo pipefail
PR_DATA=$(gh pr list --state merged --limit 1 --json headRefName,headRefOid \
--template '{{range .}}{{.headRefName}},{{.headRefOid}}{{end}}')
if [[ -z "$PR_DATA" ]]; then
echo "No merge detected. Continue with Lint, Test, & Build."
exit 0
fi
SOURCE_BRANCH="${PR_DATA%,*}"
SOURCE_SHA="${PR_DATA#*,}"
echo "A merge just happened from $SOURCE_BRANCH."
git fetch origin "$SOURCE_BRANCH" --depth=1
if ! git diff --quiet HEAD "origin/$SOURCE_BRANCH"; then
echo "Code differs from $SOURCE_BRANCH. Continue with Lint, Test, & Build."
exit 0
fi
echo "Code is identical to $SOURCE_BRANCH"
RUN_ID=$(gh api "repos/${GH_REPO}/actions/runs?head_sha=$SOURCE_SHA&status=success" \
--jq '[.workflow_runs[] | select(.name=="CI")] | .[0].id')
if [[ -z "$RUN_ID" || "$RUN_ID" == "null" ]]; then
echo "No successful CI run found for commit $SOURCE_SHA"
exit 0
fi
echo "Found CI run $RUN_ID for commit $SOURCE_SHA"
if gh run download "$RUN_ID" --name python-dist --dir dist; then
echo "dist_found=true" >> "$GITHUB_OUTPUT"
echo "Successfully retrieved dist from run $RUN_ID"
else
echo "Failed to download python-dist artifact from run $RUN_ID"
fi
================================================
FILE: bin/clean-pycache.sh
================================================
#!/usr/bin/env bash
# remove all pycache dirs
find . -name "__pycache__" -print0 | xargs -0 rm -rf
================================================
FILE: bin/collectstatic.sh
================================================
#!/usr/bin/env bash
# Run the django collectstatic command to collect static files from all
# locations specified in settings.STATIC_DIRS and place them in
# settings.STATIC_ROOT for production builds.
set -euo pipefail
BUILD=1 ./bin/pm collectstatic --clear --no-input --ignore "rest_framework"
================================================
FILE: bin/create-output-dirs.sh
================================================
#!/usr/bin/env bash
# create output directories with correct perms for ci builder docker mounts
# circleci only
set -euo pipefail
mkdir -p -m 777 test-results dist
chown -R circleci:circleci test-results dist
================================================
FILE: bin/delete-files.sh
================================================
#!/usr/bin/env bash
# Delete all files listed in the delete.txt file
set -euo pipefail
DEVENV=$1
DELETE_FILE=$DEVENV/delete.txt
existing_files=()
while IFS= read -r file || [[ -n "$file" ]]; do
[[ -z "$file" || "$file" == \#* ]] && continue
[[ -f "$file" ]] && existing_files+=("$file")
done < "$DELETE_FILE"
echo "Deleting ${#existing_files[@]} files..."
rm -f -- "${existing_files[@]}"
================================================
FILE: bin/dev-docker.sh
================================================
#!/usr/bin/env bash
# Recreate the codex-dev container and enter it with a shell
set -euo pipefail
docker rm -f codex-dev || true
docker compose down
docker compose up codex-dev -d
================================================
FILE: bin/dev-module.sh
================================================
#!/usr/bin/env bash
# Run a main method in an arbitrary module
set -euxo pipefail
THIS_DIR="$(dirname "$0")"
cd "$THIS_DIR" || exit 1
export PYTHONPATH="${PYTHONPATH:-}:$THIS_DIR"
export DEBUG="${DEBUG:-1}"
export PYTHONDEVMODE="$DEBUG"
export PYTHONDONTWRITEBYTECODE=1 #"$DEBUG"
uv run python3 "$@"
================================================
FILE: bin/dev-prod-server.sh
================================================
#!/usr/bin/env bash
# run a production-like server
export PYTHONPATH="$PYTHONPATH:$THIS_DIR"
uv run python3 ./codex/run.py
================================================
FILE: bin/dev-reverse-proxy.sh
================================================
#!/usr/bin/env bash
# Run an nginx reverse proxy with a subpath for development testing
set -euo pipefail
cd "$(dirname "$0")/nginx" || exit 1
docker-compose -f nginx.yaml up
================================================
FILE: bin/dev-server.sh
================================================
#!/usr/bin/env bash
# Run the codex server
set -euxo pipefail
THIS_DIR="$(dirname "$0")/.."
cd "$THIS_DIR" || exit 1
export DEBUG="${DEBUG:-1}"
export PYTHONDEBUG=1
export PYTHONDEVMODE="$DEBUG"
export PYTHONDONTWRITEBYTECODE=1
export PYTHONPATH="${PYTHONPATH:-}:$THIS_DIR"
export PYTHONWARNINGS=always
#export CODEX_THROTTLE_OPDS=10
#export CODEX_THROTTLE_USER=10
export DJANGO_SETTINGS_MODULE=codex.settings
#uv run python3 -X tracemalloc ./codex/run.py
#uv run righttyper --all-files --overwrite codex/run.py
uv run python3 ./codex/run.py
================================================
FILE: bin/dev-ttabs.sh
================================================
#!/usr/bin/env bash
# Open development server processes in macOS terminal tabs
# Requires npm ttab
set -euo pipefail
# The Vue dev server
ttab -t "Codex Vue" "make dev-frontend-server"
# The API server
make dev-server
================================================
FILE: bin/docker-compose-exit.sh
================================================
#!/usr/bin/env bash
# Run a docker compose service and return its exit code
set -euo pipefail
SERVICE=$1
# docker compose without the dash doesn't have the exit-code-from param
docker compose up --exit-code-from "$SERVICE" "$SERVICE"
================================================
FILE: bin/docker-tag-latest.sh
================================================
#!/usr/bin/env bash
# Tag old version as latest
set -euo pipefail
if [[ "$#" -lt 3 ]]; then
echo "Usage: $0 <registry> <image_name> <version_tag>"
echo "Example: $0 ghcr.io ajslater/codex 1.10.3"
exit 1
fi
# Configuration
REGISTRY=$1
IMAGE_NAME=$2
SOURCE_TAG=$3
TARGET_TAG="latest"
# Ensure DOCKER_PASS and DOCKER_USER are set in your environment
if [[ -z "$DOCKER_PASS" || -z "$DOCKER_USER" ]]; then
echo "Error: DOCKER_PASS and DOCKER_USER environment variables must be set."
exit 1
fi
# 1. Log in to registry
echo "Logging in to $REGISTRY..."
echo "$DOCKER_PASS" | docker login "$REGISTRY" -u "$DOCKER_USER" --password-stdin
# 2. Retag the multi-arch image
# This creates a new manifest on the registry side without downloading image layers
echo "Tagging $IMAGE_NAME:$SOURCE_TAG as $TARGET_TAG..."
docker buildx imagetools create \
--tag "$REGISTRY/$IMAGE_NAME:$TARGET_TAG" \
"$REGISTRY/$IMAGE_NAME:$SOURCE_TAG"
# shellcheck disable=SC2181
if [ $? -eq 0 ]; then
echo "Successfully updated $TARGET_TAG"
else
echo "Failed to update tag."
exit 1
fi
================================================
FILE: bin/fix-docker.sh
================================================
#!/usr/bin/env bash
# Fix common linting errors with docker
set -euxo pipefail
#######################
###### Dockerfile #####
#######################
mapfile -t dockerfiles < <(find . -type f -name '*Dockerfile' -print -quit)
if [ ${#dockerfiles[@]} -gt 0 ]; then
dockerfmt --write "${dockerfiles[@]}"
fi
================================================
FILE: bin/fix-python.sh
================================================
#!/usr/bin/env bash
# Fix common linting errors
set -euxo pipefail
# Python
uv run --group lint ruff check --fix .
uv run --group lint ruff format .
================================================
FILE: bin/fix.sh
================================================
#!/usr/bin/env bash
# Fix common linting errors
set -euxo pipefail
#####################
###### Makefile #####
#####################
uv run mbake format Makefile cfg/*.mk
################
# Ignore files #
################
bin/sort-ignore.sh
############################################
##### Javascript, JSON, Markdown, YAML #####
############################################
bun run fix
###################
###### Shell ######
###################
shellharden --replace ./**/*.sh
================================================
FILE: bin/icons_transform.py
================================================
#!/usr/bin/env python
"""Generate production icons from svg sources."""
import shutil
import subprocess
from pathlib import Path
from types import MappingProxyType
from cairosvg import svg2png
TOP_PATH = Path(__file__).parent.parent
SRC_IMG_PATH = TOP_PATH / Path("img")
STATIC_IMG_PATH = TOP_PATH / Path("codex/static_src/img")
INKSCAPE_PATH = Path("/Applications/Inkscape.app/Contents/MacOS/inkscape")
_COVER_RATIO = 1.5372233400402415
ICONS = MappingProxyType(
{
"logo": (32, 32),
"logo-maskable": (180, 180),
"missing-cover": (165, round(165 * _COVER_RATIO)),
"publisher": (),
"imprint": (),
"series": (),
"volume": (),
"folder": (),
"story-arc": (),
}
)
def create_maskable_icon(input_path):
"""Create logo-maskable.svg from logo.svg by editing the XML."""
with input_path.open("r") as f:
lines = f.readlines()
modified_lines = []
for line in lines:
modified_lines.append(line)
if 'inkscape:label="logo"' in line:
modified_lines.append(' transform="matrix(0.80,0,0,0.80,51.5,51.5)"')
output_path = SRC_IMG_PATH / "logo-maskable.svg"
with output_path.open("w") as f:
f.writelines(modified_lines)
def inkscape(input_path, export_path, width, height):
"""Transform svgs with xlinks into pngs."""
# Needed because cairosvg doesn't support xlinks
# https://github.com/Kozea/CairoSVG/issues/163
args = (
INKSCAPE_PATH,
f"--export-width={width}",
f"--export-height={height}",
f"--export-filename={export_path}",
input_path,
)
subprocess.run(args, check=False) # noqa: S603
def transform_icon(name, size):
"""Transform svgs into optimized svgs and pngs."""
svg_name = name + ".svg"
input_svg_path = SRC_IMG_PATH / svg_name
output_svg_path = STATIC_IMG_PATH / svg_name
input_svg_mtime = input_svg_path.stat().st_mtime
do_gen_svg = (
not output_svg_path.exists()
or output_svg_path.stat().st_mtime < input_svg_mtime
)
if do_gen_svg:
if name == "logo":
create_maskable_icon(input_svg_path)
(SRC_IMG_PATH / "missing-cover.svg").touch()
shutil.copy(input_svg_path, output_svg_path)
if not size:
return
width, height = size
output_png_name = f"{name}-{width}"
output_png_path = STATIC_IMG_PATH / (output_png_name + ".png")
output_webp_path = STATIC_IMG_PATH / (output_png_name + ".webp")
do_gen_png = (
not output_webp_path.exists()
or output_webp_path.stat().st_mtime < input_svg_mtime
)
if do_gen_png:
if name == "missing-cover":
inkscape(input_svg_path, output_png_path, width, height)
else:
svg2png(
url=str(input_svg_path),
write_to=str(output_png_path),
output_width=width,
output_height=height,
)
def picopt():
"""Optimize output with picopt."""
args = ("picopt", "-rtx", "SVG", "-c", "WEBP", STATIC_IMG_PATH)
subprocess.run(args, check=False) # noqa: S603
def main():
"""Create all icons."""
for name, size in ICONS.items():
transform_icon(name, size)
picopt()
if __name__ == "__main__":
main()
================================================
FILE: bin/kill-codex.sh
================================================
#!/usr/bin/env bash
# kill all codex processes
set -euo pipefail
pkill -9 -f 'codex/run.py'
================================================
FILE: bin/kill-eslint_d.sh
================================================
#!/usr/bin/env bash
# eslint_d can get into a bad state if git switches branches underneath it
bunx eslint_d stop
pkill eslint_d
rm -f .eslintcache
================================================
FILE: bin/lint-ci.sh
================================================
#!/usr/bin/env bash
# Lint checks for ci
set -euxo pipefail
if [ "$(uname)" != "Darwin" ]; then
exit 0
fi
if [ -f .github/workflows/ci.yml ]; then
actionlint .github/workflows/ci.yml
fi
if [ -f .circleci/config.yml ]; then
circleci config validate .circleci/config.yml
fi
================================================
FILE: bin/lint-complexity.sh
================================================
#!/usr/bin/env bash
# Lint complexity
set -euo pipefail
if [ "$(uname)" != "Darwin" ]; then
exit 0
fi
uv run --group lint complexipy
uv run --group lint radon mi --min B .
uv run --group lint radon cc --min C .
================================================
FILE: bin/lint-darwin.sh
================================================
#!/usr/bin/env bash
# Lint checks
set -euxo pipefail
if [ "$(uname)" != "Darwin" ]; then
exit 0
fi
shellharden --check ./**/*.sh
# subdirs aren't copied into docker builder
# .env files aren't copied into docker
shellcheck --external-sources ./**/*.sh
================================================
FILE: bin/lint-docker.sh
================================================
#!/usr/bin/env bash
# Lint checks for docker
set -euxo pipefail
if [ "$(uname)" != "Darwin" ]; then
exit 0
fi
mapfile -t dockerfiles < <(find . -type f -name '*Dockerfile' -print -quit)
if [ ${#dockerfiles[@]} -gt 0 ]; then
hadolint "${dockerfiles[@]}"
dockerfmt --check "${dockerfiles[@]}"
fi
================================================
FILE: bin/lint-python.sh
================================================
#!/usr/bin/env bash
# Lint checks
set -euxo pipefail
####################
###### Python ######
####################
uv run --group lint ruff check .
uv run --group lint ruff format --check .
make typecheck
uv run --group lint vulture .
bin/lint-complexity.sh
uv run --group lint codespell .
================================================
FILE: bin/lint.sh
================================================
#!/usr/bin/env bash
# Lint checks
set -euxo pipefail
uv run mbake validate Makefile cfg/*.mk
# Javascript, JSON, Markdown, YAML #####
bun run lint
bin/lint-darwin.sh
uv run bin/roman.py -i .prettierignore .
================================================
FILE: bin/localize-db.sh
================================================
#!/usr/bin/env bash
# copy old database a localize
set -euo pipefail
REMOTE_DB=$1
LOCAL_LIB_PATH=$2
ROOT_PATH=$(realpath "$(dirname "$0")/..")
DB_PATH=$ROOT_PATH/config/codex.sqlite3
SQL_PATH=$(dirname "$0")/localize_library.sql
rm -f "$DB_PATH"-*
cp "$REMOTE_DB" "$DB_PATH"
SQL_DECLARE="DECLARE @LOCAL_LIB_PATH VARCHAR; SET @LOCAL_LIB_PATH = '$LOCAL_LIB_PATH';"
echo "$SQL_DECLARE" | cat - <("$SQL_PATH") | sqlite "$DB_PATH"
================================================
FILE: bin/localize_library.sql
================================================
UPDATE codex_library
SET
path = REPLACE(path, '/comics', @LOCAL_LIB_PATH)
WHERE
path LIKE '/comics%';
UPDATE codex_failedimport
SET
path = REPLACE(path, '/comics', @LOCAL_LIB_PATH)
WHERE
path LIKE '/comics%';
UPDATE codex_comic
SET
path = REPLACE(path, '/comics', @LOCAL_LIB_PATH)
WHERE
path LIKE '/comics%';
================================================
FILE: bin/manage.py
================================================
#!/usr/bin/env python
"""Django's command-line utility for administrative tasks."""
import os
import sys
def main():
"""Run the server."""
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "codex.settings")
try:
from django.core.management import execute_from_command_line
except ImportError as exc:
reason = (
"Couldn't import Django. Are you sure it's installed and "
"available on your PYTHONPATH environment variable? Did you "
"forget to activate a virtual environment?"
)
raise ImportError(reason) from exc
execute_from_command_line(sys.argv)
if __name__ == "__main__":
main()
================================================
FILE: bin/pm
================================================
#!/usr/bin/env bash
# Convenience script for running django manage tasks with uv
set -euo pipefail
export PYTHONPATH=.
uv run python3 bin/manage.py "$@"
================================================
FILE: bin/prettier-nginx.sh
================================================
#!/usr/bin/env bash
# Run prettier on nginx files because overrides doesn't work yet.
set -euxo pipefail
CONFIG_DIR=nginx/http.d
if [ -d "$CONFIG_DIR" ]; then
prettier --parser nginx "$CONFIG_DIR/*.conf" "$@"
fi
================================================
FILE: bin/roman.py
================================================
#!/usr/bin/env python3
"""
Check shell scripts recursively for a descriptive comment on line 2.
Detects shell scripts by shebang.
Inspired by @defunctzombie
"""
from __future__ import annotations
import re
import sys
from argparse import ArgumentParser, Namespace, RawDescriptionHelpFormatter
from pathlib import Path
from typing import TYPE_CHECKING
from pathspec import PathSpec
if TYPE_CHECKING:
from collections.abc import Generator, Sequence
# ---------------------------------------------------------------------------
# Constants
# ---------------------------------------------------------------------------
SHELL_SHEBANG_PATTERN: re.Pattern[str] = re.compile(r"^#!.*sh")
# Patterns that are always excluded regardless of an ignore file.
DEFAULT_EXCLUDE_PATTERNS: list[str] = [
".*", # hidden files / directories
"*~", # editor backup files
]
COMMENT_PATTERN: re.Pattern[str] = re.compile(r"^#+.{4}")
# Number of bytes to read when sniffing the shebang — avoids loading huge
# binary files into memory.
SHEBANG_READ_BYTES: int = 512
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
def build_ignore_spec(ignore_path: Path | None) -> PathSpec:
"""Return a gitignore style PathSpec built from *ignore_path* plus the built-in defaults."""
lines: list[str] = list(DEFAULT_EXCLUDE_PATTERNS)
if ignore_path is not None:
lines += ignore_path.read_text(encoding="utf-8").splitlines()
return PathSpec.from_lines("gitwildmatch", lines)
def read_first_two_lines(path: Path) -> tuple[str, str]:
"""Return the first two lines of *path* as a (line1, line2) tuple."""
try:
raw = path.read_bytes()[:SHEBANG_READ_BYTES]
text = raw.decode("utf-8", errors="replace")
except OSError:
return "", ""
lines = text.splitlines()
line1 = lines[0] if len(lines) > 0 else ""
line2 = lines[1] if len(lines) > 1 else ""
return line1, line2
def is_shell_script(line1: str) -> bool:
"""Return True when *line1* looks like a shell shebang."""
return bool(SHELL_SHEBANG_PATTERN.search(line1))
def has_description_comment(line2: str) -> bool:
"""Return True when *line2* starts with a ``# `` comment."""
return bool(COMMENT_PATTERN.match(line2))
def iter_files(path_strs: Sequence[str], spec: PathSpec) -> Generator[Path]:
"""
Yield every file under *roots* that is not excluded by *spec*.
Each candidate path is tested relative to the root it was found under so
that gitignore-style directory patterns (e.g. ``vendor/``) work correctly.
"""
for path_str in path_strs:
path = Path(path_str)
if not path.exists():
print(f"👎 Path does not exist: {path}", file=sys.stderr) # noqa: T201
sys.exit(2)
root = Path(path).resolve()
if root.is_file():
rel = Path(root.name)
if not spec.match_file(str(rel)):
yield root
continue
for sub_path in sorted(root.rglob("*")):
if not sub_path.is_file():
continue
try:
rel = sub_path.relative_to(root)
except ValueError:
rel = sub_path
# Match against each component so directory patterns work
if spec.match_file(str(rel)):
continue
yield sub_path
# ---------------------------------------------------------------------------
# CLI
# ---------------------------------------------------------------------------
def build_parser() -> ArgumentParser:
"""Build cli arg parser."""
parser = ArgumentParser(
description="Find shell scripts that are missing a descriptive comment on line 2.",
formatter_class=RawDescriptionHelpFormatter,
epilog=(
"Exit status: 0 if all shell scripts pass, 1 if any are missing\n"
"a comment on line 2, 2 on usage / IO errors."
),
)
parser.add_argument(
"paths",
nargs="+",
metavar="PATH",
help="Files or directories to examine.",
)
parser.add_argument(
"-i",
"--ignore-file",
metavar="FILE",
help="Ignore-file with gitignore-style patterns (e.g. .zombieignore).",
)
return parser
def _parse_ignore_file(args: Namespace) -> PathSpec:
ignore_path: Path | None = None
if args.ignore_file:
ignore_path = Path(args.ignore_file)
if not ignore_path.is_file():
print(f"👎 Can't read ignore file: {ignore_path}", file=sys.stderr) # noqa: T201
sys.exit(2)
try:
return build_ignore_spec(ignore_path)
except OSError as exc:
print(f"👎 Failed to parse ignore file: {exc}", file=sys.stderr) # noqa: T201
sys.exit(2)
def main() -> None:
"""Run program."""
parser = build_parser()
args = parser.parse_args()
spec = _parse_ignore_file(args)
offenders: list[Path] = []
for path in iter_files(args.paths, spec):
line1, line2 = read_first_two_lines(path)
if not is_shell_script(line1):
continue
if not has_description_comment(line2):
print(f"🔪 {path}") # noqa: T201
offenders.append(path)
if offenders:
print( # noqa: T201
f"\n{len(offenders)} script(s) missing a description comment.",
file=sys.stderr,
)
sys.exit(1)
print("👍") # noqa: T201
if __name__ == "__main__":
main()
================================================
FILE: bin/sort-ignore.sh
================================================
#!/usr/bin/env bash
# Sort all ignore files in place and remove duplicates
# Set locale to make output deterministic across shells
export LC_ALL=en_US.UTF-8
for f in .*ignore; do
if [ ! -L "$f" ]; then
sort --mmap --unique --output="$f" "$f"
echo "$f" sorted
fi
done
================================================
FILE: bin/test-python.sh
================================================
#!/usr/bin/env bash
# Run all tests
set -euxo pipefail
mkdir -p test-results
# LOGLEVEL=DEBUG uv run --group test righttyper --all-files --overwrite --output-files --python-version 3.10 -m pytest "$@"
LOGLEVEL=DEBUG uv run --group test pytest "$@"
# pytest-cov leaves .coverage.$HOST.$PID.$RAND files around while coverage itself doesn't
uv run --group test coverage erase || true
================================================
FILE: bin/uml.sh
================================================
#!/usr/bin/env bash
# Create UML diagram
set -euo pipefail
PACKAGE=$(uv run toml get --toml-path=pyproject.toml project.name)
uvx --from pylint pyreverse -o png "$PACKAGE"
================================================
FILE: bin/update-deps-node.sh
================================================
#!/usr/bin/env bash
# Update bun dependencies
set -euo pipefail
bun update
bun outdated || true
================================================
FILE: bin/update-deps-python.sh
================================================
#!/usr/bin/env bash
# Update python dependencies
set -euo pipefail
uv sync --no-install-project --all-extras --all-groups --all-packages --upgrade
uv tree --all-groups --depth 1 --upgrade --outdated | grep --color=always "(latest:.*)" || true
================================================
FILE: bin/vendor-diff-package.sh
================================================
#!/usr/bin/env bash
# Find the diffs for two vendored packages.
# vendor the original package into codex/_vendor_orig before comparing edits
# Would be slicker if this automated the creation and destruction of _vendor-orig in /tmp
set -euo pipefail
PKG=$1
MODULE=$2
VENDOR_TARGET=/tmp/_vendor_orig
rm -rf "$VENDOR_TARGET"
mkdir -p "$VENDOR_TARGET"
cd cache
# vendorize the original in a tmp dir
cat << EOT > vendorize.toml
target = "$VENDOR_TARGET"
packages = [ "$PKG" ]
EOT
uv run python-vendorize
# compare
DIFF_FN="../codex/_vendor/$PKG.diff"
echo "# Non automated/import patches to $PKG" > "$DIFF_FN"
diff --minimal --recursive --suppress-common-lines \
-x "*~" \
-x "*.pyc" \
-x "*__pycache__*" \
"$VENDOR_TARGET/$MODULE" \
"../codex/_vendor/$MODULE" \
| rg -v "Binary|Only" >> "$DIFF_FN"
# cleanup
rm -rf "$VENDOR_TARGET"
rm -f vendorize.toml
================================================
FILE: bin/vendor-patch-imports.sh
================================================
#!/usr/bin/env bash
# Replace relative imports with direct vendor imports
set -euo pipefail
MODULE=$1
MODULE_DIR="codex/_vendor/$MODULE"
find "$MODULE_DIR" -name "*.py" -print0 | xargs -0 sed -ri '' "s/from \.+$MODULE/from codex._vendor.$MODULE/g"
================================================
FILE: bin/version-node.sh
================================================
#!/usr/bin/env bash
# Get version or set version in Frontend & API.
set -euo pipefail
VERSION="${1:-}"
if [ "$VERSION" = "" ]; then
if [ -d frontend ]; then
cd frontend
node -e "const {name, version} = require('./package.json'); console.log(name, version);"
fi
else
if [ -d frontend ]; then
cd frontend
bunx npm version --allow-same-version "$VERSION"
fi
fi
================================================
FILE: bin/version-python.sh
================================================
#!/usr/bin/env bash
# Get version or set version for python.
set -euo pipefail
VERSION="${1:-}"
if [ "$VERSION" = "" ]; then
uv version
else
uv version "$VERSION"
fi
================================================
FILE: cfg/ci.mk
================================================
DEVENV_CI := 1
export DEVENV_CI
.PHONY: lint
## Lint ci errors
## @category Lint
lint::
bin/lint-ci.sh
================================================
FILE: cfg/codex.mk
================================================
.PHONY: install
## Configure wheel building for Darwin
## @category Install
install::
BREW_PREFIX=$(brew --prefix)
export LDFLAGS="-L${BREW_PREFIX}/opt/openssl@3/lib"
export CPPFLAGS="-I${BREW_PREFIX}/opt/openssl@3/include"
export PKG_CONFIG_PATH="${BREW_PREFIX}/opt/openssl@3/lib/pkgconfig"
.PHONY: test-frontend
## Run frontend test with dependencies
## @category Test
test-frontend:: build-choices
.PHONY: dev-ttabs
## Run the vite dev frontend and dev-server in ttabs
## @category Run Server
dev-ttabs:
./bin/dev-ttabs.sh
.PHONY: dev-reverse-proxy
## Run an nginx reverse proxy to codex in docker
## @category Run Server
dev-reverse-proxy:
./bin/dev-reverse-proxy.sh
## Module to run
## @category Run Server
M :=
.PHONY: dev-module
## Run a single codex module in dev mode
## @category Run Server
dev-module:
./bin/dev-module.sh $(M)
.PHONY: build-choices
## Build JSON choices for frontend
## @category Build
build-choices:
./bin/build-choices.sh
.PHONY: build-icons
## Build all icons from source
## @category Build
build-icons:
uv run --group build bin/icons_transform.py
.PHONY: build
## Build codex dependencies
## @category Build
build:: build-choices build-icons
================================================
FILE: cfg/common.mk
================================================
SHELL := /usr/bin/env bash
DEVENV_SRC ?= ../devenv
# export DEVENV_SRC
DEVENV_COMMON := 1
export DEVENV_COMMON
.PHONY: clean
## Clean caches
## @category Clean
clean::
rm -rf .*cache
.PHONY: update-devenv
## Update development environment
## @category Update
update-devenv:
$(DEVENV_SRC)/scripts/update_devenv.py
.PHONY: fix
## Fix lint errors
## @category Fix
fix::
./bin/fix.sh
.PHONY: lint
## Lint
## @category Lint
lint::
./bin/lint.sh
.PHONY: news
## Show recent NEWS
## @category Deploy
news:
head -40 NEWS.md
================================================
FILE: cfg/django.mk
================================================
DEVENV_DJANGO := 1
export DEVENV_DJANGO
.PHONY: fix
## Fix django lint errors in templates
## @category Fix
fix::
uv run --group lint djlint --reformat **/templates/**/*.html
.PHONY: lint
## Lint django templates
## @category Lint
lint::
uv run --group lint djlint --lint **/templates/**/*.html
.PHONY: django-check
## Django check
## @category Test
django-check:
bin/pm check
.PHONY: dev-server
## Run the dev webserver
## @category Serve
dev-server:
./bin/dev-server.sh
.PHONY: dev-prod-server
## Run a bundled production webserver
## @category Run Server
dev-prod-server: build-frontend collectstatic
./bin/dev-prod-server.sh
.PHONY: collectstatic
## Collect static files for django
## @category Build
collectstatic: build-icons build-frontend
bin/collectstatic.sh
.PHONY: build-only
## Build python package
## @category Build
build-only:
uv build
.PHONY: build
## Build python package
## @category Build
build:: collectstatic build-only
================================================
FILE: cfg/docker.mk
================================================
DEVENV_DOCKER := 1
export DEVENV_DOCKER
.PHONY: fix
## Fix docker lint errors
## @category Fix
fix::
bin/fix-docker.sh
.PHONY: lint
## Lint docker files
## @category Lint
lint::
bin/lint-docker.sh
================================================
FILE: cfg/docs.mk
================================================
DEVENV_DOCS := 1
export DEVENV_DOCS
.PHONY: docs
## Build doc site
## @category Docs
docs:
uv run --only-group docs --no-dev mkdocs build --strict --site-dir docs/site
.PHONY: docs-server
## Run the docs server
## @category Docs
docs-server:
uv run --only-group docs --no-dev mkdocs serve --open --dirty
================================================
FILE: cfg/eslint.config.base.js
================================================
import eslintJs from "@eslint/js";
import eslintJson from "@eslint/json";
import eslintPluginComments from "@eslint-community/eslint-plugin-eslint-comments/configs";
import eslintPluginStylistic from "@stylistic/eslint-plugin";
import { defineConfig } from "eslint/config";
import eslintConfigPrettier from "eslint-config-prettier";
import eslintPluginCompat from "eslint-plugin-compat";
import eslintPluginDeMorgan from "eslint-plugin-de-morgan";
import eslintPluginDepend from "eslint-plugin-depend";
import eslintPluginHtml from "eslint-plugin-html";
import eslintPluginImport from "eslint-plugin-import-x";
import eslintPluginMath from "eslint-plugin-math";
import * as eslintPluginMdx from "eslint-plugin-mdx";
import eslintPluginNoSecrets from "eslint-plugin-no-secrets";
import eslintPluginNoUnsanitized from "eslint-plugin-no-unsanitized";
import eslintPluginNoUseExtendNative from "eslint-plugin-no-use-extend-native";
import eslintPluginPerfectionist from "eslint-plugin-perfectionist";
import eslintPluginPrettierRecommended from "eslint-plugin-prettier/recommended";
import eslintPluginPromise from "eslint-plugin-promise";
import eslintPluginRegexp from "eslint-plugin-regexp";
import eslintPluginSecurity from "eslint-plugin-security";
import eslintPluginSonarjs from "eslint-plugin-sonarjs";
import eslintPluginToml from "eslint-plugin-toml";
import eslintPluginUnicorn from "eslint-plugin-unicorn";
import eslintPluginYml from "eslint-plugin-yml";
import globals from "globals";
export const FLAT_ALL = "flat/all";
export const FLAT_RECOMMENDED = "flat/recommended";
export const CONFIGS = {
js: {
...eslintJs.configs.recommended,
...eslintPluginComments.recommended,
...eslintPluginCompat.configs[FLAT_RECOMMENDED],
...eslintPluginDeMorgan.configs.recommended,
...eslintPluginDepend.configs[FLAT_RECOMMENDED],
...eslintPluginImport.flatConfigs.all,
...eslintPluginMath.configs.recommended,
...eslintPluginNoUnsanitized.configs.recommended,
...eslintPluginPerfectionist.configs["recommended-natural"],
...eslintPluginPromise.configs[FLAT_ALL],
...eslintPluginRegexp.configs.all,
...eslintPluginSonarjs.configs.all,
...eslintPluginUnicorn.configs.all,
plugins: {
depend: eslintPluginDepend,
sonarjs: eslintPluginSonarjs,
unicorn: eslintPluginUnicorn,
},
languageOptions: {
ecmaVersion: "latest",
},
rules: {
"@stylistic/multiline-comment-style": "off", // Multiple bugs with this rule
// "import-x/order": "off",
"max-params": ["warn", 4],
"no-console": "warn",
"no-debugger": "warn",
"no-secrets/no-secrets": "error",
"security/detect-object-injection": "off",
},
},
};
Object.freeze(CONFIGS);
export default defineConfig([
{
name: "globalIgnores",
ignores: [
"!.circleci",
"**/*min.css",
"**/*min.js",
"**/__pycache__/",
"**/node_modules/",
"**/package-lock.json",
"*~",
".claude",
".git/",
".*cache/",
".venv/",
"dist/",
"uv.lock",
"test-results/",
"typings/",
],
},
eslintPluginNoUseExtendNative.configs.recommended,
eslintPluginSecurity.configs.recommended,
eslintPluginStylistic.configs.all,
eslintPluginPrettierRecommended,
{
languageOptions: {
globals: {
...globals.node,
},
},
linterOptions: {
reportUnusedDisableDirectives: "warn",
},
plugins: {
"no-secrets": eslintPluginNoSecrets,
perfectionist: eslintPluginPerfectionist,
},
rules: {
"prettier/prettier": "warn",
},
},
{
files: ["**/*.html"],
plugins: { html: eslintPluginHtml },
},
{
files: ["**/*.js"],
...CONFIGS.js,
},
{
files: ["**/*.json", "**/*.md/*.json"],
plugins: {
json: eslintJson,
},
...eslintJson.configs.recommended,
language: "json/json",
},
{
files: ["package.json"],
languageOptions: {
parser: "jsonc-eslint-parser",
},
plugins: { depend: eslintPluginDepend },
rules: {
"depend/ban-dependencies": "error",
},
},
{
files: ["**/*.{md,mdx}"],
...eslintPluginMdx.flat,
...eslintPluginMdx.flatCodeBlocks,
processor: eslintPluginMdx.createRemarkProcessor({
lintCodeBlocks: true,
}),
rules: {
"no-undef": "off",
"no-unused-vars": "off",
"prettier/prettier": ["warn", { parser: "markdown" }],
},
},
...eslintPluginToml.configs.recommended,
{
files: ["**/*.toml", "**/*.md/*.toml"],
rules: {
"prettier/prettier": ["error", { parser: "toml" }],
},
},
...eslintPluginYml.configs.standard,
...eslintPluginYml.configs.prettier,
{
files: ["**/*.yaml", "**/*.yml", "**/*.md/*.yaml", "**/*.md/*.yml"],
rules: {
"prettier/prettier": ["error", { parser: "yaml" }],
},
},
{
files: ["**/certbot.yaml", "**/compose*.yaml", "**/.*_treestamps.yaml"],
rules: {
"yml/no-empty-mapping-value": "off",
},
},
eslintConfigPrettier, // Best if last
]);
================================================
FILE: cfg/frontend.mk
================================================
DEVENV_FRONTEND := 1
export DEVENV_FRONTEND
.PHONY: clean-frontend
## Clean frontend
## @category Clean
clean-frontend:
cd frontend && make clean
.PHONY: clean
## Clean frontend too
## @category Clean
clean:: clean-frontend
.PHONY: install-frontend
## Install frontend
## @category Install
install-frontend:
cd frontend && make install
.PHONY: install
## Install with all extras
## @category Install
install:: install-frontend
.PHONY: update-frontend
## Update deps for frontend
## @category Update
update-frontend:
cd frontend && make update
.PHONY: update
## Update deps for frontend too
## @category Update
update:: update-frontend
.PHONY: fix-frontend
## Fix only frontend lint errors
## @category Lint
fix-frontend:
bash -c "cd frontend && make fix"
.PHONY: fix
## Fix lint errors
## @category Lint
fix:: fix-frontend
.PHONY: lint-frontend
## Lint the frontend
## @category Lint
lint-frontend:
bash -c "cd frontend && make lint"
.PHONY: lint-frontend
## Lint
## @category Lint
lint:: lint-frontend
.PHONY: dev-frontend-server
## Run the vite dev frontend
## @category Run
dev-frontend-server:
bash -c "cd frontend && make dev-server"
.PHONY: test-frontend
## Run frontend tests
## @category Test
test-frontend::
cd frontend && make test
.PHONY: test-frontend
## Run frontend tests too
## Test
## @category Test
test:: test-frontend
.PHONY: build-frontend
## Build frontend
## @category Build
build-frontend:
cd frontend && make build
.PHONY: build
## Build with frontend
## @category Build
build:: build-frontend
================================================
FILE: cfg/help.mk
================================================
# Inspired from
# https://github.com/Mischback/django-calingen/blob/3f0e6db6/Makefile
# and https://gist.github.com/klmr/575726c7e05d8780505a
# fancy colors
cyan := "$$(tput setaf 6)"
green := "$$(tput setaf 2)"
red := "$$(tput setaf 1)"
yel := "$$(tput setaf 3)"
gray := "$$(tput setaf 8)"
grayb := "$$(printf "\033[1m"; tput setaf 8)"
end := "$$(tput sgr0)"
TARGET_STYLED_HELP_NAME = "$(cyan)TARGET$(end)"
ARGUMENTS_HELP_NAME = "$(green)ARGUMENT$(end)=$(red)VALUE$(end)"
# This mountrous sed is compatible with both GNU sed and BSD sed (for macOS).
# That's why "-E", "|", "+", "\s", "?", and "\t" aren't used. See the details
# about BSD sed vs. GNU sed: https://riptutorial.com/sed/topic/9436
target_regex := [a-zA-Z0-9%_\/%-][a-zA-Z0-9%_\/%-]*
variable_regex := [^:= ][^:= ]*
variable_assignment_regex := [ ]*:*[+:!\?]*= *
value_regex := .*
category_annotation_regex := @category *
category_regex := [^<][^<]*
# We first parse and markup with these ad-hoc tags, and then we turn the markup
# into a colorful output.
target_tag_start := <target-definition>
target_tag_end := </target-definition>
target_variable_tag_start := <target-variable>
target_variable_tag_end := </target-variable>
variable_tag_start := <variable>
variable_tag_end := </variable>
global_variable_tag_start := <global-variable>
global_variable_tag_end := </global-variable>
value_tag_start := <value>
value_tag_end := </value>
prerequisites_tag_start := <prerequisites>
prerequisites_tag_end := </prerequisites>
doc_tag_start := <doc>
doc_tag_indented_start := <doc-indent>
doc_tag_indented_end := </doc-indent>
doc_tag_end := </doc>
category_tag_start := <category-other>
category_tag_end := </category-other>
default_category_tag_start := <category-default>
default_category_tag_end := </category-default>
DEFAULT_CATEGORY = General
.DEFAULT_GOAL := help
.PHONY: help
help:
@echo "Usage: make [$(TARGET_STYLED_HELP_NAME) [$(TARGET_STYLED_HELP_NAME) ...]] [$(ARGUMENTS_HELP_NAME) [$(ARGUMENTS_HELP_NAME) ...]]"
@cat ${MAKEFILE_LIST} \
| tr '\t' ' ' \
| sed -n -e "/^## / { \
h; \
s/.*/##/; \
:doc" \
-e "H; \
n; \
s|^## *\(.*\)|$(doc_tag_start)$(doc_tag_indented_start)\1$(doc_tag_indented_end)$(doc_tag_end)|; \
s|^## *\(.*\)|$(doc_tag_start)\1$(doc_tag_end)|; \
t doc" \
-e "s| *#[^#].*||; " \
-e "s|^\(define *\)\($(variable_regex)\)$(variable_assignment_regex)\($(value_regex)\)|$(global_variable_tag_start)\2$(global_variable_tag_end)$(value_tag_start)\3$(value_tag_end)|;" \
-e "s|^\($(variable_regex)\)$(variable_assignment_regex)\($(value_regex)\)|$(global_variable_tag_start)\1$(global_variable_tag_end)$(value_tag_start)\2$(value_tag_end)|;" \
-e "s|^\($(target_regex)\) *: *\(\($(variable_regex)\)$(variable_assignment_regex)\($(value_regex)\)\)|$(target_variable_tag_start)\1$(target_variable_tag_end)$(variable_tag_start)\3$(variable_tag_end)$(value_tag_start)\4$(value_tag_end)|;" \
-e "s|^\($(target_regex)\) *: *\($(target_regex)\( *$(target_regex)\)*\) *\(\| *\( *$(target_regex)\)*\)|$(target_tag_start)\1$(target_tag_end)$(prerequisites_tag_start)\2$(prerequisites_tag_end)|;" \
-e "s|^\($(target_regex)\) *: *\($(target_regex)\( *$(target_regex)\)*\)|$(target_tag_start)\1$(target_tag_end)$(prerequisites_tag_start)\2$(prerequisites_tag_end)|;" \
-e "s|^\($(target_regex)\) *: *\(\| *\( *$(target_regex)\)*\)|$(target_tag_start)\1$(target_tag_end)|;" \
-e "s|^\($(target_regex)\) *: *|$(target_tag_start)\1$(target_tag_end)|;" \
-e " \
G; \
s|## *\(.*\) *##|$(doc_tag_start)\1$(doc_tag_end)|; \
s|\\n||g;" \
-e "/$(category_annotation_regex)/!s|.*|$(default_category_tag_start)$(DEFAULT_CATEGORY)$(default_category_tag_end)&|" \
-e "s|^\(.*\)$(doc_tag_start)$(category_annotation_regex)\($(category_regex)\)$(doc_tag_end)|$(category_tag_start)\2$(category_tag_end)\1|" \
-e "p; \
}" \
| sort \
| sed -n \
-e "s|$(default_category_tag_start)|$(category_tag_start)|" \
-e "s|$(default_category_tag_end)|$(category_tag_end)|" \
-e "{G; s|\($(category_tag_start)$(category_regex)$(category_tag_end)\)\(.*\)\n\1|\2|; s|\n.*||; H; }" \
-e "s|$(category_tag_start)||" \
-e "s|$(category_tag_end)|:\n|" \
-e "s|$(target_variable_tag_start)|$(target_tag_start)|" \
-e "s|$(target_variable_tag_end)|$(target_tag_end)|" \
-e "s|$(target_tag_start)| $(cyan)|" \
-e "s|$(target_tag_end)|$(end) |" \
-e "s|$(prerequisites_tag_start).*$(prerequisites_tag_end)||" \
-e "s|$(variable_tag_start)|$(green)|g" \
-e "s|$(variable_tag_end)|$(end)|" \
-e "s|$(global_variable_tag_start)| $(green)|g" \
-e "s|$(global_variable_tag_end)|$(end)|" \
-e "s|$(value_tag_start)| (default: $(red)|" \
-e "s|$(value_tag_end)|$(end))|" \
-e "s|$(doc_tag_indented_start)|$(grayb)|g" \
-e "s|$(doc_tag_indented_end)|$(end)|g" \
-e "s|$(doc_tag_start)|\n |g" \
-e "s|$(doc_tag_end)||g" \
-e "p"
================================================
FILE: cfg/node.mk
================================================
DEVENV_NODE := 1
export DEVENV_NODE
.PHONY: install-deps-node
## Update and install node packages
## @category Install
install-deps-node:
bun install
.PHONY: install
## Install
## @category Install
install:: install-deps-node
.PHONY: update-node
## Update node dependencies
## @category Update
update-node:
./bin/update-deps-node.sh
.PHONY: update
## Update dependencies
## @category Update
update:: update-node
.PHONY: kill-eslint_d
## Kill eslint daemon
## @category Lint
kill-eslint_d:
bin/kill-eslint_d.sh
## Show version. Use V variable to set version
## @category Update
V :=
.PHONY: version
## Show or set project version for node
## @category Update
version::
bin/version-node.sh $(V)
================================================
FILE: cfg/node_root.mk
================================================
DEVENV_NODE_ROOT := 1
export DEVENV_NODE_ROOT
# Dummy target for mbake linting
.PHONY: all
all: ;
================================================
FILE: cfg/python.mk
================================================
DEVENV_PYTHON := 1
export DEVENV_PYTHON
.PHONY: clean
## Clean python caches
## @category Clean
clean::
find . -name "__pycache__" -print0 | xargs -0 rm -rf
rm -rf .coverage
.PHONY: install-deps-pip
## Update pip and install node packages
## @category Install
install-deps-pip:
pip install --upgrade pip
.PHONY: install-prod
## Install for production
## @category Install
install-prod: install-deps-pip
uv sync --no-install-project --no-dev
.PHONY: install
## Install with dev and all extras and groups
## @category Install
install:: install-deps-pip
uv sync --no-install-project --all-extras --all-groups --all-packages
.PHONY: update-python
## Update python dependencies
## @category Update
update-python:
./bin/update-deps-python.sh
.PHONY: update
## Update dependencies
## @category Update
update:: update-python
## Show version. Use V variable to set version
## @category Update
V :=
.PHONY: version
## Show or set project version for python
## @category Update
version::
bin/version-python.sh $(V)
.PHONY: fix-python
## Fix python lint errors
## @category Fix
fix-python:
./bin/fix-python.sh
.PHONY: fix
## Fix python lint errors
## @category Fix
fix:: fix-python
.PHONY: typecheck
## Static typecheck
## @category Lint
typecheck:
uv run --group lint --group test --group build basedpyright .
.PHONY: ty
## Static typecheck with ty
## @category Lint
ty:
uv run --group lint --group test --group build ty check .
.PHONY: complexity
## Lint backend complexity
## @category Lint
complexity:
./bin/lint-complexity.sh
.PHONY: lint-python
## Lint python
## @category Lint
lint-python:
./bin/lint-python.sh
.PHONY: lint
## Lint python
## @category Lint
lint:: lint-python
.PHONY: uml
## Create a UML class diagram
## @category Lint
uml:
bin/uml.sh
.PHONY: cycle
## Detect Circular imports
## @category Lint
cycle:
uvx pycycle --ignore node_modules,.venv --verbose --here
T :=
.PHONY: test-python
## Test Python
## @category Test
test-python:
./bin/test-python.sh $(T)
.PHONY: test
## Run Python Tests. Use T variable to run specific tests
## @category Test
test:: test-python
ifndef OVERRIDE_BUILD
.PHONY: build
## Build package
## @category Build
build::
uv build
endif
ifndef OVERRIDE_PUBLISH
.PHONY: publish
## Publish package to pypi
## @category Deploy
publish:
uv publish
endif
================================================
FILE: ci/Dockerfile
================================================
ARG CODEX_BUILDER_BASE_VERSION
ARG CODEX_BASE_VERSION
FROM ajslater/codex-builder-base:${CODEX_BUILDER_BASE_VERSION} AS codex-built
ARG CODEX_WHEEL
WORKDIR /app
# Install codex
COPY ./dist/$CODEX_WHEEL ./dist/$CODEX_WHEEL
# hadolint ignore=DL3059,DL3013
RUN PYMUPDF_SETUP_PY_LIMITED_API=0 pip3 install --no-cache-dir ./dist/$CODEX_WHEEL
# Slim down /usr/local before it gets copied to the final image
# hadolint ignore=DL3059
RUN set -eux \
# Remove pip, setuptools, wheel — not needed at runtime
&& pip3 uninstall -y pip setuptools wheel 2>/dev/null || true \
&& rm -rf /usr/local/bin/pip* \
# Strip debug symbols from shared libraries (~30-50% size reduction on .so files)
&& find /usr/local -name '*.so' -exec strip --strip-unneeded {} + 2>/dev/null || true \
&& find /usr/local -name '*.so.*' -exec strip --strip-unneeded {} + 2>/dev/null || true \
# Remove Python bytecode caches (regenerated on first import)
&& find /usr/local -type d -name '__pycache__' -exec rm -rf {} + 2>/dev/null || true \
&& find /usr/local -name '*.pyc' -delete 2>/dev/null || true \
# Remove the stdlib test suite (~30MB) — safe, never needed at runtime
&& rm -rf /usr/local/lib/python*/test \
&& rm -rf /usr/local/lib/python*/idlelib \
&& rm -rf /usr/local/lib/python*/ensurepip \
# Remove type stubs — only used by type checkers
&& find /usr/local -name '*.pyi' -delete 2>/dev/null || true \
# Remove the installed wheel
&& rm -f /tmp/${CODEX_WHEEL}
FROM ajslater/codex-base:${CODEX_BASE_VERSION}
ARG CODEX_VERSION
LABEL org.opencontainers.image.title="Codex" \
org.opencontainers.image.version="${CODEX_VERSION}" \
org.opencontainers.image.authors="AJ Slater <aj@slater.net>" \
org.opencontainers.image.url="https://codex-reader-app" \
org.opencontainers.image.source="https://github.com/ajslater/codex" \
org.opencontainers.image.licenses="GPL-3.0-only" \
org.opencontainers.image.deprecated="true" \
org.opencontainers.image.description="This image has moved to ghcr.io/ajslater/codex"
ENV DOCKER_IMAGE_DEPRECATED="This docker image has moved to ghcr.io/ajslater/codex. This may be the last version on docker.io"
# Create the comics directory
RUN mkdir -p /comics && touch /comics/DOCKER_UNMOUNTED_VOLUME
# Fix Synology comicbox requiring config
RUN mkdir -p /home/abc/.config/comicbox \
&& chown -R abc /home/abc/.config \
&& chmod 777 /home/abc/.config /home/abc/.config/comicbox
# The final image is the mininimal base with /usr/local copied.
# Possibly could optimize this further to only get python and bin
COPY --from=codex-built /usr/local /usr/local
VOLUME /comics
VOLUME /config
EXPOSE 9810
CMD ["/usr/local/bin/codex"]
================================================
FILE: ci/base.Dockerfile
================================================
FROM ajslater/python-debian:3.14.3-slim-trixie_2
ARG CODEX_BASE_VERSION
LABEL maintainer="AJ Slater <aj@slater.net>"
LABEL version=$CODEX_BASE_VERSION
COPY ci/debian.sources /etc/apt/sources.list.d/
# hadolint ignore=DL3008
RUN apt-get clean \
&& apt-get update \
&& apt-get install --no-install-recommends -y \
curl \
libimagequant0 \
libjpeg62-turbo \
libopenjp2-7 \
libssl3 \
libyaml-0-2 \
libtiff6 \
libwebp7 \
ruamel.yaml.clib \
unrar \
zlib1g \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*
# hadolint ignore=DL3013,DL3042
RUN pip3 install --no-cache --upgrade pip
================================================
FILE: ci/builder-base.Dockerfile
================================================
FROM nikolaik/python-nodejs:python3.14-nodejs24
# nodejs25 blocked on bug https://github.com/nodejs/node/issues/60303
ARG CODEX_BUILDER_BASE_VERSION
LABEL maintainer="AJ Slater <aj@slater.net>"
LABEL version=${CODEX_BUILDER_BASE_VERSION}
# **** install codex system build dependency packages ****"
COPY ci/debian.sources /etc/apt/sources.list.d/
# hadolint ignore=DL3008
RUN apt-get clean \
&& apt-get update \
&& apt-get install --no-install-recommends -y \
bash \
build-essential \
cmake \
git \
libimagequant0 \
libjpeg62-turbo \
libopenjp2-7 \
libssl3 \
libyaml-0-2 \
libtiff6 \
libwebp7 \
python3-dev \
ruamel.yaml.clib \
unrar \
zlib1g \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /app
# hadolint ignore=DL3013,DL3042
RUN pip3 install --no-cache --upgrade pip
================================================
FILE: ci/circleci-step-halt.sh
================================================
#!/bin/bash
# If the skip job flag is step. skip this step.
set -euo pipefail
if [ -f ./SKIP_STEPS ]; then
circleci-agent step halt
fi
================================================
FILE: ci/cleanup-repo.py
================================================
#!/usr/bin/env python3
"""Remove old tags from a docker repo."""
import argparse
import json
import sys
import time
from datetime import datetime
from getpass import getpass
from urllib.request import Request, urlopen
API_BASE = "https://hub.docker.com/v2"
HTTP_OK = 200
HTTP_NO_CONTENT = 204
API_TIMEOUT = 10
def login(username, password):
"""Docker login."""
url = f"{API_BASE}/users/login/"
payload = {"username": username, "password": password}
data = json.dumps(payload).encode("utf-8")
request = Request(url, data=data, method="POST") # noqa: S310
resp = urlopen( # noqa: S310
request,
timeout=API_TIMEOUT,
)
if resp.status_code != HTTP_OK:
print(f"Request {url} failed with status code {resp.status_code}:")
print(resp.text)
resp.raise_for_status()
return resp.json()["token"]
def fetch_all_tags(namespace, repo, token):
"""Get all the tags for a repo."""
url = f"{API_BASE}/repositories/{namespace}/{repo}/tags?page_size=100"
headers = {"Authorization": f"JWT {token}"}
tags = []
while url:
request = Request(url, headers=headers) # noqa: S310
response = urlopen(request, timeout=API_TIMEOUT) # noqa: S310
response.raise_for_status()
data = response.json()
tags.extend(data["results"])
url = data.get("next")
return tags
def delete_tag(namespace, repo, tag, token, retries=3, delay=2):
"""Delete a tag."""
headers = {"Authorization": f"JWT {token}"}
url = f"{API_BASE}/repositories/{namespace}/{repo}/tags/{tag}/"
for attempt in range(1, retries + 1):
request = Request(url, headers=headers, method="DELETE") # noqa: S310
resp = urlopen(request, timeout=API_TIMEOUT) # noqa: S310
if resp.status_code == HTTP_NO_CONTENT:
return True
print(
f"Attempt {attempt} failed to delete {tag} (HTTP {resp.status_code}). Retrying in {delay}s..."
)
time.sleep(delay)
return False
def read_password(args):
"""Read password/token securely from stdin or prompt."""
if not sys.stdin.isatty():
# user piped input
return sys.stdin.read().strip()
if args.password_prompt:
# flag for interactive prompt
return getpass("Docker Hub password or access token: ")
return args.password
def get_args():
"""Get Args."""
parser = argparse.ArgumentParser(
description="Cleanup old Docker Hub tags",
epilog="password is preferentially read from stdin",
)
parser.add_argument("username", help="Docker Hub username")
parser.add_argument(
"--password", help="Password or access token (not recommended for security)"
)
parser.add_argument(
"--password-prompt",
action="store_true",
help="Read password securely from prompt",
)
parser.add_argument("namespace", help="Namespace or user of the repository")
parser.add_argument("repository", help="Repository name")
parser.add_argument(
"--no-confirm",
action="store_true",
help="Do not confirm deletion with input prompt",
)
parser.add_argument(
"--keep",
type=int,
default=10,
help="Number of latest tags to keep (default 10)",
)
parser.add_argument(
"--dry-run",
action="store_true",
help="Do not actually delete, just show what would be deleted",
)
return parser.parse_args()
def _init():
args = get_args()
password = read_password(args)
if not password:
sys.exit(
"❌ No password provided. Use --password, --password-stdin, or pipe it in."
)
return args, password
def _get_tags_to_delete(args, token):
"""Get deletebale tags."""
tags = fetch_all_tags(args.namespace, args.repository, token)
if not tags:
print("No tags found.")
return None
# Sort tags by last_updated descending
tags.sort(
key=lambda t: datetime.fromisoformat(t["last_updated"]),
reverse=True,
)
to_delete = tags[args.keep :]
if not to_delete:
print(f"Nothing to delete (<= {args.keep} tags).")
return None
print(f"Keeping {args.keep} most recent tags:")
for t in tags[: args.keep]:
print(f" {t['name']} ({t['last_updated']})")
print(f"\nTags to delete ({len(to_delete)}):")
for t in to_delete:
print(f" {t['name']} ({t['last_updated']})")
return to_delete
def main():
"""Run the Program."""
args, password = _init()
token = login(args.username, password)
print(f"Logged in as {args.username}")
to_delete = _get_tags_to_delete(args, token)
if not to_delete:
return
if args.dry_run:
print("\nDry ru
gitextract_jcsuoc91/ ├── .circleci/ │ └── config.yml ├── .dockerignore ├── .github/ │ └── workflows/ │ └── ci.yml ├── .gitignore ├── .picopt_treestamps.yaml ├── .prettierignore ├── .readthedocs.yaml ├── .shellcheckrc ├── CLAUDE.md ├── Dockerfile ├── LICENSE ├── Makefile ├── NEWS.md ├── README.md ├── bin/ │ ├── benchmark-opds.sh │ ├── build-choices.sh │ ├── build-dist.sh │ ├── ci-download-dist-if-identical.sh │ ├── clean-pycache.sh │ ├── collectstatic.sh │ ├── create-output-dirs.sh │ ├── delete-files.sh │ ├── dev-docker.sh │ ├── dev-module.sh │ ├── dev-prod-server.sh │ ├── dev-reverse-proxy.sh │ ├── dev-server.sh │ ├── dev-ttabs.sh │ ├── docker-compose-exit.sh │ ├── docker-tag-latest.sh │ ├── fix-docker.sh │ ├── fix-python.sh │ ├── fix.sh │ ├── icons_transform.py │ ├── kill-codex.sh │ ├── kill-eslint_d.sh │ ├── lint-ci.sh │ ├── lint-complexity.sh │ ├── lint-darwin.sh │ ├── lint-docker.sh │ ├── lint-python.sh │ ├── lint.sh │ ├── localize-db.sh │ ├── localize_library.sql │ ├── manage.py │ ├── pm │ ├── prettier-nginx.sh │ ├── roman.py │ ├── sort-ignore.sh │ ├── test-python.sh │ ├── uml.sh │ ├── update-deps-node.sh │ ├── update-deps-python.sh │ ├── vendor-diff-package.sh │ ├── vendor-patch-imports.sh │ ├── version-node.sh │ └── version-python.sh ├── cfg/ │ ├── ci.mk │ ├── codex.mk │ ├── common.mk │ ├── django.mk │ ├── docker.mk │ ├── docs.mk │ ├── eslint.config.base.js │ ├── frontend.mk │ ├── help.mk │ ├── node.mk │ ├── node_root.mk │ └── python.mk ├── ci/ │ ├── Dockerfile │ ├── base.Dockerfile │ ├── builder-base.Dockerfile │ ├── circleci-step-halt.sh │ ├── cleanup-repo.py │ ├── debian.sources │ ├── dev.Dockerfile │ ├── dist-builder.Dockerfile │ ├── docker-bake.hcl │ ├── docker-build-image.sh │ ├── docker-compose-exit.sh │ ├── docker-init.sh │ ├── docker-push.sh │ ├── docker-tag-remote-version-as-latest.sh │ ├── machine-arch.sh │ ├── machine-env.sh │ ├── machine-init.sh │ ├── machine-packages.sh │ ├── package.Dockerfile │ ├── python-publish.sh │ ├── version-checksum.sh │ ├── version-codex-base.sh │ ├── version-codex-builder-base.sh │ ├── version-codex-dist-builder.sh │ ├── versions-create-env.sh │ └── versions-env-filename.sh ├── codex/ │ ├── __init__.py │ ├── applications/ │ │ ├── __init__.py │ │ ├── lifespan.py │ │ └── websocket.py │ ├── asgi.py │ ├── authentication.py │ ├── choices/ │ │ ├── __init__.py │ │ ├── admin.py │ │ ├── browser.py │ │ ├── choices_to_json.py │ │ ├── jobs.py │ │ ├── notifications.py │ │ ├── reader.py │ │ ├── search.py │ │ └── statii.py │ ├── librarian/ │ │ ├── README.md │ │ ├── __init__.py │ │ ├── bookmark/ │ │ │ ├── __init__.py │ │ │ ├── bookmarkd.py │ │ │ ├── latest_version.py │ │ │ ├── tasks.py │ │ │ ├── update.py │ │ │ └── user_active.py │ │ ├── covers/ │ │ │ ├── __init__.py │ │ │ ├── coverd.py │ │ │ ├── create.py │ │ │ ├── path.py │ │ │ ├── purge.py │ │ │ ├── status.py │ │ │ └── tasks.py │ │ ├── cron/ │ │ │ ├── __init__.py │ │ │ └── crond.py │ │ ├── fs/ │ │ │ ├── README.md │ │ │ ├── __init__.py │ │ │ ├── event_batcherd.py │ │ │ ├── events.py │ │ │ ├── filters.py │ │ │ ├── poller/ │ │ │ │ ├── __init__.py │ │ │ │ ├── events.py │ │ │ │ ├── poller.py │ │ │ │ ├── snapshot.py │ │ │ │ ├── snapshot_diff.py │ │ │ │ ├── status.py │ │ │ │ └── tasks.py │ │ │ ├── status.py │ │ │ ├── tasks.py │ │ │ └── watcher/ │ │ │ ├── __init__.py │ │ │ ├── data.py │ │ │ ├── dirs.py │ │ │ ├── events.py │ │ │ ├── move.py │ │ │ ├── status.py │ │ │ ├── tasks.py │ │ │ └── watcher.py │ │ ├── librariand.py │ │ ├── memory.py │ │ ├── mp_queue.py │ │ ├── notifier/ │ │ │ ├── __init__.py │ │ │ ├── notifierd.py │ │ │ └── tasks.py │ │ ├── restarter/ │ │ │ ├── __init__.py │ │ │ ├── restarter.py │ │ │ ├── status.py │ │ │ └── tasks.py │ │ ├── scribe/ │ │ │ ├── __init__.py │ │ │ ├── importer/ │ │ │ │ ├── __init__.py │ │ │ │ ├── const.py │ │ │ │ ├── create/ │ │ │ │ │ ├── __init__.py │ │ │ │ │ ├── comics.py │ │ │ │ │ ├── const.py │ │ │ │ │ ├── covers.py │ │ │ │ │ ├── folders.py │ │ │ │ │ ├── foreign_keys.py │ │ │ │ │ └── link_fks.py │ │ │ │ ├── delete/ │ │ │ │ │ ├── __init__.py │ │ │ │ │ ├── comics.py │ │ │ │ │ ├── covers.py │ │ │ │ │ └── folders.py │ │ │ │ ├── failed/ │ │ │ │ │ ├── __init__.py │ │ │ │ │ ├── create.py │ │ │ │ │ ├── failed.py │ │ │ │ │ └── query.py │ │ │ │ ├── finish.py │ │ │ │ ├── importer.py │ │ │ │ ├── init.py │ │ │ │ ├── link/ │ │ │ │ │ ├── __init__.py │ │ │ │ │ ├── const.py │ │ │ │ │ ├── covers.py │ │ │ │ │ ├── delete.py │ │ │ │ │ ├── many_to_many.py │ │ │ │ │ ├── prepare.py │ │ │ │ │ └── sum.py │ │ │ │ ├── moved/ │ │ │ │ │ ├── __init__.py │ │ │ │ │ ├── comics.py │ │ │ │ │ ├── covers.py │ │ │ │ │ └── folders.py │ │ │ │ ├── query/ │ │ │ │ │ ├── __init__.py │ │ │ │ │ ├── covers.py │ │ │ │ │ ├── filters.py │ │ │ │ │ ├── foreign_keys.py │ │ │ │ │ ├── links.py │ │ │ │ │ ├── links_fk.py │ │ │ │ │ ├── links_m2m.py │ │ │ │ │ ├── update_comics.py │ │ │ │ │ └── update_fks.py │ │ │ │ ├── read/ │ │ │ │ │ ├── __init__.py │ │ │ │ │ ├── aggregate_path.py │ │ │ │ │ ├── const.py │ │ │ │ │ ├── extract.py │ │ │ │ │ ├── folders.py │ │ │ │ │ ├── foreign_keys.py │ │ │ │ │ └── many_to_many.py │ │ │ │ ├── search/ │ │ │ │ │ ├── __init__.py │ │ │ │ │ ├── prepare.py │ │ │ │ │ ├── sync_m2m.py │ │ │ │ │ └── update.py │ │ │ │ ├── statii/ │ │ │ │ │ ├── __init__.py │ │ │ │ │ ├── create.py │ │ │ │ │ ├── delete.py │ │ │ │ │ ├── failed.py │ │ │ │ │ ├── link.py │ │ │ │ │ ├── moved.py │ │ │ │ │ ├── query.py │ │ │ │ │ ├── read.py │ │ │ │ │ └── search.py │ │ │ │ ├── status.py │ │ │ │ └── tasks.py │ │ │ ├── janitor/ │ │ │ │ ├── __init__.py │ │ │ │ ├── adopt_folders.py │ │ │ │ ├── cleanup.py │ │ │ │ ├── failed_imports.py │ │ │ │ ├── integrity/ │ │ │ │ │ ├── __init__.py │ │ │ │ │ └── foreign_keys.py │ │ │ │ ├── janitor.py │ │ │ │ ├── scheduled_time.py │ │ │ │ ├── status.py │ │ │ │ ├── tasks.py │ │ │ │ ├── update.py │ │ │ │ └── vacuum.py │ │ │ ├── lazy_importer.py │ │ │ ├── priority.py │ │ │ ├── scribed.py │ │ │ ├── search/ │ │ │ │ ├── __init__.py │ │ │ │ ├── const.py │ │ │ │ ├── handler.py │ │ │ │ ├── optimize.py │ │ │ │ ├── prepare.py │ │ │ │ ├── remove.py │ │ │ │ ├── status.py │ │ │ │ ├── sync.py │ │ │ │ └── tasks.py │ │ │ ├── status.py │ │ │ ├── tasks.py │ │ │ └── timestamp_update.py │ │ ├── status.py │ │ ├── status_controller.py │ │ ├── tasks.py │ │ ├── telemeter/ │ │ │ ├── __init__.py │ │ │ ├── scheduled_time.py │ │ │ ├── stats.py │ │ │ ├── tasks.py │ │ │ └── telemeter.py │ │ ├── threads.py │ │ └── worker.py │ ├── middleware.py │ ├── migrations/ │ │ ├── 0001_init.py │ │ ├── 0002_auto_20200826_0622.py │ │ ├── 0003_auto_20200831_2033.py │ │ ├── 0004_failedimport.py │ │ ├── 0005_auto_20200918_0146.py │ │ ├── 0006_update_default_names_and_remove_duplicate_comics.py │ │ ├── 0007_auto_20211210_1710.py │ │ ├── 0008_alter_comic_created_at_alter_comic_format_and_more.py │ │ ├── 0009_alter_comic_parent_folder.py │ │ ├── 0010_haystack.py │ │ ├── 0011_library_groups_and_metadata_changes.py │ │ ├── 0012_rename_description_comic_comments.py │ │ ├── 0013_int_issue_count_longer_charfields.py │ │ ├── 0014_pdf_issue_suffix_remove_cover_image_sort_name.py │ │ ├── 0015_link_comics_to_top_level_folders.py │ │ ├── 0016_remove_comic_cover_path_librarianstatus.py │ │ ├── 0017_alter_timestamp_options_alter_adminflag_name_and_more.py │ │ ├── 0018_rename_userbookmark_bookmark.py │ │ ├── 0019_delete_queuejob.py │ │ ├── 0020_remove_search_tables.py │ │ ├── 0021_bookmark_fit_to_choices_read_in_reverse.py │ │ ├── 0022_bookmark_vertical_useractive_null_statuses.py │ │ ├── 0023_rename_credit_creator_and_more.py │ │ ├── 0024_comic_gtin_comic_story_arc_number.py │ │ ├── 0025_add_story_arc_number.py │ │ ├── 0026_comicbox_1.py │ │ ├── 0027_import_order_and_covers.py │ │ ├── 0028_telemeter.py │ │ ├── 0029_comicfts.py │ │ ├── 0030_nocase_collation_day_month_indexes_status_types.py │ │ ├── 0031_adminflag_banner.py │ │ ├── 0032_alter_librarianstatus_preactive.py │ │ ├── 0033_alter_librarianstatus_status_type.py │ │ ├── 0034_comicbox2.py │ │ ├── 0035_fts_optmize.py │ │ ├── 0036_alter_comic_path_alter_customcover_path_and_more.py │ │ ├── 0037_redefine_reading_direction_filetype_choices.py │ │ ├── 0038_settings_tables.py │ │ └── __init__.py │ ├── models/ │ │ ├── __init__.py │ │ ├── admin.py │ │ ├── base.py │ │ ├── bookmark.py │ │ ├── choices.py │ │ ├── comic.py │ │ ├── fields.py │ │ ├── functions.py │ │ ├── groups.py │ │ ├── identifier.py │ │ ├── library.py │ │ ├── named.py │ │ ├── paths.py │ │ ├── query.py │ │ ├── settings.py │ │ └── util.py │ ├── run.py │ ├── serializers/ │ │ ├── README.md │ │ ├── __init__.py │ │ ├── admin/ │ │ │ ├── __init__.py │ │ │ ├── flags.py │ │ │ ├── groups.py │ │ │ ├── libraries.py │ │ │ ├── stats.py │ │ │ ├── tasks.py │ │ │ └── users.py │ │ ├── auth.py │ │ ├── browser/ │ │ │ ├── __init__.py │ │ │ ├── choices.py │ │ │ ├── filters.py │ │ │ ├── metadata.py │ │ │ ├── mixins.py │ │ │ ├── mtime.py │ │ │ ├── page.py │ │ │ ├── saved.py │ │ │ └── settings.py │ │ ├── fields/ │ │ │ ├── __init__.py │ │ │ ├── auth.py │ │ │ ├── base.py │ │ │ ├── browser.py │ │ │ ├── group.py │ │ │ ├── reader.py │ │ │ ├── sanitized.py │ │ │ ├── settings.py │ │ │ ├── stats.py │ │ │ └── vuetify.py │ │ ├── homepage.py │ │ ├── mixins.py │ │ ├── models/ │ │ │ ├── __init__.py │ │ │ ├── admin.py │ │ │ ├── base.py │ │ │ ├── bookmark.py │ │ │ ├── comic.py │ │ │ ├── groups.py │ │ │ ├── named.py │ │ │ └── pycountry.py │ │ ├── opds/ │ │ │ ├── __init__.py │ │ │ ├── authentication.py │ │ │ ├── urls.py │ │ │ ├── v1.py │ │ │ └── v2/ │ │ │ ├── __init__.py │ │ │ ├── facet.py │ │ │ ├── feed.py │ │ │ ├── links.py │ │ │ ├── metadata.py │ │ │ ├── progression.py │ │ │ ├── publication.py │ │ │ └── unused.py │ │ ├── reader.py │ │ ├── redirect.py │ │ ├── route.py │ │ ├── settings.py │ │ └── versions.py │ ├── settings/ │ │ ├── README.md │ │ ├── __init__.py │ │ ├── codex.toml.default │ │ ├── config.py │ │ ├── hypercorn_migrate.py │ │ ├── logging.py │ │ ├── secret_key.py │ │ ├── servestatic.py │ │ └── timezone.py │ ├── signals/ │ │ ├── __init__.py │ │ ├── django_signals.py │ │ └── os_signals.py │ ├── startup/ │ │ ├── __init__.py │ │ ├── custom_cover_libraries.py │ │ ├── db.py │ │ ├── loguru.py │ │ └── registration.py │ ├── static_src/ │ │ ├── img/ │ │ │ └── .picopt_treestamps.yaml │ │ ├── pwa/ │ │ │ └── offline.html │ │ └── robots.txt │ ├── templates/ │ │ ├── README.md │ │ ├── headers-icons.html │ │ ├── headers-script-globals.html │ │ ├── index.html │ │ ├── opds_v1/ │ │ │ ├── index.xml │ │ │ └── opensearch_v1.xml │ │ └── pwa/ │ │ ├── headers.html │ │ ├── manifest.webmanifest │ │ ├── serviceworker-register.js │ │ └── serviceworker.js │ ├── urls/ │ │ ├── __init__.py │ │ ├── api/ │ │ │ ├── __init__.py │ │ │ ├── admin.py │ │ │ ├── auth.py │ │ │ ├── browser.py │ │ │ ├── reader.py │ │ │ ├── root.py │ │ │ └── v3.py │ │ ├── app.py │ │ ├── const.py │ │ ├── converters.py │ │ ├── opds/ │ │ │ ├── __init__.py │ │ │ ├── authentication.py │ │ │ ├── binary.py │ │ │ ├── root.py │ │ │ ├── v1.py │ │ │ └── v2.py │ │ ├── pwa.py │ │ ├── root.py │ │ └── spectacular.py │ ├── util.py │ ├── version.py │ ├── views/ │ │ ├── README.md │ │ ├── __init__.py │ │ ├── admin/ │ │ │ ├── __init__.py │ │ │ ├── api_key.py │ │ │ ├── auth.py │ │ │ ├── flag.py │ │ │ ├── group.py │ │ │ ├── library.py │ │ │ ├── permissions.py │ │ │ ├── stats.py │ │ │ ├── tasks.py │ │ │ └── user.py │ │ ├── auth.py │ │ ├── bookmark.py │ │ ├── browser/ │ │ │ ├── __init__.py │ │ │ ├── annotate/ │ │ │ │ ├── __init__.py │ │ │ │ ├── bookmark.py │ │ │ │ ├── card.py │ │ │ │ └── order.py │ │ │ ├── bookmark.py │ │ │ ├── breadcrumbs.py │ │ │ ├── browser.py │ │ │ ├── choices.py │ │ │ ├── const.py │ │ │ ├── cover.py │ │ │ ├── download.py │ │ │ ├── filters/ │ │ │ │ ├── __init__.py │ │ │ │ ├── bookmark.py │ │ │ │ ├── field.py │ │ │ │ ├── filter.py │ │ │ │ ├── group.py │ │ │ │ └── search/ │ │ │ │ ├── __init__.py │ │ │ │ ├── field/ │ │ │ │ │ ├── __init__.py │ │ │ │ │ ├── column.py │ │ │ │ │ ├── expression.py │ │ │ │ │ ├── filter.py │ │ │ │ │ ├── optimize.py │ │ │ │ │ └── parse.py │ │ │ │ ├── fts.py │ │ │ │ └── parse.py │ │ │ ├── group_mtime.py │ │ │ ├── metadata/ │ │ │ │ ├── __init__.py │ │ │ │ ├── annotate.py │ │ │ │ ├── const.py │ │ │ │ ├── copy_intersections.py │ │ │ │ └── query_intersections.py │ │ │ ├── mtime.py │ │ │ ├── order_by.py │ │ │ ├── page_in_bounds.py │ │ │ ├── paginate.py │ │ │ ├── params.py │ │ │ ├── saved_settings.py │ │ │ ├── settings.py │ │ │ ├── title.py │ │ │ └── validate.py │ │ ├── const.py │ │ ├── download.py │ │ ├── error.py │ │ ├── exceptions.py │ │ ├── frontend.py │ │ ├── healthcheck.py │ │ ├── lazy_import.py │ │ ├── mixins.py │ │ ├── opds/ │ │ │ ├── __init__.py │ │ │ ├── auth.py │ │ │ ├── authentication/ │ │ │ │ ├── __init__.py │ │ │ │ └── v1.py │ │ │ ├── binary.py │ │ │ ├── const.py │ │ │ ├── error.py │ │ │ ├── feed.py │ │ │ ├── metadata.py │ │ │ ├── opensearch/ │ │ │ │ ├── __init__.py │ │ │ │ └── v1.py │ │ │ ├── settings.py │ │ │ ├── start.py │ │ │ ├── urls.py │ │ │ ├── user_agent.py │ │ │ ├── v1/ │ │ │ │ ├── __init__.py │ │ │ │ ├── const.py │ │ │ │ ├── entry/ │ │ │ │ │ ├── __init__.py │ │ │ │ │ ├── entry.py │ │ │ │ │ └── links.py │ │ │ │ ├── facets.py │ │ │ │ ├── feed.py │ │ │ │ └── links.py │ │ │ └── v2/ │ │ │ ├── __init__.py │ │ │ ├── const.py │ │ │ ├── feed/ │ │ │ │ ├── __init__.py │ │ │ │ ├── feed_links.py │ │ │ │ ├── groups.py │ │ │ │ ├── links.py │ │ │ │ └── publications.py │ │ │ ├── href.py │ │ │ ├── manifest.py │ │ │ └── progression.py │ │ ├── public.py │ │ ├── pwa.py │ │ ├── reader/ │ │ │ ├── __init__.py │ │ │ ├── arcs.py │ │ │ ├── books.py │ │ │ ├── page.py │ │ │ ├── params.py │ │ │ ├── reader.py │ │ │ └── settings.py │ │ ├── settings.py │ │ ├── template.py │ │ ├── timezone.py │ │ ├── util.py │ │ └── version.py │ └── websockets/ │ ├── README.md │ ├── __init__.py │ ├── consumers.py │ ├── listener.py │ └── mp_queue.py ├── compose.yaml ├── docs/ │ ├── DOCKER.md │ ├── WINDOWS.md │ ├── requirements.txt │ ├── style.material.css │ ├── style.mkdocs.css │ └── style.readthedocs.css ├── eslint.config.js ├── frontend/ │ ├── .gitignore │ ├── .npmrc │ ├── .prettierignore │ ├── .remarkignore │ ├── .shellcheckrc │ ├── Makefile │ ├── README.md │ ├── bin/ │ │ ├── dev-server.sh │ │ ├── fix.sh │ │ ├── kill-eslint_d.sh │ │ ├── lint-darwin.sh │ │ ├── lint.sh │ │ ├── roman.py │ │ ├── sort-ignore.sh │ │ ├── update-deps-node.sh │ │ └── version-node.sh │ ├── cfg/ │ │ ├── codex-frontend.mk │ │ ├── common.mk │ │ ├── help.mk │ │ └── node.mk │ ├── jsconfig.json │ ├── package.json │ ├── src/ │ │ ├── admin.vue │ │ ├── api/ │ │ │ └── v3/ │ │ │ ├── admin.js │ │ │ ├── auth.js │ │ │ ├── base.js │ │ │ ├── browser.js │ │ │ ├── common.js │ │ │ ├── notify.js │ │ │ ├── reader.js │ │ │ └── vuetify-items.js │ │ ├── app.vue │ │ ├── browser.vue │ │ ├── comic-name.js │ │ ├── components/ │ │ │ ├── admin/ │ │ │ │ ├── admin-header.vue │ │ │ │ ├── browser-link.vue │ │ │ │ ├── create-update-dialog/ │ │ │ │ │ ├── create-update-button.vue │ │ │ │ │ ├── create-update-dialog.vue │ │ │ │ │ ├── create-update-inputs-mixin.js │ │ │ │ │ ├── duration-input.vue │ │ │ │ │ ├── group-create-update-inputs.vue │ │ │ │ │ ├── library-create-update-inputs.vue │ │ │ │ │ ├── relation-picker.vue │ │ │ │ │ ├── server-folder-picker.vue │ │ │ │ │ └── user-create-update-inputs.vue │ │ │ │ ├── drawer/ │ │ │ │ │ ├── admin-menu.vue │ │ │ │ │ ├── admin-settings-button-progress.vue │ │ │ │ │ ├── admin-settings-drawer.vue │ │ │ │ │ ├── admin-settings-panel.vue │ │ │ │ │ ├── status-list-item.vue │ │ │ │ │ └── status-list.vue │ │ │ │ ├── group-chip.vue │ │ │ │ ├── status-helpers.js │ │ │ │ ├── tabs/ │ │ │ │ │ ├── admin-table.vue │ │ │ │ │ ├── custom-covers-panel.vue │ │ │ │ │ ├── datetime-column.vue │ │ │ │ │ ├── delete-row-dialog.vue │ │ │ │ │ ├── failed-imports-panel.vue │ │ │ │ │ ├── flag-descriptions.json │ │ │ │ │ ├── flag-tab.vue │ │ │ │ │ ├── group-tab.vue │ │ │ │ │ ├── job-tab.vue │ │ │ │ │ ├── library-tab.vue │ │ │ │ │ ├── library-table.vue │ │ │ │ │ ├── relation-chips.vue │ │ │ │ │ ├── stats-tab.vue │ │ │ │ │ ├── stats-table.vue │ │ │ │ │ ├── tabs.vue │ │ │ │ │ └── user-tab.vue │ │ │ │ └── use-now-timer.js │ │ │ ├── anchors.scss │ │ │ ├── auth/ │ │ │ │ ├── auth-form-mixin.js │ │ │ │ ├── auth-menu.vue │ │ │ │ ├── auth-token.vue │ │ │ │ ├── change-password-dialog.vue │ │ │ │ └── login-dialog.vue │ │ │ ├── banner.vue │ │ │ ├── book-cover.scss │ │ │ ├── book-cover.vue │ │ │ ├── browser/ │ │ │ │ ├── browser-header.vue │ │ │ │ ├── card/ │ │ │ │ │ ├── browser-card-menu.vue │ │ │ │ │ ├── card.vue │ │ │ │ │ ├── controls.vue │ │ │ │ │ ├── order-by-caption.vue │ │ │ │ │ └── subtitle.vue │ │ │ │ ├── drawer/ │ │ │ │ │ ├── browser-settings-covers.vue │ │ │ │ │ ├── browser-settings-drawer.vue │ │ │ │ │ ├── browser-settings-group.vue │ │ │ │ │ ├── browser-settings-misc.vue │ │ │ │ │ ├── browser-settings-panel.vue │ │ │ │ │ └── browser-settings-saved.vue │ │ │ │ ├── empty.vue │ │ │ │ ├── filter-warning-snackbar.vue │ │ │ │ ├── main.vue │ │ │ │ └── toolbars/ │ │ │ │ ├── breadcrumbs/ │ │ │ │ │ ├── breadcrumbs.vue │ │ │ │ │ └── browser-toolbar-breadcrumbs.vue │ │ │ │ ├── browser-toolbar-title.vue │ │ │ │ ├── nav/ │ │ │ │ │ ├── browser-nav-button.vue │ │ │ │ │ └── browser-toolbar-nav.vue │ │ │ │ ├── search/ │ │ │ │ │ ├── browser-toolbar-search.vue │ │ │ │ │ ├── search-combobox.vue │ │ │ │ │ ├── search-help-text.vue │ │ │ │ │ └── search-help.vue │ │ │ │ ├── select-many/ │ │ │ │ │ └── browser-toolbar-select-many.vue │ │ │ │ └── top/ │ │ │ │ ├── browser-toolbar-top.vue │ │ │ │ ├── filter-by-select.vue │ │ │ │ ├── filter-sub-menu.vue │ │ │ │ ├── order-by-select.vue │ │ │ │ ├── order-reverse-button.vue │ │ │ │ ├── search-button.vue │ │ │ │ ├── toolbar-button.vue │ │ │ │ └── top-group-select.vue │ │ │ ├── cancel-button.vue │ │ │ ├── clipboard.vue │ │ │ ├── close-button.vue │ │ │ ├── codex-list-item.vue │ │ │ ├── confirm-dialog.vue │ │ │ ├── confirm-footer.vue │ │ │ ├── download-button.vue │ │ │ ├── empty.vue │ │ │ ├── mark-read-button.vue │ │ │ ├── metadata/ │ │ │ │ ├── expand-button.vue │ │ │ │ ├── metadata-activator.vue │ │ │ │ ├── metadata-body.vue │ │ │ │ ├── metadata-chip.vue │ │ │ │ ├── metadata-controls.vue │ │ │ │ ├── metadata-cover.vue │ │ │ │ ├── metadata-dialog.vue │ │ │ │ ├── metadata-header.vue │ │ │ │ ├── metadata-ratings.vue │ │ │ │ ├── metadata-tags.vue │ │ │ │ ├── metadata-text.vue │ │ │ │ ├── table.scss │ │ │ │ └── tags-table.vue │ │ │ ├── pagination-nav-button.vue │ │ │ ├── pagination-slider.vue │ │ │ ├── pagination-toolbar.vue │ │ │ ├── placeholder-loading.vue │ │ │ ├── reader/ │ │ │ │ ├── book-change-activator.vue │ │ │ │ ├── book-change-drawer.vue │ │ │ │ ├── books-window.vue │ │ │ │ ├── change-column.scss │ │ │ │ ├── drawer/ │ │ │ │ │ ├── download-panel.vue │ │ │ │ │ ├── keyboard-shortcuts-panel.vue │ │ │ │ │ ├── keyboard-shortcuts-table.vue │ │ │ │ │ ├── reader-settings-controls.vue │ │ │ │ │ ├── reader-settings-drawer.vue │ │ │ │ │ ├── reader-settings-panel.vue │ │ │ │ │ ├── reader-settings-reader.vue │ │ │ │ │ └── reader-settings-scope.vue │ │ │ │ ├── empty.vue │ │ │ │ ├── pager/ │ │ │ │ │ ├── horizontal-pages.vue │ │ │ │ │ ├── page/ │ │ │ │ │ │ ├── page-error.vue │ │ │ │ │ │ ├── page-img.vue │ │ │ │ │ │ ├── page-loading.vue │ │ │ │ │ │ └── page.vue │ │ │ │ │ ├── page-change-link.vue │ │ │ │ │ ├── pager-full-pdf.vue │ │ │ │ │ ├── pager-horizontal.vue │ │ │ │ │ ├── pager-vertical.vue │ │ │ │ │ ├── pager.vue │ │ │ │ │ ├── pdf-doc.vue │ │ │ │ │ └── scale-for-scroll.vue │ │ │ │ └── toolbars/ │ │ │ │ ├── nav/ │ │ │ │ │ ├── reader-book-change-nav-button.vue │ │ │ │ │ ├── reader-nav-button.vue │ │ │ │ │ └── reader-toolbar-nav.vue │ │ │ │ └── top/ │ │ │ │ ├── reader-arc-select.vue │ │ │ │ └── reader-toolbar-top.vue │ │ │ ├── scale-button.vue │ │ │ ├── settings/ │ │ │ │ ├── button.vue │ │ │ │ ├── docs-footer.vue │ │ │ │ ├── opds-dialog.vue │ │ │ │ ├── opds-url.vue │ │ │ │ ├── settings-drawer.vue │ │ │ │ └── version-footer.vue │ │ │ ├── submit-footer.vue │ │ │ ├── toolbar-select.vue │ │ │ └── unauthorized.vue │ │ ├── datetime.js │ │ ├── http-error.vue │ │ ├── main.js │ │ ├── platform.js │ │ ├── plugins/ │ │ │ ├── drag-scroll.js │ │ │ ├── router.js │ │ │ └── vuetify.js │ │ ├── reader.vue │ │ ├── route.js │ │ ├── stores/ │ │ │ ├── admin.js │ │ │ ├── auth.js │ │ │ ├── browser-select-many.js │ │ │ ├── browser.js │ │ │ ├── common.js │ │ │ ├── metadata.js │ │ │ ├── reader.js │ │ │ ├── socket.js │ │ │ └── store.js │ │ └── util.js │ ├── tests/ │ │ └── unit/ │ │ └── reader-nav-button.test.js │ └── vite.config.js ├── mkdocs.yml ├── mock_comics/ │ ├── __init__.py │ ├── bigbook.py │ ├── mock_comics.py │ └── mock_comics.sh ├── nginx/ │ └── default.conf ├── package.json ├── pyproject.toml ├── tests/ │ ├── README.md │ ├── __init__.py │ ├── files/ │ │ ├── comicbox-2-example.cbz │ │ ├── comicbox-2-update.cbz │ │ ├── comicbox.example.yaml │ │ └── comicbox.update.yaml │ ├── importer/ │ │ ├── __init__.py │ │ ├── test_basic.py │ │ ├── test_update_all.py │ │ └── test_update_none.py │ ├── nginx-local-codex.conf │ ├── test_asgi.py │ └── test_models.py └── vulture_ignorelist.py
Showing preview only (205K chars total). Download the full file or copy to clipboard to get everything.
SYMBOL INDEX (2299 symbols across 384 files)
FILE: bin/icons_transform.py
function create_maskable_icon (line 31) | def create_maskable_icon(input_path):
function inkscape (line 47) | def inkscape(input_path, export_path, width, height):
function transform_icon (line 61) | def transform_icon(name, size):
function picopt (line 100) | def picopt():
function main (line 106) | def main():
FILE: bin/manage.py
function main (line 8) | def main():
FILE: bin/roman.py
function build_ignore_spec (line 46) | def build_ignore_spec(ignore_path: Path | None) -> PathSpec:
function read_first_two_lines (line 56) | def read_first_two_lines(path: Path) -> tuple[str, str]:
function is_shell_script (line 70) | def is_shell_script(line1: str) -> bool:
function has_description_comment (line 75) | def has_description_comment(line2: str) -> bool:
function iter_files (line 80) | def iter_files(path_strs: Sequence[str], spec: PathSpec) -> Generator[Pa...
function build_parser (line 120) | def build_parser() -> ArgumentParser:
function _parse_ignore_file (line 145) | def _parse_ignore_file(args: Namespace) -> PathSpec:
function main (line 160) | def main() -> None:
FILE: cfg/eslint.config.base.js
constant FLAT_ALL (line 28) | const FLAT_ALL = "flat/all";
constant FLAT_RECOMMENDED (line 29) | const FLAT_RECOMMENDED = "flat/recommended";
constant CONFIGS (line 31) | const CONFIGS = {
FILE: ci/cleanup-repo.py
function login (line 18) | def login(username, password):
function fetch_all_tags (line 37) | def fetch_all_tags(namespace, repo, token):
function delete_tag (line 52) | def delete_tag(namespace, repo, tag, token, retries=3, delay=2):
function read_password (line 68) | def read_password(args):
function get_args (line 79) | def get_args():
function _init (line 115) | def _init():
function _get_tags_to_delete (line 126) | def _get_tags_to_delete(args, token):
function main (line 154) | def main():
FILE: codex/applications/lifespan.py
class LifespanApplication (line 13) | class LifespanApplication:
method __init__ (line 18) | def __init__(self, broadcast_queue) -> None:
method _event (line 25) | async def _event(self, event, send) -> None:
method _startup (line 38) | async def _startup(self) -> None:
method _shutdown (line 45) | async def _shutdown(self) -> None:
method __call__ (line 53) | async def __call__(self, scope, receive, send) -> None:
FILE: codex/authentication.py
class BearerTokenAuthentication (line 7) | class BearerTokenAuthentication(TokenAuthentication):
class HttpRemoteUserMiddleware (line 13) | class HttpRemoteUserMiddleware(RemoteUserMiddleware):
FILE: codex/choices/admin.py
class AdminFlagChoices (line 8) | class AdminFlagChoices(TextChoices):
FILE: codex/choices/choices_to_json.py
function _to_vuetify_choices (line 44) | def _to_vuetify_choices(defaults, key: str, obj_map) -> list:
function _json_key (line 59) | def _json_key(key: str):
function _make_json_serializable (line 64) | def _make_json_serializable(data, *, jsonize_keys: bool = True) -> list ...
function _to_vuetify_dict (line 78) | def _to_vuetify_dict(fn: str, data) -> dict:
function _dump (line 91) | def _dump(
function _make_websocket_messages (line 106) | def _make_websocket_messages() -> MappingProxyType:
function main (line 112) | def main() -> None:
FILE: codex/choices/notifications.py
class Notifications (line 6) | class Notifications(Enum):
FILE: codex/choices/search.py
function gen_multipart_field_aliases (line 32) | def gen_multipart_field_aliases(field) -> frozenset:
function _get_fieldmap_values (line 51) | def _get_fieldmap_values(*args) -> tuple:
function create_search_field_map (line 125) | def create_search_field_map() -> dict:
FILE: codex/librarian/bookmark/bookmarkd.py
class BookmarkKey (line 24) | class BookmarkKey:
method __hash__ (line 32) | def __hash__(self) -> int:
method __eq__ (line 40) | def __eq__(self, other) -> bool:
class BookmarkThread (line 45) | class BookmarkThread(
method __init__ (line 55) | def __init__(self, *args, **kwargs) -> None:
method _process_task_immediately (line 61) | def _process_task_immediately(self, task) -> None:
method aggregate_items (line 78) | def aggregate_items(self, item) -> None:
method send_all_items (line 96) | def send_all_items(self) -> None:
FILE: codex/librarian/bookmark/latest_version.py
class CodexLatestVersionUpdater (line 21) | class CodexLatestVersionUpdater(WorkerStatusBase):
method _fetch_latest_version (line 25) | def _fetch_latest_version():
method update_latest_version (line 32) | def update_latest_version(self, *, force: bool, update: bool = False) ...
FILE: codex/librarian/bookmark/tasks.py
class BookmarkTask (line 10) | class BookmarkTask(LibrarianTask):
class BookmarkUpdateTask (line 15) | class BookmarkUpdateTask(BookmarkTask):
class UserActiveTask (line 24) | class UserActiveTask(BookmarkTask):
class ClearLibrarianStatusTask (line 30) | class ClearLibrarianStatusTask(BookmarkTask):
class CodexLatestVersionTask (line 35) | class CodexLatestVersionTask(BookmarkTask):
FILE: codex/librarian/bookmark/update.py
class BookmarkUpdateMixin (line 21) | class BookmarkUpdateMixin(GroupACLMixin):
method _get_existing_bookmarks_for_update (line 27) | def _get_existing_bookmarks_for_update(
method _prepare_bookmark_updates (line 45) | def _prepare_bookmark_updates(cls, existing_bookmarks, updates) -> list:
method _update_bookmarks_validate_page (line 57) | def _update_bookmarks_validate_page(bm, updates) -> None:
method _notify_library_changed (line 70) | def _notify_library_changed(uid) -> None:
method _update_bookmarks (line 77) | def _update_bookmarks(cls, auth_filter, comic_pks, updates) -> int:
method _get_comics_without_bookmarks (line 95) | def _get_comics_without_bookmarks(cls, auth_filter, comic_pks):
method _prepare_bookmark_creates (line 104) | def _prepare_bookmark_creates(
method _create_bookmarks (line 115) | def _create_bookmarks(cls, auth_filter, comic_pks, updates) -> int:
method update_bookmarks (line 139) | def update_bookmarks(cls, auth_filter, comic_pks, updates) -> int:
FILE: codex/librarian/bookmark/user_active.py
class UserActiveMixin (line 12) | class UserActiveMixin:
method init_user_active (line 18) | def init_user_active(self) -> None:
method update_user_active (line 22) | def update_user_active(self, pk: int, log) -> None:
FILE: codex/librarian/covers/coverd.py
class CoverThread (line 15) | class CoverThread(CoverPurgeThread):
method process_item (line 19) | def process_item(self, item) -> None:
FILE: codex/librarian/covers/create.py
class CoverCreateThread (line 27) | class CoverCreateThread(QueuedThread, CoverPathMixin, ABC):
method _create_cover_thumbnail (line 31) | def _create_cover_thumbnail(cls, cover_image_data) -> BytesIO:
method _get_comic_cover_image (line 46) | def _get_comic_cover_image(cls, comic_path, log):
method _get_custom_cover_image (line 60) | def _get_custom_cover_image(cls, cover_path):
method create_cover_from_path (line 66) | def create_cover_from_path(
method save_cover_to_cache (line 95) | def save_cover_to_cache(self, cover_path_str: str, data) -> None:
method _bulk_create_comic_cover (line 106) | def _bulk_create_comic_cover(
method _bulk_create_comic_covers (line 127) | def _bulk_create_comic_covers(self, pks, *, custom: bool) -> int:
method create_all_covers (line 149) | def create_all_covers(self) -> None:
FILE: codex/librarian/covers/path.py
class CoverPathMixin (line 8) | class CoverPathMixin:
method _hex_path (line 18) | def _hex_path(cls, pk: int) -> Path:
method get_cover_path (line 28) | def get_cover_path(cls, pk: int, *, custom: bool):
method get_cover_paths (line 35) | def get_cover_paths(cls, pks, *, custom: bool) -> set:
FILE: codex/librarian/covers/purge.py
class CoverPurgeThread (line 14) | class CoverPurgeThread(CoverCreateThread, ABC):
method _cleanup_cover_dirs (line 20) | def _cleanup_cover_dirs(cls, path, cover_root) -> None:
method purge_cover_paths (line 30) | def purge_cover_paths(self, cover_paths, cover_root) -> int:
method purge_comic_covers (line 51) | def purge_comic_covers(self, pks: frozenset[int], *, custom: bool) -> ...
method purge_all_comic_covers (line 57) | def purge_all_comic_covers(self, librarian_queue) -> None:
method _cleanup_orphan_covers (line 74) | def _cleanup_orphan_covers(self, cover_class, cover_root: Path, name: ...
method cleanup_orphan_covers (line 95) | def cleanup_orphan_covers(self) -> None:
FILE: codex/librarian/covers/status.py
class CoversStatus (line 8) | class CoversStatus(Status, ABC):
class CreateCoversStatus (line 14) | class CreateCoversStatus(CoversStatus):
class RemoveCoversStatus (line 21) | class RemoveCoversStatus(CoversStatus):
class FindOrphanCoversStatus (line 29) | class FindOrphanCoversStatus(CoversStatus):
FILE: codex/librarian/covers/tasks.py
class CoverTask (line 9) | class CoverTask(LibrarianTask):
class CoverRemoveAllTask (line 14) | class CoverRemoveAllTask(CoverTask):
class CoverRemoveOrphansTask (line 19) | class CoverRemoveOrphansTask(CoverTask):
class CoverRemoveTask (line 24) | class CoverRemoveTask(CoverTask):
class CoverSaveToCache (line 32) | class CoverSaveToCache(CoverTask):
class CoverCreateAllTask (line 40) | class CoverCreateAllTask(CoverTask):
FILE: codex/librarian/cron/crond.py
class CronThread (line 24) | class CronThread(NamedThread):
method __init__ (line 27) | def __init__(self, *args, **kwargs) -> None:
method _create_task_times (line 34) | def _create_task_times(self) -> None:
method _get_timeout (line 42) | def _get_timeout(self) -> int:
method _run_expired_jobs (line 53) | def _run_expired_jobs(self) -> None:
method run (line 63) | def run(self) -> None:
method end_timeout (line 81) | def end_timeout(self) -> None:
method stop (line 87) | def stop(self) -> None:
FILE: codex/librarian/fs/event_batcherd.py
class FSEventBatcherThread (line 45) | class FSEventBatcherThread(AggregateMessageQueuedThread):
method create_import_task_args (line 52) | def create_import_task_args(library_id: int) -> dict:
method _remove_paths (line 59) | def _remove_paths(args, deleted_key: str, moved_key: str) -> None:
method deduplicate_events (line 65) | def deduplicate_events(cls, args: dict) -> None:
method __init__ (line 87) | def __init__(self, *args, **kwargs) -> None:
method _args_field_by_event (line 94) | def _args_field_by_event(self, library_id: int, event: FSEvent):
method aggregate_items (line 106) | def aggregate_items(self, item) -> None:
method _set_check_metadata_mtime (line 126) | def _set_check_metadata_mtime(self, item) -> None:
method _start_poll (line 133) | def _start_poll(self, item) -> None:
method _finish_poll (line 137) | def _finish_poll(self, item) -> None:
method process_item (line 142) | def process_item(self, item) -> None:
method _subtract_args_items (line 152) | def _subtract_args_items(self, args) -> None:
method _create_task (line 159) | def _create_task(self, library_id) -> ImportTask:
method _send_import_task (line 168) | def _send_import_task(self, library_id: int) -> None:
method send_all_items (line 176) | def send_all_items(self) -> None:
FILE: codex/librarian/fs/events.py
class FSChange (line 9) | class FSChange(IntEnum):
class FSEvent (line 19) | class FSEvent:
method diff_key (line 29) | def diff_key(self) -> str:
FILE: codex/librarian/fs/filters.py
function _build_comic_matcher (line 16) | def _build_comic_matcher() -> re.Pattern:
function _match_suffix (line 39) | def _match_suffix(pattern: re.Pattern, path: Path) -> bool:
function match_comic (line 44) | def match_comic(path: Path) -> bool:
function match_image (line 49) | def match_image(path: Path) -> bool:
function match_folder_cover (line 54) | def match_folder_cover(path: Path) -> bool:
function match_group_cover_image (line 59) | def match_group_cover_image(path: Path) -> bool:
FILE: codex/librarian/fs/poller/events.py
class PollEventType (line 7) | class PollEventType(IntEnum):
class PollEvent (line 15) | class PollEvent:
FILE: codex/librarian/fs/poller/poller.py
class LibraryPollerThread (line 34) | class LibraryPollerThread(NamedThread, WorkerStatusMixin):
method __init__ (line 37) | def __init__(self, *args, **kwargs) -> None:
method wake (line 50) | def wake(self) -> None:
method poll (line 55) | def poll(self, task: FSPollLibrariesTask) -> None:
method stop (line 65) | def stop(self) -> None:
method _get_poll_timeout (line 76) | def _get_poll_timeout(self, library: Library) -> float | None: # noqa...
method _seconds_until_poll (line 115) | def _seconds_until_poll(library: Library) -> float:
method _get_diff (line 125) | def _get_diff(self, library: Library, *, force: bool) -> SnapshotDiff ...
method _queue_poll_events (line 146) | def _queue_poll_events(self, library: Library, *, force: bool) -> None:
method _poll_library (line 178) | def _poll_library(self, library: Library, *, force: bool) -> None:
method _get_min_timeout (line 199) | def _get_min_timeout(self) -> float | None:
method _poll_due_libraries (line 223) | def _poll_due_libraries(self, *, force: bool = False) -> None:
method _handle_pending_polls (line 237) | def _handle_pending_polls(self) -> None:
method run (line 255) | def run(self) -> None:
FILE: codex/librarian/fs/poller/snapshot.py
class Snapshot (line 19) | class Snapshot:
method __init__ (line 22) | def __init__(
method _inode (line 33) | def _inode(self, st: os.stat_result) -> tuple[int, int]:
method _set_lookups (line 38) | def _set_lookups(self, path: str, st: os.stat_result) -> None:
method paths (line 45) | def paths(self) -> frozenset[str]:
method inode (line 49) | def inode(self, path: str) -> tuple[int, int]:
method path (line 54) | def path(self, st_lookup: tuple[int, int]) -> str | None:
method mtime (line 58) | def mtime(self, path: str) -> float:
method size (line 62) | def size(self, path: str) -> int:
method is_dir (line 66) | def is_dir(self, path: str) -> bool:
method is_cover (line 70) | def is_cover(self, path: str) -> bool:
class DiskSnapshot (line 75) | class DiskSnapshot(Snapshot):
method __init__ (line 78) | def __init__(
method _init_walk (line 96) | def _init_walk(self):
method _walk (line 102) | def _walk(self, root: str) -> None:
class DatabaseSnapshot (line 124) | class DatabaseSnapshot(Snapshot):
method __init__ (line 131) | def __init__(
method _init_walk (line 147) | def _init_walk(self):
method _walk (line 164) | def _walk(root: str, models: tuple) -> Iterator:
method _create_stat (line 173) | def _create_stat(self, wp: dict, *, force: bool) -> os.stat_result:
FILE: codex/librarian/fs/poller/snapshot_diff.py
class _DiffData (line 37) | class _DiffData:
class SnapshotDiff (line 49) | class SnapshotDiff:
method _init_added (line 52) | def _init_added(self, data: _DiffData, snapshot: Snapshot):
method _init_deleted (line 61) | def _init_deleted(self, data: _DiffData, ref: Snapshot):
method _init_modified (line 70) | def _init_modified(self, data: _DiffData, snapshot: Snapshot):
method _init_moved (line 79) | def _init_moved(self, data: _DiffData, ref: Snapshot):
method __init__ (line 88) | def __init__(
method _is_inode_equal (line 128) | def _is_inode_equal(self, data: _DiffData, path: str) -> bool:
method _is_stats_equal (line 132) | def _is_stats_equal(self, data: _DiffData, old_path: str, new_path: st...
method _check_unchanged_for_inode_changes (line 138) | def _check_unchanged_for_inode_changes(self, data: _DiffData) -> None:
method _find_moved_paths (line 144) | def _find_moved_paths(self, data: _DiffData) -> None:
method _find_modified_paths (line 158) | def _find_modified_paths(self, data: _DiffData) -> None:
method is_empty (line 170) | def is_empty(self) -> bool:
method to_events (line 189) | def to_events(self) -> tuple[FSEvent, ...]:
FILE: codex/librarian/fs/poller/status.py
class FSPollStatus (line 6) | class FSPollStatus(FSStatus):
FILE: codex/librarian/fs/poller/tasks.py
class FSPollLibrariesTask (line 9) | class FSPollLibrariesTask(FSTask):
FILE: codex/librarian/fs/status.py
class FSStatus (line 8) | class FSStatus(Status, ABC):
FILE: codex/librarian/fs/tasks.py
class FSTask (line 11) | class FSTask(LibrarianTask):
class FSEventTask (line 16) | class FSEventTask(FSTask):
FILE: codex/librarian/fs/watcher/data.py
class ChangeBatch (line 9) | class ChangeBatch:
FILE: codex/librarian/fs/watcher/dirs.py
function _classify_added_file (line 20) | def _classify_added_file(path: Path, *, covers_only: bool) -> FSEvent | ...
function expand_dir_added (line 33) | def expand_dir_added(
function expand_dir_deleted (line 55) | def expand_dir_deleted(dir_path: str, library_pk: int, batch: ChangeBatc...
FILE: codex/librarian/fs/watcher/events.py
function _process_change (line 22) | def _process_change(
function _find_library (line 73) | def _find_library(
function process_changes (line 83) | def process_changes(
FILE: codex/librarian/fs/watcher/move.py
function _model_for_event (line 17) | def _model_for_event(event: FSEvent):
function _get_db_inode (line 26) | def _get_db_inode(event: FSEvent, library_pk: int) -> int | None:
function _get_disk_inode (line 39) | def _get_disk_inode(path: str) -> int | None:
function _detect_one_move (line 48) | def _detect_one_move(
function detect_moves (line 90) | def detect_moves(batch: ChangeBatch) -> list[tuple[int, FSEvent]]:
FILE: codex/librarian/fs/watcher/status.py
class FSWatcherRestartStatus (line 6) | class FSWatcherRestartStatus(FSStatus):
FILE: codex/librarian/fs/watcher/tasks.py
class FSWatcherRestartTask (line 9) | class FSWatcherRestartTask(FSTask):
FILE: codex/librarian/fs/watcher/watcher.py
class CodexWatchFilter (line 24) | class CodexWatchFilter:
method __init__ (line 27) | def __init__(self, covers_only_paths: set[str]):
method __call__ (line 31) | def __call__(self, change: Change, path: str) -> bool:
class LibraryWatcherThread (line 53) | class LibraryWatcherThread(NamedThread):
method __init__ (line 56) | def __init__(self, *args, **kwargs) -> None:
method _log_update_paths_from_db (line 65) | def _log_update_paths_from_db(self, new_paths_dict: dict[str, int]):
method _update_paths_from_db (line 78) | def _update_paths_from_db(self) -> None:
method restart (line 108) | def restart(self) -> None:
method _process_changes (line 122) | def _process_changes(self, changes: set[tuple[Change, str]]) -> None:
method _get_extant_paths (line 132) | def _get_extant_paths(self, paths: list[str]) -> list[str]:
method _watch_loop (line 152) | def _watch_loop(self) -> None:
method run (line 179) | def run(self) -> None:
method stop (line 187) | def stop(self) -> None:
FILE: codex/librarian/librariand.py
class LibrarianDaemon (line 59) | class LibrarianDaemon(Process):
method __init__ (line 62) | def __init__(self, logger_, queue: Queue, broadcast_queue: Queue) -> N...
method _restart_fs_watcher (line 80) | def _restart_fs_watcher(self) -> None:
method _restart_codex (line 83) | def _restart_codex(self, task: LibrarianTask) -> None:
method _process_task (line 87) | def _process_task(self, task) -> None:
method _create_threads (line 112) | def _create_threads(self) -> None:
method _start_threads (line 131) | def _start_threads(self) -> None:
method _startup (line 138) | def _startup(self) -> None:
method _stop_threads (line 146) | def _stop_threads(self) -> None:
method _join_threads (line 153) | def _join_threads(self) -> None:
method _shutdown (line 160) | def _shutdown(self) -> None:
method run (line 172) | def run(self) -> None:
method stop (line 195) | def stop(self) -> None:
FILE: codex/librarian/memory.py
function _get_cgroups2_mem_limit (line 18) | def _get_cgroups2_mem_limit() -> int:
function _get_cgroups1_mem_limit (line 24) | def _get_cgroups1_mem_limit() -> int | None:
function get_mem_limit (line 36) | def get_mem_limit(divisor="b"):
FILE: codex/librarian/notifier/notifierd.py
class NotifierThread (line 8) | class NotifierThread(AggregateMessageQueuedThread):
method __init__ (line 11) | def __init__(self, *args, broadcast_queue, **kwargs) -> None:
method aggregate_items (line 17) | def aggregate_items(self, item) -> None:
method _send_task (line 21) | def _send_task(self, task) -> None:
method send_all_items (line 38) | def send_all_items(self) -> None:
method stop (line 53) | def stop(self) -> None:
FILE: codex/librarian/notifier/tasks.py
class NotifierTask (line 11) | class NotifierTask(LibrarianTask):
FILE: codex/librarian/restarter/restarter.py
class CodexRestarter (line 19) | class CodexRestarter(WorkerStatusBase):
method _shutdown_codex (line 22) | def _shutdown_codex(
method shutdown_codex (line 36) | def shutdown_codex(self) -> None:
method restart_codex (line 40) | def restart_codex(self) -> None:
method handle_task (line 44) | def handle_task(self, task) -> None:
FILE: codex/librarian/restarter/status.py
class CodexRestarterStatus (line 8) | class CodexRestarterStatus(Status, ABC):
class CodexRestarterRestartStatus (line 12) | class CodexRestarterRestartStatus(CodexRestarterStatus):
class CodexRestarterStopStatus (line 22) | class CodexRestarterStopStatus(CodexRestarterStatus):
FILE: codex/librarian/restarter/tasks.py
class CodexRestarterTask (line 6) | class CodexRestarterTask(LibrarianTask):
class CodexRestartTask (line 10) | class CodexRestartTask(CodexRestarterTask):
class CodexShutdownTask (line 14) | class CodexShutdownTask(CodexRestarterTask):
FILE: codex/librarian/scribe/importer/const.py
function get_key_index (line 306) | def get_key_index(model: type[BaseModel]) -> int:
FILE: codex/librarian/scribe/importer/create/__init__.py
class CreateForeignKeysImporter (line 12) | class CreateForeignKeysImporter(CreateForeignKeysCreateUpdateImporter):
method create_and_update (line 15) | def create_and_update(self) -> None:
FILE: codex/librarian/scribe/importer/create/comics.py
class CreateComicsImporter (line 28) | class CreateComicsImporter(CreateForeignKeyLinksImporter):
method _populate_fts_attribute_values (line 31) | def _populate_fts_attribute_values(self, key: str, sub_key: str | int,...
method _update_comic_values (line 38) | def _update_comic_values(
method update_comics (line 59) | def update_comics(self) -> int:
method _bulk_create_comic (line 112) | def _bulk_create_comic(self, path: str, create_comics: list[Comic]) ->...
method create_comics (line 123) | def create_comics(self) -> int:
FILE: codex/librarian/scribe/importer/create/covers.py
class CreateCoversImporter (line 22) | class CreateCoversImporter(CreateComicsImporter):
method add_custom_cover_to_group (line 26) | def add_custom_cover_to_group(group_class, obj) -> None:
method update_custom_covers (line 38) | def update_custom_covers(self) -> int:
method create_custom_covers (line 82) | def create_custom_covers(self) -> int:
FILE: codex/librarian/scribe/importer/create/folders.py
class CreateForeignKeysFolderImporter (line 11) | class CreateForeignKeysFolderImporter(CreateCoversImporter):
method _get_parent_folder (line 14) | def _get_parent_folder(self, path: Path):
method _bulk_folders_create_add_folder (line 30) | def _bulk_folders_create_add_folder(self, path: Path, create_folders) ...
method _bulk_folders_create_depth_level (line 43) | def _bulk_folders_create_depth_level(self, paths, status: Status) -> int:
method bulk_folders_create (line 59) | def bulk_folders_create(self, folder_paths: frozenset, status) -> int:
method bulk_folders_update (line 86) | def bulk_folders_update(self, folder_paths: frozenset, status) -> int:
FILE: codex/librarian/scribe/importer/create/foreign_keys.py
class CreateForeignKeysCreateUpdateImporter (line 42) | class CreateForeignKeysCreateUpdateImporter(CreateForeignKeysFolderImpor...
method _get_create_update_args (line 46) | def _get_create_update_args(
method _add_custom_cover (line 77) | def _add_custom_cover(self, model, obj) -> None:
method _finish_create_update (line 80) | def _finish_create_update(self, model, objs, status: Status) -> None:
method _bulk_create_models (line 89) | def _bulk_create_models(
method bulk_create_all_models (line 141) | def bulk_create_all_models(self, status) -> int:
method _bulk_update_models (line 171) | def _bulk_update_models(self, model: type[BaseModel], status) -> int:
method bulk_update_all_models (line 216) | def bulk_update_all_models(self, status) -> int:
method create_all_fks (line 223) | def create_all_fks(self) -> int:
method update_all_fks (line 242) | def update_all_fks(self) -> int:
FILE: codex/librarian/scribe/importer/create/link_fks.py
class CreateForeignKeyLinksImporter (line 27) | class CreateForeignKeyLinksImporter(LinkComicsImporter):
method _get_comic_folder_fk_link (line 30) | def _get_comic_folder_fk_link(self, md, subkey: int | str, path: str) ...
method _get_comic_protagonist_fk_link (line 39) | def _get_comic_protagonist_fk_link(self, md, link_fks: dict[str, tuple...
method _get_comic_simple_fk_links (line 50) | def _get_comic_simple_fk_links(
method get_comic_fk_links (line 66) | def get_comic_fk_links(self, subkey: str | int, path: str) -> dict:
FILE: codex/librarian/scribe/importer/delete/__init__.py
class DeletedImporter (line 7) | class DeletedImporter(DeletedFoldersImporter):
method delete (line 10) | def delete(self) -> None:
FILE: codex/librarian/scribe/importer/delete/comics.py
class DeletedComicsImporter (line 13) | class DeletedComicsImporter(DeletedCoversImporter):
method _init_deleted_comic_groups (line 17) | def _init_deleted_comic_groups() -> dict:
method _populate_deleted_comic_group (line 29) | def _populate_deleted_comic_group(deleted_comic_groups, comic) -> None:
method _populate_deleted_comic_groups (line 45) | def _populate_deleted_comic_groups(cls, delete_qs, deleted_comic_group...
method bulk_comics_deleted (line 55) | def bulk_comics_deleted(self, **kwargs) -> tuple[int, dict]:
FILE: codex/librarian/scribe/importer/delete/covers.py
class DeletedCoversImporter (line 9) | class DeletedCoversImporter(SearchIndexImporter):
method remove_covers (line 12) | def remove_covers(self, delete_pks, *, custom: bool) -> None:
method bulk_covers_deleted (line 17) | def bulk_covers_deleted(self, **kwargs) -> int:
FILE: codex/librarian/scribe/importer/delete/folders.py
class DeletedFoldersImporter (line 9) | class DeletedFoldersImporter(DeletedComicsImporter):
method bulk_folders_deleted (line 12) | def bulk_folders_deleted(self, **kwargs) -> int:
FILE: codex/librarian/scribe/importer/failed/create.py
class FailedImportsCreateUpdateImporter (line 18) | class FailedImportsCreateUpdateImporter(FailedImportsQueryImporter):
method _bulk_update_failed_imports (line 21) | def _bulk_update_failed_imports(
method _bulk_create_failed_imports (line 58) | def _bulk_create_failed_imports(
FILE: codex/librarian/scribe/importer/failed/failed.py
class FailedImportsImporter (line 13) | class FailedImportsImporter(FailedImportsCreateUpdateImporter):
method _bulk_cleanup_failed_imports (line 16) | def _bulk_cleanup_failed_imports(
method fail_imports (line 39) | def fail_imports(self) -> None:
FILE: codex/librarian/scribe/importer/failed/query.py
class FailedImportsQueryImporter (line 21) | class FailedImportsQueryImporter(DeletedImporter):
method _query_failed_import_deletes (line 24) | def _query_failed_import_deletes(self, existing_failed_import_paths, n...
method _query_failed_imports (line 55) | def _query_failed_imports(
FILE: codex/librarian/scribe/importer/finish.py
class FinishImporter (line 37) | class FinishImporter(InitImporter):
method _get_log_finish_changed_text (line 40) | def _get_log_finish_changed_text(self, elapsed, elapsed_time) -> str:
method _log_finish (line 59) | def _log_finish(self) -> None:
method finish (line 69) | def finish(self) -> None:
FILE: codex/librarian/scribe/importer/importer.py
class ComicImporter (line 18) | class ComicImporter(MovedImporter):
method apply (line 21) | def apply(self) -> None:
FILE: codex/librarian/scribe/importer/init.py
class Counts (line 58) | class Counts:
method _any (line 76) | def _any(self, exclude_prefixes: tuple[str, ...]) -> bool:
method changed (line 83) | def changed(self):
method search_changed (line 87) | def search_changed(self):
class InitImporter (line 97) | class InitImporter(WorkerStatusBase):
method __init__ (line 100) | def __init__(
method _wait_for_filesystem_ops_to_finish (line 115) | def _wait_for_filesystem_ops_to_finish(self) -> bool:
method _log_task_construct_dirs_log (line 156) | def _log_task_construct_dirs_log(self) -> list:
method _log_task_construct_comics_log (line 167) | def _log_task_construct_comics_log(self) -> list:
method _log_task (line 180) | def _log_task(self) -> None:
method _init_librarian_status_moved (line 201) | def _init_librarian_status_moved(self, status_list) -> int:
method _init_if_modified_or_created (line 213) | def _init_if_modified_or_created(self, path, status_list) -> tuple:
method _init_librarian_status_deleted (line 280) | def _init_librarian_status_deleted(self, status_list) -> int:
method _init_librarian_status_search_index (line 299) | def _init_librarian_status_search_index(
method _init_librarian_status (line 309) | def _init_librarian_status(self, path) -> None:
method init_apply (line 324) | def init_apply(self) -> None:
FILE: codex/librarian/scribe/importer/link/__init__.py
class LinkComicsImporter (line 6) | class LinkComicsImporter(LinkManyToManyImporter):
method link (line 9) | def link(self) -> None:
FILE: codex/librarian/scribe/importer/link/covers.py
class LinkCoversImporter (line 14) | class LinkCoversImporter(FailedImportsImporter):
method _link_custom_cover_prepare (line 17) | def _link_custom_cover_prepare(self, cover, model_map) -> None:
method _link_custom_cover_group (line 40) | def _link_custom_cover_group(self, model, objs, status) -> None:
method link_custom_covers (line 48) | def link_custom_covers(self) -> int | None:
FILE: codex/librarian/scribe/importer/link/delete.py
class LinkImporterDelete (line 21) | class LinkImporterDelete(LinkComicsImporterPrepare):
method get_through_model (line 25) | def get_through_model(field: ManyToManyField) -> type[BaseModel]:
method _delete_m2m_field_batch (line 30) | def _delete_m2m_field_batch(
method _delete_m2m_fts_entries (line 48) | def _delete_m2m_fts_entries(self, field_name: str, comic_ids: set[int]...
method delete_m2m_field (line 60) | def delete_m2m_field(self, field_name: str, delete_m2ms: dict, status)...
method delete_m2ms (line 97) | def delete_m2ms(self, status) -> int:
FILE: codex/librarian/scribe/importer/link/many_to_many.py
class LinkManyToManyImporter (line 18) | class LinkManyToManyImporter(LinkSumImporter):
method link_comic_m2m_field (line 21) | def link_comic_m2m_field(self, field_name, m2m_links, status: Status) ...
method link_comic_m2m_fields (line 56) | def link_comic_m2m_fields(self) -> int:
FILE: codex/librarian/scribe/importer/link/prepare.py
class LinkComicsImporterPrepare (line 26) | class LinkComicsImporterPrepare(LinkCoversImporter):
method _get_link_folders_filter (line 30) | def _get_link_folders_filter(_field_name, values_set) -> Q:
method _get_link_complex_model_filter (line 36) | def _get_link_complex_model_filter(field_name, values_set) -> Q:
method _add_complex_link_to_fts (line 45) | def _add_complex_link_to_fts(
method _link_prepare_complex_m2ms (line 62) | def _link_prepare_complex_m2ms(
method _link_prepare_named_m2ms (line 83) | def _link_prepare_named_m2ms(
method link_prepare_m2m_links (line 105) | def link_prepare_m2m_links(self, status) -> Mapping:
FILE: codex/librarian/scribe/importer/link/sum.py
class LinkSumImporter (line 6) | class LinkSumImporter(LinkImporterDelete):
method sum_path_ops (line 9) | def sum_path_ops(self, key) -> int:
method sum_ops (line 17) | def sum_ops(self, key):
FILE: codex/librarian/scribe/importer/moved/__init__.py
class MovedImporter (line 13) | class MovedImporter(MovedFoldersImporter):
method _bulk_folders_modified (line 16) | def _bulk_folders_modified(self) -> int:
method move_and_modify_dirs (line 45) | def move_and_modify_dirs(self) -> None:
FILE: codex/librarian/scribe/importer/moved/comics.py
class MovedComicsImporter (line 21) | class MovedComicsImporter(ReadMetadataImporter):
method _bulk_comics_moved_ensure_folders (line 24) | def _bulk_comics_moved_ensure_folders(self) -> None:
method _prepare_moved_comic (line 52) | def _prepare_moved_comic(
method _bulk_comics_move_prepare (line 77) | def _bulk_comics_move_prepare(self) -> tuple[list, dict, dict]:
method bulk_comics_moved (line 99) | def bulk_comics_moved(self) -> int:
FILE: codex/librarian/scribe/importer/moved/covers.py
class MovedCoversImporter (line 17) | class MovedCoversImporter(MovedComicsImporter):
method _bulk_covers_moved_prepare (line 20) | def _bulk_covers_moved_prepare(self, status) -> tuple[list, set]:
method _bulk_covers_moved_unlink (line 44) | def _bulk_covers_moved_unlink(self, unlink_pks) -> None:
method bulk_covers_moved (line 63) | def bulk_covers_moved(self, status=None) -> int:
FILE: codex/librarian/scribe/importer/moved/folders.py
class MovedFoldersImporter (line 18) | class MovedFoldersImporter(MovedCoversImporter):
method _folder_sort_key (line 22) | def _folder_sort_key(element: Folder) -> int:
method _bulk_move_folders (line 25) | def _bulk_move_folders(
method _bulk_move_folders_under_existing_parents (line 66) | def _bulk_move_folders_under_existing_parents(
method _get_move_create_folders_one_layer (line 108) | def _get_move_create_folders_one_layer(
method _remove_move_collisions (line 135) | def _remove_move_collisions(self, dirs_moved: bidict[str, str]) -> None:
method _bulk_move_folders_and_create_parents (line 150) | def _bulk_move_folders_and_create_parents(self, status) -> int:
method bulk_folders_moved (line 189) | def bulk_folders_moved(self, *, mark_in_progress=False) -> int:
FILE: codex/librarian/scribe/importer/query/__init__.py
class QueryForeignKeysImporter (line 15) | class QueryForeignKeysImporter(QueryPruneLinks):
method query (line 18) | def query(self) -> None:
FILE: codex/librarian/scribe/importer/query/covers.py
class QueryCustomCoversImporter (line 18) | class QueryCustomCoversImporter(CreateForeignKeysImporter):
method query_missing_custom_covers (line 21) | def query_missing_custom_covers(self) -> None:
FILE: codex/librarian/scribe/importer/query/filters.py
class QueryForeignKeysFilterImporter (line 11) | class QueryForeignKeysFilterImporter(QueryCustomCoversImporter):
method _get_query_missing_simple_filter (line 15) | def _get_query_missing_simple_filter(
method _query_missing_complex_model_filter (line 26) | def _query_missing_complex_model_filter(
method query_missing_model_filter (line 46) | def query_missing_model_filter(
FILE: codex/librarian/scribe/importer/query/foreign_keys.py
class QueryForeignKeysQueryImporter (line 26) | class QueryForeignKeysQueryImporter(QueryIsUpdateImporter):
method query_existing_mds (line 29) | def query_existing_mds(
method _query_missing_models_batch (line 57) | def _query_missing_models_batch(
method _finish_query_missing (line 95) | def _finish_query_missing(
method query_missing_models (line 126) | def query_missing_models(
method _query_missing_model (line 165) | def _query_missing_model(self, model: type[BaseModel], status: Status)...
method _set_fk_totals (line 179) | def _set_fk_totals(self, fk_key: str, status_class) -> None:
method query_all_missing_models (line 188) | def query_all_missing_models(self):
FILE: codex/librarian/scribe/importer/query/links.py
class QueryPruneLinks (line 11) | class QueryPruneLinks(QueryPruneLinksM2M):
method query_prune_comic_links (line 14) | def query_prune_comic_links(self) -> None:
FILE: codex/librarian/scribe/importer/query/links_fk.py
class QueryPruneLinksFKs (line 19) | class QueryPruneLinksFKs(QueryUpdateComics):
method pop_links_to_fts (line 22) | def pop_links_to_fts(self, path, field_name) -> None:
method _query_prune_comic_fk_links_protagonist (line 27) | def _query_prune_comic_fk_links_protagonist(
method _query_prune_comic_fk_links_key_equal (line 37) | def _query_prune_comic_fk_links_key_equal(field_obj, key_rel, key_valu...
method _query_prune_comic_fk_links_field (line 46) | def _query_prune_comic_fk_links_field(self, comic, path, field_name) -...
method _query_prune_comic_fk_links_comic (line 66) | def _query_prune_comic_fk_links_comic(self, comic, status) -> None:
method _query_prune_comic_fk_links_batch (line 82) | def _query_prune_comic_fk_links_batch(
method query_prune_comic_fk_links (line 93) | def query_prune_comic_fk_links(self, status) -> None:
FILE: codex/librarian/scribe/importer/query/links_m2m.py
class QueryPruneLinksM2M (line 19) | class QueryPruneLinksM2M(QueryPruneLinksFKs):
method _m2m_obj_to_key_tuple (line 23) | def _m2m_obj_to_key_tuple(key_attrs: tuple[str, ...], m2m_obj: BaseMod...
method _query_prune_comic_m2m_links_field_obj (line 33) | def _query_prune_comic_m2m_links_field_obj(
method _query_prune_comic_m2m_links_field (line 57) | def _query_prune_comic_m2m_links_field(
method _query_prune_comic_m2m_links_comic (line 78) | def _query_prune_comic_m2m_links_comic(self, comic: Comic, status) -> ...
method _query_prune_comic_m2m_links_batch (line 85) | def _query_prune_comic_m2m_links_batch(self, paths: tuple[str], status...
method query_prune_comic_m2m_links (line 94) | def query_prune_comic_m2m_links(self, status) -> None:
FILE: codex/librarian/scribe/importer/query/update_comics.py
class QueryUpdateComics (line 20) | class QueryUpdateComics(QueryForeignKeysQueryImporter):
method _query_update_comic (line 23) | def _query_update_comic(self, comic: Comic, status: Status) -> None:
method query_update_comics (line 41) | def query_update_comics(self) -> None:
FILE: codex/librarian/scribe/importer/query/update_fks.py
class QueryIsUpdateImporter (line 12) | class QueryIsUpdateImporter(QueryForeignKeysFilterImporter):
method _query_missing_models_is_do_update_identifier (line 16) | def _query_missing_models_is_do_update_identifier(
method _query_missing_models_is_do_update_extra (line 43) | def _query_missing_models_is_do_update_extra(
method _query_normalize_existing_values (line 70) | def _query_normalize_existing_values(
method _query_update_init_best_and_existing_values (line 80) | def _query_update_init_best_and_existing_values(
method query_model_best_extra_values (line 101) | def query_model_best_extra_values(
FILE: codex/librarian/scribe/importer/read/__init__.py
class ReadMetadataImporter (line 8) | class ReadMetadataImporter(ExtractMetadataImporter):
method read (line 11) | def read(self) -> None:
FILE: codex/librarian/scribe/importer/read/aggregate_path.py
class AggregateMetadataImporter (line 79) | class AggregateMetadataImporter(AggregatePathMetadataImporter):
method _transform_metadata (line 83) | def _transform_metadata(md) -> None:
method _aggregate_path (line 102) | def _aggregate_path(self, path, status) -> None:
method aggregate_metadata (line 120) | def aggregate_metadata(
FILE: codex/librarian/scribe/importer/read/extract.py
class ExtractMetadataImporter (line 25) | class ExtractMetadataImporter(AggregateMetadataImporter):
method _old_comic_values (line 29) | def _old_comic_values(
method _set_import_metadata_flag (line 42) | def _set_import_metadata_flag(self) -> bool:
method _extract_path_comicbox (line 53) | def _extract_path_comicbox(
method _extract_path (line 91) | def _extract_path(
method _get_all_old_comic_values (line 120) | def _get_all_old_comic_values(all_paths: frozenset[str]) -> MappingPro...
method extract_metadata (line 131) | def extract_metadata(self, status=None) -> int:
FILE: codex/librarian/scribe/importer/read/folders.py
class AggregatePathMetadataImporter (line 17) | class AggregatePathMetadataImporter(AggregateManyToManyMetadataImporter):
method get_all_library_relative_paths (line 20) | def get_all_library_relative_paths(
method get_path_metadata (line 34) | def get_path_metadata(self, md: dict, path: Path | str) -> None:
FILE: codex/librarian/scribe/importer/read/foreign_keys.py
class AggregateForeignKeyMetadataImporter (line 40) | class AggregateForeignKeyMetadataImporter(QueryForeignKeysImporter):
method add_query_model (line 43) | def add_query_model(
method get_identifier_tuple (line 60) | def get_identifier_tuple(
method _set_simple_fk (line 95) | def _set_simple_fk(self, related_field: Field, value) -> tuple:
method _set_group_tree_group (line 101) | def _set_group_tree_group(
method get_fk_metadata (line 134) | def get_fk_metadata(self, md, path) -> None:
FILE: codex/librarian/scribe/importer/read/many_to_many.py
class AggregateManyToManyMetadataImporter (line 35) | class AggregateManyToManyMetadataImporter(AggregateForeignKeyMetadataImp...
method _get_m2m_metadata_dict_model_aggregate_sub_sub_value_identifiers (line 38) | def _get_m2m_metadata_dict_model_aggregate_sub_sub_value_identifiers(
method _get_m2m_metadata_dict_model_aggregate_sub_sub_value_roles (line 45) | def _get_m2m_metadata_dict_model_aggregate_sub_sub_value_roles(
method _get_m2m_metadata_dict_model_aggregate_sub_sub_value (line 61) | def _get_m2m_metadata_dict_model_aggregate_sub_sub_value(
method _get_m2m_metadata_aggregate_sub_values_init (line 95) | def _get_m2m_metadata_aggregate_sub_values_init(
method _get_roles_or_numbers (line 129) | def _get_roles_or_numbers(
method _create_clean_sub_map (line 157) | def _create_clean_sub_map(
method _get_m2m_metadata_dict_model_aggregate_sub_values (line 192) | def _get_m2m_metadata_dict_model_aggregate_sub_values(
method _get_m2m_metadata_dict_model (line 221) | def _get_m2m_metadata_dict_model(
method _get_m2m_metadata_for_field (line 248) | def _get_m2m_metadata_for_field(self, field, md, m2m_md) -> None:
method get_m2m_metadata (line 268) | def get_m2m_metadata(self, md, path) -> None:
FILE: codex/librarian/scribe/importer/search/__init__.py
class SearchIndexImporter (line 16) | class SearchIndexImporter(SearchIndexPrepareImporter):
method clean_fts (line 19) | def clean_fts(self) -> int:
method full_text_search (line 26) | def full_text_search(self) -> None:
FILE: codex/librarian/scribe/importer/search/prepare.py
class SearchIndexPrepareImporter (line 19) | class SearchIndexPrepareImporter(SearchIndexCreateUpdateImporter):
method minify_complex_link_to_fts_tuple (line 23) | def minify_complex_link_to_fts_tuple(
method _to_fts_tuple (line 34) | def _to_fts_tuple(values) -> tuple:
method add_to_fts_existing (line 39) | def add_to_fts_existing(self, pk: int, field_name: str, values: tuple)...
method add_links_to_fts (line 51) | def add_links_to_fts(
FILE: codex/librarian/scribe/importer/search/sync_m2m.py
class SearchIndexSyncManyToManyImporter (line 14) | class SearchIndexSyncManyToManyImporter(FinishImporter):
method _to_fts_str (line 18) | def _to_fts_str(values) -> str:
method _get_fts_m2m_concat (line 22) | def _get_fts_m2m_concat(field_name: str) -> Concat | GroupConcat:
method _sync_fts_for_m2m_updates_model (line 44) | def _sync_fts_for_m2m_updates_model(
method sync_fts_for_m2m_updates (line 69) | def sync_fts_for_m2m_updates(
FILE: codex/librarian/scribe/importer/search/update.py
class SearchIndexCreateUpdateImporter (line 27) | class SearchIndexCreateUpdateImporter(SearchIndexSyncManyToManyImporter):
method _create_comicfts_entry (line 30) | def _create_comicfts_entry(
method _update_comicfts_entry (line 46) | def _update_comicfts_entry(
method _update_search_index_operate_get_status (line 68) | def _update_search_index_operate_get_status(
method _update_search_index_operate_create (line 74) | def _update_search_index_operate_create(
method _update_search_index_operate_update (line 87) | def _update_search_index_operate_update(
method _update_search_index_create_or_update (line 105) | def _update_search_index_create_or_update(
method _update_search_index_operate (line 126) | def _update_search_index_operate(self, *, create: bool) -> int:
method _update_search_index_update (line 169) | def _update_search_index_update(self) -> int:
method _update_search_index_create (line 173) | def _update_search_index_create(self) -> int:
method _update_search_index (line 177) | def _update_search_index(self, cleaned_count: int) -> None:
method import_search_index (line 202) | def import_search_index(self, cleaned_count: int) -> None:
FILE: codex/librarian/scribe/importer/statii/create.py
class ImporterCreateStatus (line 8) | class ImporterCreateStatus(ImporterStatus, ABC):
class ImporterCreateTagsStatus (line 12) | class ImporterCreateTagsStatus(ImporterCreateStatus):
class ImporterUpdateTagsStatus (line 20) | class ImporterUpdateTagsStatus(ImporterCreateStatus):
class ImporterCreateComicsStatus (line 28) | class ImporterCreateComicsStatus(ImporterCreateStatus):
class ImporterUpdateComicsStatus (line 36) | class ImporterUpdateComicsStatus(ImporterCreateStatus):
class ImporterCreateCoversStatus (line 44) | class ImporterCreateCoversStatus(ImporterCreateStatus):
class ImporterUpdateCoversStatus (line 52) | class ImporterUpdateCoversStatus(ImporterCreateStatus):
FILE: codex/librarian/scribe/importer/statii/delete.py
class ImporterRemoveStatus (line 8) | class ImporterRemoveStatus(ImporterStatus, ABC):
class ImporterRemoveFoldersStatus (line 14) | class ImporterRemoveFoldersStatus(ImporterRemoveStatus):
class ImporterRemoveComicsStatus (line 21) | class ImporterRemoveComicsStatus(ImporterRemoveStatus):
class ImporterRemoveCoversStatus (line 28) | class ImporterRemoveCoversStatus(ImporterRemoveStatus):
FILE: codex/librarian/scribe/importer/statii/failed.py
class ImporterFailedImportStatus (line 8) | class ImporterFailedImportStatus(ImporterStatus, ABC):
class ImporterFailedImportsQueryStatus (line 14) | class ImporterFailedImportsQueryStatus(ImporterFailedImportStatus):
class ImporterFailedImportsUpdateStatus (line 22) | class ImporterFailedImportsUpdateStatus(ImporterFailedImportStatus):
class ImporterFailedImportsCreateStatus (line 29) | class ImporterFailedImportsCreateStatus(ImporterFailedImportStatus):
class ImporterFailedImportsDeleteStatus (line 37) | class ImporterFailedImportsDeleteStatus(ImporterFailedImportStatus):
FILE: codex/librarian/scribe/importer/statii/link.py
class ImporterLinkStatus (line 6) | class ImporterLinkStatus(ImporterStatus):
class ImporterLinkTagsStatus (line 13) | class ImporterLinkTagsStatus(ImporterLinkStatus):
class ImporterLinkCoversStatus (line 20) | class ImporterLinkCoversStatus(ImporterLinkStatus):
FILE: codex/librarian/scribe/importer/statii/moved.py
class ImporterMoveStatus (line 8) | class ImporterMoveStatus(ImporterStatus, ABC):
class ImporterMoveFoldersStatus (line 14) | class ImporterMoveFoldersStatus(ImporterMoveStatus):
class ImporterMoveComicsStatus (line 21) | class ImporterMoveComicsStatus(ImporterMoveStatus):
class ImporterMoveCoversStatus (line 28) | class ImporterMoveCoversStatus(ImporterMoveStatus):
FILE: codex/librarian/scribe/importer/statii/query.py
class ImporterQueryStatus (line 6) | class ImporterQueryStatus(ImporterStatus):
class ImporterQueryMissingTagsStatus (line 13) | class ImporterQueryMissingTagsStatus(ImporterQueryStatus):
class ImporterQueryComicUpdatesStatus (line 20) | class ImporterQueryComicUpdatesStatus(ImporterQueryStatus):
class ImporterQueryTagLinksStatus (line 27) | class ImporterQueryTagLinksStatus(ImporterQueryStatus):
class ImporterQueryMissingCoversStatus (line 34) | class ImporterQueryMissingCoversStatus(ImporterQueryStatus):
FILE: codex/librarian/scribe/importer/statii/read.py
class ImporterReadStatus (line 8) | class ImporterReadStatus(ImporterStatus, ABC):
class ImporterReadComicsStatus (line 14) | class ImporterReadComicsStatus(ImporterReadStatus):
class ImporterAggregateStatus (line 22) | class ImporterAggregateStatus(ImporterReadStatus):
FILE: codex/librarian/scribe/importer/statii/search.py
class ImporterFTSStatus (line 8) | class ImporterFTSStatus(ImporterStatus, ABC):
class ImporterFTSUpdateStatus (line 14) | class ImporterFTSUpdateStatus(ImporterFTSStatus):
class ImporterFTSCreateStatus (line 21) | class ImporterFTSCreateStatus(ImporterFTSStatus):
FILE: codex/librarian/scribe/importer/status.py
class ImporterStatus (line 8) | class ImporterStatus(ScribeStatus, ABC):
FILE: codex/librarian/scribe/importer/tasks.py
class ImportTask (line 10) | class ImportTask(ScribeTask):
method total (line 35) | def total(self) -> int:
FILE: codex/librarian/scribe/janitor/adopt_folders.py
class OrphanFolderAdopter (line 15) | class OrphanFolderAdopter(WorkerStatusAbortableBase):
method _adopt_orphan_folders_for_library (line 18) | def _adopt_orphan_folders_for_library(self, library) -> tuple | tuple[...
method adopt_orphan_folders (line 48) | def adopt_orphan_folders(self) -> None:
FILE: codex/librarian/scribe/janitor/cleanup.py
function _create_reverse_rel_map_for_model (line 80) | def _create_reverse_rel_map_for_model(model, rel_map) -> None:
function _create_reverse_rel_map (line 101) | def _create_reverse_rel_map() -> MappingProxyType:
class JanitorCleanup (line 117) | class JanitorCleanup(JanitorUpdateFailedImports):
method _cleanup_fks_model (line 120) | def _cleanup_fks_model(self, model, filter_dict, status):
method _cleanup_fks_one_level (line 129) | def _cleanup_fks_one_level(self, status) -> int:
method cleanup_fks (line 137) | def cleanup_fks(self) -> None:
method cleanup_custom_covers (line 156) | def cleanup_custom_covers(self) -> None:
method cleanup_sessions (line 174) | def cleanup_sessions(self) -> None:
method cleanup_orphan_bookmarks (line 196) | def cleanup_orphan_bookmarks(self) -> None:
method cleanup_orphan_settings (line 208) | def cleanup_orphan_settings(self) -> None:
FILE: codex/librarian/scribe/janitor/failed_imports.py
class JanitorUpdateFailedImports (line 9) | class JanitorUpdateFailedImports(JanitorVacuum):
method _force_update_failed_imports (line 12) | def _force_update_failed_imports(self, library_id) -> None:
method force_update_all_failed_imports (line 22) | def force_update_all_failed_imports(self) -> None:
FILE: codex/librarian/scribe/janitor/integrity/__init__.py
function _exec_sql (line 21) | def _exec_sql(sql):
function _is_integrity_ok (line 31) | def _is_integrity_ok(results) -> bool:
function integrity_check (line 37) | def integrity_check(log, *, long: bool) -> None:
function fts_rebuild (line 55) | def fts_rebuild() -> None:
function fts_integrity_check (line 61) | def fts_integrity_check(log) -> bool:
class JanitorIntegrity (line 80) | class JanitorIntegrity(WorkerStatusAbortableBase):
method foreign_key_check (line 83) | def foreign_key_check(self) -> None:
method integrity_check (line 93) | def integrity_check(self, *, long: bool) -> None:
method fts_rebuild (line 104) | def fts_rebuild(self) -> None:
method fts_integrity_check (line 114) | def fts_integrity_check(self) -> None:
FILE: codex/librarian/scribe/janitor/integrity/foreign_keys.py
function _get_fk_column_name (line 16) | def _get_fk_column_name(cursor, table_name: str, fkid: int) -> str | None:
function _is_column_nullable (line 26) | def _is_column_nullable(cursor, table_name: str, column_name: str) -> bool:
function _collect_comic_ids_for_table (line 36) | def _collect_comic_ids_for_table(cursor, table_name: str, rowids: set) -...
function _mark_comics_for_update (line 57) | def _mark_comics_for_update(fix_comic_pks, log) -> None:
function _group_fk_violations (line 86) | def _group_fk_violations(
function _fix_fk_violations (line 120) | def _fix_fk_violations(
function fix_foreign_keys (line 162) | def fix_foreign_keys(log) -> None:
FILE: codex/librarian/scribe/janitor/janitor.py
class Janitor (line 106) | class Janitor(JanitorCodexUpdate):
method queue_nightly_tasks (line 109) | def queue_nightly_tasks(self) -> None:
method handle_task (line 119) | def handle_task(self, task) -> None:
FILE: codex/librarian/scribe/janitor/scheduled_time.py
function get_janitor_time (line 9) | def get_janitor_time(_log: Logger) -> datetime:
FILE: codex/librarian/scribe/janitor/status.py
class JanitorStatus (line 8) | class JanitorStatus(ScribeStatus, ABC):
class JanitorAdoptOrphanFoldersStatus (line 12) | class JanitorAdoptOrphanFoldersStatus(JanitorStatus):
class JanitorCleanupTagsStatus (line 21) | class JanitorCleanupTagsStatus(JanitorStatus):
class JanitorCodexLatestVersionStatus (line 30) | class JanitorCodexLatestVersionStatus(JanitorStatus):
class JanitorCodexUpdateStatus (line 40) | class JanitorCodexUpdateStatus(JanitorStatus):
class JanitorDBOptimizeStatus (line 50) | class JanitorDBOptimizeStatus(JanitorStatus):
class JanitorDBBackupStatus (line 60) | class JanitorDBBackupStatus(JanitorStatus):
class JanitorCleanupSessionsStatus (line 70) | class JanitorCleanupSessionsStatus(JanitorStatus):
class JanitorCleanupCoversStatus (line 79) | class JanitorCleanupCoversStatus(JanitorStatus):
class JanitorCleanupBookmarksStatus (line 88) | class JanitorCleanupBookmarksStatus(JanitorStatus):
class JanitorCleanupSettingsStatus (line 97) | class JanitorCleanupSettingsStatus(JanitorStatus):
class JanitorDBFKIntegrityStatus (line 106) | class JanitorDBFKIntegrityStatus(JanitorStatus):
class JanitorDBIntegrityStatus (line 116) | class JanitorDBIntegrityStatus(JanitorStatus):
class JanitorDBFTSIntegrityStatus (line 126) | class JanitorDBFTSIntegrityStatus(JanitorStatus):
class JanitorDBFTSRebuildStatus (line 136) | class JanitorDBFTSRebuildStatus(JanitorStatus):
FILE: codex/librarian/scribe/janitor/tasks.py
class JanitorTask (line 8) | class JanitorTask(ScribeTask):
class JanitorCodexUpdateTask (line 13) | class JanitorCodexUpdateTask(JanitorTask):
class JanitorAdoptOrphanFoldersTask (line 19) | class JanitorAdoptOrphanFoldersTask(JanitorTask):
class JanitorBackupTask (line 23) | class JanitorBackupTask(JanitorTask):
class JanitorVacuumTask (line 27) | class JanitorVacuumTask(JanitorTask):
class JanitorCleanFKsTask (line 31) | class JanitorCleanFKsTask(JanitorTask):
class JanitorCleanCoversTask (line 35) | class JanitorCleanCoversTask(JanitorTask):
class JanitorCleanupSessionsTask (line 39) | class JanitorCleanupSessionsTask(JanitorTask):
class JanitorCleanupBookmarksTask (line 43) | class JanitorCleanupBookmarksTask(JanitorTask):
class JanitorCleanupSettingsTask (line 47) | class JanitorCleanupSettingsTask(JanitorTask):
class JanitorForeignKeyCheckTask (line 51) | class JanitorForeignKeyCheckTask(JanitorTask):
class JanitorImportForceAllFailedTask (line 55) | class JanitorImportForceAllFailedTask(JanitorTask):
class JanitorIntegrityCheckTask (line 60) | class JanitorIntegrityCheckTask(JanitorTask):
class JanitorFTSIntegrityCheckTask (line 66) | class JanitorFTSIntegrityCheckTask(JanitorTask):
class JanitorFTSRebuildTask (line 70) | class JanitorFTSRebuildTask(JanitorTask):
class JanitorNightlyTask (line 74) | class JanitorNightlyTask(JanitorTask):
FILE: codex/librarian/scribe/janitor/update.py
class JanitorCodexUpdate (line 17) | class JanitorCodexUpdate(JanitorCleanup):
method _is_outdated (line 20) | def _is_outdated(self) -> bool:
method _update_codex (line 42) | def _update_codex(self, *, force: bool) -> None:
method update_codex (line 68) | def update_codex(self, *, force: bool) -> None:
FILE: codex/librarian/scribe/janitor/vacuum.py
class JanitorVacuum (line 16) | class JanitorVacuum(JanitorIntegrity):
method vacuum_db (line 19) | def vacuum_db(self) -> None:
method backup_db (line 35) | def backup_db(self, backup_path=BACKUP_DB_PATH, *, show_status: bool) ...
FILE: codex/librarian/scribe/lazy_importer.py
class LazyImporter (line 10) | class LazyImporter(WorkerBase):
method lazy_import (line 13) | def lazy_import(self, task) -> None:
FILE: codex/librarian/scribe/priority.py
function get_task_priority (line 67) | def get_task_priority(task: ScribeTask) -> tuple[int, float]:
FILE: codex/librarian/scribe/scribed.py
class ScribeThread (line 40) | class ScribeThread(QueuedThread):
method __init__ (line 45) | def __init__(self, *args, **kwargs) -> None:
method process_item (line 53) | def process_item(self, item) -> None:
method put (line 103) | def put(self, task) -> None:
FILE: codex/librarian/scribe/search/handler.py
class SearchIndexer (line 13) | class SearchIndexer(SearchIndexerSync):
method handle_task (line 16) | def handle_task(self, task: SearchIndexerTask) -> None:
FILE: codex/librarian/scribe/search/optimize.py
class SearchIndexerOptimize (line 14) | class SearchIndexerOptimize(WorkerStatusAbortableBase):
method optimize (line 17) | def optimize(self) -> None:
FILE: codex/librarian/scribe/search/prepare.py
class SearchEntryPrepare (line 47) | class SearchEntryPrepare:
method _get_entry_str_value (line 51) | def _get_entry_str_value(entry: dict, key: str) -> str:
method _get_sources_fts_field (line 62) | def _get_sources_fts_field(entry: dict) -> str:
method _get_pycountry_fts_field (line 80) | def _get_pycountry_fts_field(cls, entry, field_name) -> str:
method _create_comicfts_entry_attributes (line 88) | def _create_comicfts_entry_attributes(cls, entry, *, create: bool) -> ...
method _create_comicfts_entry_fks (line 95) | def _create_comicfts_entry_fks(cls, entry) -> None:
method _create_comicfts_entry_m2ms (line 100) | def _create_comicfts_entry_m2ms(cls, entry, existing_values: dict | No...
method prepare_import_fts_entry (line 109) | def prepare_import_fts_entry(
method prepare_sync_fts_entry (line 141) | def prepare_sync_fts_entry(
FILE: codex/librarian/scribe/search/remove.py
class SearchIndexerRemove (line 13) | class SearchIndexerRemove(SearchIndexerOptimize):
method clear_search_index (line 16) | def clear_search_index(self) -> None:
method _remove_stale_records (line 23) | def _remove_stale_records(self, status):
method remove_stale_records (line 38) | def remove_stale_records(self, *, log_success: bool = True) -> int:
method remove_duplicate_records (line 50) | def remove_duplicate_records(self) -> int:
FILE: codex/librarian/scribe/search/status.py
class SearchIndexStatus (line 8) | class SearchIndexStatus(ScribeStatus, ABC):
class SearchIndexClearStatus (line 12) | class SearchIndexClearStatus(SearchIndexStatus):
class SearchIndexCleanStatus (line 23) | class SearchIndexCleanStatus(SearchIndexStatus):
class SearchIndexOptimizeStatus (line 32) | class SearchIndexOptimizeStatus(SearchIndexStatus):
class SearchIndexSyncUpdateStatus (line 42) | class SearchIndexSyncUpdateStatus(SearchIndexStatus):
class SearchIndexSyncCreateStatus (line 51) | class SearchIndexSyncCreateStatus(SearchIndexStatus):
FILE: codex/librarian/scribe/search/sync.py
class SearchIndexerSync (line 78) | class SearchIndexerSync(SearchIndexerRemove):
method _init_statuses (line 81) | def _init_statuses(self, rebuild) -> None:
method _update_search_index_clean (line 98) | def _update_search_index_clean(self, rebuild) -> None:
method _select_related_fts_query (line 107) | def _select_related_fts_query(qs):
method _prefetch_related_fts_query (line 119) | def _prefetch_related_fts_query(qs):
method _annotate_fts_query (line 139) | def _annotate_fts_query(qs):
method _update_search_index_operate_get_status (line 156) | def _update_search_index_operate_get_status(
method _update_search_index_create_or_update (line 165) | def _update_search_index_create_or_update(
method _get_operation_comics_query (line 189) | def _get_operation_comics_query(qs, *, create: bool):
method _update_search_index_operate (line 194) | def _update_search_index_operate(
method _update_search_index_update (line 257) | def _update_search_index_update(self):
method _update_search_index_create (line 274) | def _update_search_index_create(self):
method _update_search_index (line 286) | def _update_search_index(self, *, rebuild: bool) -> None:
method update_search_index (line 318) | def update_search_index(self, *, rebuild: bool) -> None:
FILE: codex/librarian/scribe/search/tasks.py
class SearchIndexerTask (line 8) | class SearchIndexerTask(ScribeTask):
class SearchIndexSyncTask (line 13) | class SearchIndexSyncTask(SearchIndexerTask):
class SearchIndexOptimizeTask (line 19) | class SearchIndexOptimizeTask(SearchIndexerTask):
class SearchIndexCleanStaleTask (line 23) | class SearchIndexCleanStaleTask(SearchIndexerTask):
class SearchIndexClearTask (line 27) | class SearchIndexClearTask(SearchIndexerTask):
FILE: codex/librarian/scribe/status.py
class ScribeStatus (line 8) | class ScribeStatus(Status, ABC):
class UpdateGroupTimestampsStatus (line 12) | class UpdateGroupTimestampsStatus(ScribeStatus):
FILE: codex/librarian/scribe/tasks.py
class ScribeTask (line 9) | class ScribeTask(LibrarianTask):
class UpdateGroupsTask (line 14) | class UpdateGroupsTask(ScribeTask):
class LazyImportComicsTask (line 21) | class LazyImportComicsTask(ScribeTask):
class ImportAbortTask (line 28) | class ImportAbortTask(ScribeTask):
class SearchIndexSyncAbortTask (line 32) | class SearchIndexSyncAbortTask(ScribeTask):
class CleanupAbortTask (line 36) | class CleanupAbortTask(ScribeTask):
FILE: codex/librarian/scribe/timestamp_update.py
class TimestampUpdater (line 23) | class TimestampUpdater(WorkerStatusBase):
method _get_update_filter (line 27) | def _get_update_filter(
method _add_child_count_filter (line 51) | def _add_child_count_filter(qs: QuerySet, model: type[BrowserGroupMode...
method _update_group_model (line 59) | def _update_group_model(
method update_library_groups (line 88) | def update_library_groups(
method update_groups (line 120) | def update_groups(self, task) -> None:
FILE: codex/librarian/status.py
class Status (line 12) | class Status(ABC):
method title (line 30) | def title(cls) -> str:
method verbed (line 39) | def verbed(cls) -> str:
method increment_complete (line 45) | def increment_complete(self, count: int = 1) -> None:
method decrement_total (line 49) | def decrement_total(self) -> None:
method start (line 53) | def start(self) -> None:
method _elapsed (line 57) | def _elapsed(self):
method elapsed (line 60) | def elapsed(self) -> str:
method per_second (line 64) | def per_second(self) -> str:
method reset (line 72) | def reset(self) -> None:
FILE: codex/librarian/status_controller.py
function get_default (line 20) | def get_default(field):
class StatusController (line 29) | class StatusController:
method __init__ (line 34) | def __init__(self, logger_: Logger, librarian_queue: Queue) -> None:
method _enqueue_notifier_task (line 39) | def _enqueue_notifier_task(self, *, notify: bool = True) -> None:
method _loggit (line 45) | def _loggit(self, level: str, status: Status) -> None:
method _update (line 58) | def _update(
method start (line 86) | def start(
method start_many (line 97) | def start_many(self, statii: Iterable[Status | type[Status]]) -> None:
method update (line 107) | def update(self, status: Status, *, notify: bool = True) -> None:
method _log_finish (line 114) | def _log_finish(self, status: Status) -> None:
method _finish_many_log (line 140) | def _finish_many_log(self, updated_statii, *, is_positive_statii: bool):
method finish_many (line 149) | def finish_many(
method finish (line 190) | def finish(self, status: Status | None, *, notify: bool = True) -> None:
FILE: codex/librarian/tasks.py
class LibrarianTask (line 6) | class LibrarianTask(ABC): # noqa: B024
class LibrarianShutdownTask (line 10) | class LibrarianShutdownTask(LibrarianTask):
class WakeCronTask (line 14) | class WakeCronTask(LibrarianTask):
FILE: codex/librarian/telemeter/scheduled_time.py
function _get_utc_start_of_week (line 19) | def _get_utc_start_of_week():
function _is_created_recently (line 30) | def _is_created_recently(ts) -> bool:
function _get_scheduled_time (line 37) | def _get_scheduled_time(ts) -> int | datetime:
function get_telemeter_time (line 52) | def get_telemeter_time(log: Logger) -> int | datetime:
FILE: codex/librarian/telemeter/stats.py
class CodexStats (line 47) | class CodexStats:
method __init__ (line 50) | def __init__(self, params=None) -> None:
method _is_docker (line 57) | def _is_docker(cls) -> bool:
method _get_models (line 64) | def _get_models(self, key) -> tuple:
method _get_model_counts (line 79) | def _get_model_counts(self, key) -> dict:
method _aggregate_settings_instance (line 92) | def _aggregate_settings_instance(instance, subkeys, user_stats) -> None:
method _get_session_stats (line 104) | def _get_session_stats(cls) -> tuple[dict, int]:
method _add_platform (line 122) | def _add_platform(self, obj) -> None:
method _add_config (line 139) | def _add_config(self, obj) -> None:
method _add_groups (line 150) | def _add_groups(self, obj) -> None:
method _add_file_types (line 158) | def _add_file_types(self, obj) -> None:
method _add_metadata (line 175) | def _add_metadata(self, obj) -> None:
method get (line 182) | def get(self) -> dict:
FILE: codex/librarian/telemeter/tasks.py
class TelemeterTask (line 9) | class TelemeterTask(BookmarkTask):
FILE: codex/librarian/telemeter/telemeter.py
function get_telemeter_timestamp (line 31) | def get_telemeter_timestamp():
function _post_stats (line 42) | def _post_stats(data) -> None:
function _send_telemetry (line 52) | def _send_telemetry(uuid) -> None:
function send_telemetry (line 66) | def send_telemetry(log) -> None:
FILE: codex/librarian/threads.py
class BreakLoopError (line 17) | class BreakLoopError(Exception):
class NamedThread (line 21) | class NamedThread(Thread, WorkerStatusMixin, ABC):
method __init__ (line 27) | def __init__(
method run_start (line 41) | def run_start(self) -> None:
method join (line 47) | def join(self, timeout=None) -> None:
method stop (line 53) | def stop(self):
class QueuedThread (line 58) | class QueuedThread(NamedThread, ABC):
method __init__ (line 61) | def __init__(self, *args, **kwargs) -> None:
method process_item (line 67) | def process_item(self, item):
method get_timeout (line 71) | def get_timeout(self) -> float | None:
method timed_out (line 75) | def timed_out(self):
method _check_item (line 78) | def _check_item(self) -> None:
method run (line 90) | def run(self) -> None:
method stop (line 104) | def stop(self) -> None:
class AggregateMessageQueuedThread (line 110) | class AggregateMessageQueuedThread(QueuedThread, ABC):
method __init__ (line 116) | def __init__(self, *args, **kwargs) -> None:
method set_last_send (line 122) | def set_last_send(self) -> None:
method get_timeout (line 127) | def get_timeout(self):
method aggregate_items (line 132) | def aggregate_items(self, item):
method send_all_items (line 137) | def send_all_items(self):
method cleanup_cache (line 141) | def cleanup_cache(self, keys) -> None:
method process_item (line 148) | def process_item(self, item) -> None:
method timed_out (line 157) | def timed_out(self) -> None:
FILE: codex/librarian/worker.py
class WorkerMixin (line 11) | class WorkerMixin:
method init_worker (line 14) | def init_worker(
class WorkerStatusMixin (line 26) | class WorkerStatusMixin(WorkerMixin):
method init_worker (line 30) | def init_worker(self, /, logger_, librarian_queue: Queue, db_write_loc...
class WorkerBase (line 37) | class WorkerBase(WorkerMixin):
method __init__ (line 40) | def __init__(self, logger_, librarian_queue: Queue, db_write_lock) -> ...
class WorkerStatusBase (line 46) | class WorkerStatusBase(WorkerStatusMixin):
method __init__ (line 49) | def __init__(self, logger_, librarian_queue: Queue, db_write_lock) -> ...
class WorkerStatusAbortableBase (line 55) | class WorkerStatusAbortableBase(WorkerStatusBase):
method __init__ (line 58) | def __init__(self, logger_, librarian_queue: Queue, db_write_lock, eve...
FILE: codex/middleware.py
class CodexMiddleware (line 19) | class CodexMiddleware:
method __init__ (line 22) | def __init__(self, get_response):
method __call__ (line 26) | def __call__(self, request):
class LogResponseTimeMiddleware (line 41) | class LogResponseTimeMiddleware:
method __init__ (line 44) | def __init__(self, get_response) -> None:
method _log_response_time (line 48) | def _log_response_time(self, request):
method _log_query_times (line 63) | def _log_query_times(self) -> None:
method __call__ (line 74) | def __call__(self, request) -> Any:
class LogRequestMiddleware (line 81) | class LogRequestMiddleware:
method __init__ (line 84) | def __init__(self, get_response) -> None:
method _log_auth_headers (line 88) | def _log_auth_headers(self, request) -> None:
method __call__ (line 106) | def __call__(self, request) -> Any:
FILE: codex/migrations/0001_init.py
class Migration (line 13) | class Migration(migrations.Migration):
FILE: codex/migrations/0002_auto_20200826_0622.py
class Migration (line 6) | class Migration(migrations.Migration):
FILE: codex/migrations/0003_auto_20200831_2033.py
class Migration (line 7) | class Migration(migrations.Migration):
FILE: codex/migrations/0004_failedimport.py
class Migration (line 7) | class Migration(migrations.Migration):
FILE: codex/migrations/0005_auto_20200918_0146.py
class Migration (line 6) | class Migration(migrations.Migration):
FILE: codex/migrations/0006_update_default_names_and_remove_duplicate_comics.py
function update_default_names (line 19) | def update_default_names(apps, _schema_editor) -> None:
function remove_duplicate_comics (line 31) | def remove_duplicate_comics(apps, _schema_editor) -> None:
class Migration (line 62) | class Migration(migrations.Migration):
FILE: codex/migrations/0007_auto_20211210_1710.py
class Migration (line 9) | class Migration(migrations.Migration):
FILE: codex/migrations/0008_alter_comic_created_at_alter_comic_format_and_more.py
class Migration (line 7) | class Migration(migrations.Migration):
FILE: codex/migrations/0009_alter_comic_parent_folder.py
class Migration (line 7) | class Migration(migrations.Migration):
FILE: codex/migrations/0010_haystack.py
class Migration (line 7) | class Migration(migrations.Migration):
FILE: codex/migrations/0011_library_groups_and_metadata_changes.py
function critical_rating_to_decimal (line 8) | def critical_rating_to_decimal(apps, _schema_editor) -> None:
class Migration (line 28) | class Migration(migrations.Migration):
FILE: codex/migrations/0012_rename_description_comic_comments.py
class Migration (line 6) | class Migration(migrations.Migration):
FILE: codex/migrations/0013_int_issue_count_longer_charfields.py
function cast_issue_count (line 8) | def cast_issue_count(apps, _schema_editor) -> None:
class Migration (line 29) | class Migration(migrations.Migration):
FILE: codex/migrations/0014_pdf_issue_suffix_remove_cover_image_sort_name.py
function add_library_folders (line 8) | def add_library_folders(apps, _schema_editor) -> None:
class Migration (line 73) | class Migration(migrations.Migration):
FILE: codex/migrations/0015_link_comics_to_top_level_folders.py
function fix_no_parent_folder_comics (line 8) | def fix_no_parent_folder_comics(apps, _schema_editor) -> None:
class Migration (line 33) | class Migration(migrations.Migration):
FILE: codex/migrations/0016_remove_comic_cover_path_librarianstatus.py
function copy_versions_to_timestamp (line 15) | def copy_versions_to_timestamp(apps, _schema_editor) -> None:
function remove_old_caches (line 30) | def remove_old_caches(_apps, _schema_editor) -> None:
class Migration (line 42) | class Migration(migrations.Migration):
FILE: codex/migrations/0017_alter_timestamp_options_alter_adminflag_name_and_more.py
function clear_covers (line 15) | def clear_covers(_apps, _schema_editor) -> None:
function remove_null_librarian_statuses (line 20) | def remove_null_librarian_statuses(apps, _schema_editor) -> None:
class Migration (line 26) | class Migration(migrations.Migration):
FILE: codex/migrations/0018_rename_userbookmark_bookmark.py
class Migration (line 7) | class Migration(migrations.Migration):
FILE: codex/migrations/0019_delete_queuejob.py
class Migration (line 6) | class Migration(migrations.Migration):
FILE: codex/migrations/0020_remove_search_tables.py
function rename_search_timestamp (line 6) | def rename_search_timestamp(apps, _schema_editor) -> None:
class Migration (line 12) | class Migration(migrations.Migration):
FILE: codex/migrations/0021_bookmark_fit_to_choices_read_in_reverse.py
function ensure_fit_to_has_valid_choices (line 6) | def ensure_fit_to_has_valid_choices(apps, _schema_editor) -> None:
class Migration (line 13) | class Migration(migrations.Migration):
FILE: codex/migrations/0022_bookmark_vertical_useractive_null_statuses.py
class Migration (line 8) | class Migration(migrations.Migration):
FILE: codex/migrations/0023_rename_credit_creator_and_more.py
function prepare_librarianstatus (line 10) | def prepare_librarianstatus(apps, _schema_editor) -> None:
function prepare_bookmarks (line 22) | def prepare_bookmarks(apps, _schema_editor) -> None:
function prepare_comics (line 31) | def prepare_comics(apps, _schema_editor) -> None:
function prepare_adminflags (line 49) | def prepare_adminflags(apps, _schema_editor) -> None:
function prepare_timestamps (line 73) | def prepare_timestamps(apps, _schema_editor) -> None:
class Migration (line 99) | class Migration(migrations.Migration):
FILE: codex/migrations/0024_comic_gtin_comic_story_arc_number.py
class Migration (line 6) | class Migration(migrations.Migration):
FILE: codex/migrations/0025_add_story_arc_number.py
function _create_story_arc_numbers (line 7) | def _create_story_arc_numbers(apps, _schema_editor) -> None:
class Migration (line 33) | class Migration(migrations.Migration):
FILE: codex/migrations/0026_comicbox_1.py
function _migrate_comments (line 25) | def _migrate_comments(apps, _schema_editor) -> None:
function _migrate_reading_direction (line 35) | def _migrate_reading_direction(apps, _schema_editor) -> None:
function _get_pycountry_alpha_2 (line 45) | def _get_pycountry_alpha_2(val, lookup) -> str:
function _create_new_rows (line 51) | def _create_new_rows(comic_model, model, field_name, model_name) -> dict:
function _link_rows_to_comic (line 79) | def _link_rows_to_comic(comic_model, model, field_name, model_name, name...
function _migrate_fields_to_tables (line 95) | def _migrate_fields_to_tables(apps, _schema_editor) -> None:
function _migrate_bookmark (line 104) | def _migrate_bookmark(apps, _schema_editor) -> None:
function _migrate_gtin_to_ids_scan (line 130) | def _migrate_gtin_to_ids_scan(comics) -> tuple[dict, dict]:
function _migrate_gtin_to_ids_create_id_types (line 148) | def _migrate_gtin_to_ids_create_id_types(identifier_type_model, identifi...
function _migrate_gtin_to_ids_create_ids (line 159) | def _migrate_gtin_to_ids_create_ids(
function _migrate_gtin_to_ids_link_comics (line 174) | def _migrate_gtin_to_ids_link_comics(
function _migrate_gtin_to_identifiers (line 190) | def _migrate_gtin_to_identifiers(apps, _schema_editor) -> None:
function _migrate_volume_name (line 210) | def _migrate_volume_name(apps, _schema_editor) -> None:
function _clear_search_index_uuid (line 226) | def _clear_search_index_uuid(apps, _schema_editor) -> None:
class Migration (line 232) | class Migration(migrations.Migration):
FILE: codex/migrations/0027_import_order_and_covers.py
function _set_sort_name (line 24) | def _set_sort_name(obj) -> None:
function _generate_sort_name (line 29) | def _generate_sort_name(apps, _schema_editor) -> None:
function _remove_cover_symlinks (line 45) | def _remove_cover_symlinks(_apps, _schema_editor) -> None:
class Migration (line 61) | class Migration(migrations.Migration):
FILE: codex/migrations/0028_telemeter.py
class Migration (line 6) | class Migration(migrations.Migration):
FILE: codex/migrations/0029_comicfts.py
class Migration (line 7) | class Migration(migrations.Migration):
FILE: codex/migrations/0030_nocase_collation_day_month_indexes_status_types.py
class Migration (line 6) | class Migration(migrations.Migration):
FILE: codex/migrations/0031_adminflag_banner.py
class Migration (line 6) | class Migration(migrations.Migration):
FILE: codex/migrations/0032_alter_librarianstatus_preactive.py
class Migration (line 6) | class Migration(migrations.Migration):
FILE: codex/migrations/0033_alter_librarianstatus_status_type.py
class Migration (line 6) | class Migration(migrations.Migration):
FILE: codex/migrations/0034_comicbox2.py
class Migration (line 10) | class Migration(migrations.Migration):
FILE: codex/migrations/0035_fts_optmize.py
function _map_identifiers_to_canonical_names (line 10) | def _map_identifiers_to_canonical_names(apps) -> dict:
function _prepare_canonical_id_sources (line 26) | def _prepare_canonical_id_sources(apps, identifier_source_map) -> tuple:
function _create_link_map (line 43) | def _create_link_map(identifier_source_map, sources) -> dict:
function _prepare_updatatable_identifiers (line 51) | def _prepare_updatatable_identifiers(apps, identifier_source_map, source...
function _create_canonical_sources (line 62) | def _create_canonical_sources(apps) -> tuple[dict, tuple]:
function _update_identifiers_with_canonical_sources (line 76) | def _update_identifiers_with_canonical_sources(
function _convert_identifier_sources (line 87) | def _convert_identifier_sources(apps, _schema_editor) -> None:
class Migration (line 92) | class Migration(migrations.Migration):
FILE: codex/migrations/0036_alter_comic_path_alter_customcover_path_and_more.py
class Migration (line 8) | class Migration(migrations.Migration):
FILE: codex/migrations/0037_redefine_reading_direction_filetype_choices.py
class Migration (line 8) | class Migration(migrations.Migration):
FILE: codex/migrations/0038_settings_tables.py
function create_default_show_row (line 71) | def create_default_show_row(apps, _schema_editor):
function _get_or_create_show (line 77) | def _get_or_create_show(show_model, show_dict):
function _create_browser_filters (line 84) | def _create_browser_filters(filters_model, browser, settings_dict):
function _create_browser_last_route (line 98) | def _create_browser_last_route(route_model, browser, settings_dict):
function _migrate_browser_session (line 112) | def _migrate_browser_session(settings_dict, model_map, client, user, ses...
function _migrate_reader_session (line 158) | def _migrate_reader_session(settings_dict, model_map, client, user, sess...
function _get_user (line 188) | def _get_user(user_model, session_data):
function migrate_session_forward_model (line 199) | def migrate_session_forward_model(
function migrate_session_forward (line 211) | def migrate_session_forward(
function migrate_sessions_forward (line 243) | def migrate_sessions_forward(apps, _schema_editor):
class Migration (line 269) | class Migration(migrations.Migration):
FILE: codex/models/admin.py
class AdminFlag (line 28) | class AdminFlag(BaseModel):
class Meta (line 41) | class Meta(BaseModel.Meta):
class LibrarianStatus (line 47) | class LibrarianStatus(BaseModel):
class Meta (line 65) | class Meta(BaseModel.Meta):
class Timestamp (line 72) | class Timestamp(BaseModel):
class Choices (line 75) | class Choices(TextChoices):
method touch (line 91) | def touch(cls, choice) -> None:
method save_uuid_version (line 95) | def save_uuid_version(self) -> None:
class Meta (line 102) | class Meta(BaseModel.Meta):
method __repr__ (line 108) | def __repr__(self) -> str:
class UserActive (line 113) | class UserActive(BaseModel):
class GroupAuth (line 119) | class GroupAuth(BaseModel):
FILE: codex/models/base.py
class BaseModel (line 19) | class BaseModel(Model):
class Meta (line 26) | class Meta(ModelBase):
method presave (line 34) | def presave(self):
class NamedModel (line 38) | class NamedModel(BaseModel):
class Meta (line 43) | class Meta(BaseModel.Meta):
method __repr__ (line 50) | def __repr__(self) -> str:
FILE: codex/models/bookmark.py
function cascade_if_user_null (line 18) | def cascade_if_user_null(
class Bookmark (line 47) | class Bookmark(BaseModel):
class Meta (line 60) | class Meta(BaseModel.Meta):
FILE: codex/models/choices.py
function _prepare_text_choices_class_dict (line 12) | def _prepare_text_choices_class_dict(class_name: str) -> _EnumDict:
function _create_text_choices_class (line 17) | def _create_text_choices_class(
function text_choices_from_enum (line 25) | def text_choices_from_enum(
function text_choices_from_map (line 38) | def text_choices_from_map(choices_map: Mapping, class_name: str) -> type...
function text_choices_from_string (line 46) | def text_choices_from_string(string: str, class_name: str) -> type[TextC...
function max_choices_len (line 54) | def max_choices_len(choices: type[Choices]) -> int:
FILE: codex/models/comic.py
class Comic (line 71) | class Comic(WatchedPathBrowserGroup):
class Meta (line 208) | class Meta(WatchedPathBrowserGroup.Meta):
method _set_date (line 213) | def _set_date(self) -> None:
method _set_decade (line 226) | def _set_decade(self) -> None:
method presave (line 234) | def presave(self) -> None:
method max_page (line 242) | def max_page(self):
method _compute_zero_pad (line 247) | def _compute_zero_pad(issue_number_max) -> int:
method get_filename (line 255) | def get_filename(self) -> str:
method _get_title_issue_str (line 260) | def _get_title_issue_str(cls, obj, zero_pad) -> str:
method get_title (line 278) | def get_title(
method __repr__ (line 311) | def __repr__(self) -> str:
class ComicFTS (line 316) | class ComicFTS(BaseModel):
class Meta (line 347) | class Meta(BaseModel.Meta):
FILE: codex/models/fields.py
class CleaningStringFieldMixin (line 17) | class CleaningStringFieldMixin:
method get_prep_value (line 20) | def get_prep_value(self, value):
class CleaningCharField (line 29) | class CleaningCharField(CleaningStringFieldMixin, CharField):
class CleaningTextField (line 33) | class CleaningTextField(CleaningStringFieldMixin, TextField):
class CoercingSmallIntegerFieldMixin (line 37) | class CoercingSmallIntegerFieldMixin:
method get_prep_value (line 43) | def get_prep_value(self, value):
class CoercingSmallIntegerField (line 51) | class CoercingSmallIntegerField(CoercingSmallIntegerFieldMixin, SmallInt...
class CoercingPositiveSmallIntegerField (line 55) | class CoercingPositiveSmallIntegerField(
class CoercingDecimalField (line 63) | class CoercingDecimalField(DecimalField):
method __init__ (line 66) | def __init__(self, *args, **kwargs) -> None:
method get_prep_value (line 73) | def get_prep_value(self, value) -> Any:
FILE: codex/models/functions.py
class JsonGroupArray (line 15) | class JsonGroupArray(Aggregate):
method __init__ (line 23) | def __init__(self, *args, **kwargs) -> None:
class GroupConcat (line 28) | class GroupConcat(Aggregate):
method __init__ (line 38) | def __init__(self, *args, **kwargs) -> None:
class FTS5Match (line 44) | class FTS5Match(Lookup):
method as_sql (line 50) | def as_sql(self, compiler, connection) -> tuple:
class Like (line 64) | class Like(Lookup):
method as_sql (line 71) | def as_sql(self, compiler, connection) -> tuple:
class ComicFTSRank (line 80) | class ComicFTSRank(Func):
method __init__ (line 86) | def __init__(self, *args, **kwargs) -> None:
FILE: codex/models/groups.py
class BrowserGroupModel (line 24) | class BrowserGroupModel(BaseModel):
method set_sort_name (line 43) | def set_sort_name(self) -> None:
method presave (line 48) | def presave(self) -> None:
method save (line 53) | def save(self, *args, **kwargs) -> None:
class Meta (line 58) | class Meta(BaseModel.Meta):
method _repr_parts (line 63) | def _repr_parts(self) -> tuple[str, ...]:
method __repr__ (line 67) | def __repr__(self) -> str:
class IdentifiedBrowserGroupModel (line 72) | class IdentifiedBrowserGroupModel(BrowserGroupModel):
class Meta (line 85) | class Meta(BrowserGroupModel.Meta):
class Publisher (line 91) | class Publisher(IdentifiedBrowserGroupModel):
class Meta (line 94) | class Meta(IdentifiedBrowserGroupModel.Meta):
class Imprint (line 100) | class Imprint(IdentifiedBrowserGroupModel):
class Meta (line 107) | class Meta(IdentifiedBrowserGroupModel.Meta):
method _repr_parts (line 113) | def _repr_parts(self) -> tuple:
class Series (line 117) | class Series(IdentifiedBrowserGroupModel):
class Meta (line 126) | class Meta(IdentifiedBrowserGroupModel.Meta):
method _repr_parts (line 133) | def _repr_parts(self) -> tuple:
class Volume (line 141) | class Volume(BrowserGroupModel):
method set_sort_name (line 164) | def set_sort_name(self):
class Meta (line 167) | class Meta(BrowserGroupModel.Meta):
method to_str (line 173) | def to_str(cls, number: int | None, number_to: int | None) -> str:
method _repr_parts (line 189) | def _repr_parts(self) -> tuple:
class WatchedPathBrowserGroup (line 199) | class WatchedPathBrowserGroup(BrowserGroupModel, WatchedPath):
method presave (line 203) | def presave(self) -> None:
class Meta (line 208) | class Meta(BrowserGroupModel.Meta, WatchedPath.Meta):
class Folder (line 214) | class Folder(WatchedPathBrowserGroup):
FILE: codex/models/identifier.py
class IdentifierSource (line 21) | class IdentifierSource(NamedModel):
class IdentifierType (line 25) | class IdentifierType(TextChoices):
class Identifier (line 49) | class Identifier(BaseModel):
class Meta (line 65) | class Meta(BaseModel.Meta):
method name (line 71) | def name(self) -> str:
method __repr__ (line 78) | def __repr__(self) -> str:
FILE: codex/models/library.py
function validate_dir_exists (line 24) | def validate_dir_exists(path) -> None:
class Library (line 30) | class Library(BaseModel):
method __repr__ (line 57) | def __repr__(self) -> str:
class Meta (line 61) | class Meta(BaseModel.Meta):
method _save_update_in_progress (line 66) | def _save_update_in_progress(self, *, value: bool) -> None:
method start_update (line 70) | def start_update(self) -> None:
method end_update (line 74) | def end_update(self) -> None:
FILE: codex/models/named.py
class IdentifiedNamedModel (line 38) | class IdentifiedNamedModel(NamedModel):
class Meta (line 49) | class Meta(NamedModel.Meta):
method __repr__ (line 55) | def __repr__(self) -> str:
class AgeRating (line 61) | class AgeRating(NamedModel):
class Character (line 65) | class Character(IdentifiedNamedModel):
class CreditPerson (line 69) | class CreditPerson(IdentifiedNamedModel):
class CreditRole (line 73) | class CreditRole(IdentifiedNamedModel):
class Credit (line 77) | class Credit(BaseModel):
class Meta (line 83) | class Meta(BaseModel.Meta):
method __repr__ (line 89) | def __repr__(self) -> str:
class Country (line 94) | class Country(NamedModel):
class Meta (line 97) | class Meta(NamedModel.Meta):
class Genre (line 103) | class Genre(IdentifiedNamedModel):
class Language (line 107) | class Language(NamedModel):
class Location (line 111) | class Location(IdentifiedNamedModel):
class OriginalFormat (line 115) | class OriginalFormat(NamedModel):
class ScanInfo (line 119) | class ScanInfo(NamedModel):
class SeriesGroup (line 123) | class SeriesGroup(NamedModel):
class Story (line 127) | class Story(IdentifiedNamedModel):
class Meta (line 130) | class Meta(IdentifiedNamedModel.Meta):
class StoryArc (line 136) | class StoryArc(IdentifiedNamedModel, BrowserGroupModel):
class Meta (line 139) | class Meta(IdentifiedNamedModel.Meta, BrowserGroupModel.Meta):
class StoryArcNumber (line 143) | class StoryArcNumber(BaseModel):
class Meta (line 149) | class Meta(BaseModel.Meta):
method name (line 155) | def name(self):
class Tag (line 161) | class Tag(IdentifiedNamedModel):
class Tagger (line 165) | class Tagger(NamedModel):
class Team (line 169) | class Team(IdentifiedNamedModel):
class Universe (line 173) | class Universe(IdentifiedNamedModel):
method __repr__ (line 179) | def __repr__(self) -> str:
FILE: codex/models/paths.py
class WatchedPath (line 17) | class WatchedPath(BaseModel):
method set_stat (line 30) | def set_stat(self) -> None:
method presave (line 49) | def presave(self) -> None:
method __repr__ (line 54) | def __repr__(self) -> str:
class Meta (line 58) | class Meta(BaseModel.Meta):
method search_path (line 64) | def search_path(self) -> str:
class FailedImport (line 69) | class FailedImport(WatchedPath):
method set_reason (line 74) | def set_reason(self, exc) -> None:
class CustomCover (line 84) | class CustomCover(WatchedPath):
class GroupChoices (line 87) | class GroupChoices(TextChoices):
method _set_group_and_sort_name (line 117) | def _set_group_and_sort_name(self) -> None:
method presave (line 129) | def presave(self) -> None:
FILE: codex/models/query.py
class GroupBySQLCompiler (line 13) | class GroupBySQLCompiler(SQLCompiler):
method __init__ (line 16) | def __init__(self, *args, **kwargs) -> None:
method set_force_group_by (line 22) | def set_force_group_by(self, table, fields) -> None:
method get_group_by (line 28) | def get_group_by(self, *args, **kwargs) -> list:
class GroupByQuery (line 42) | class GroupByQuery(Query):
method __init__ (line 45) | def __init__(self, *args, **kwargs) -> None:
method get_compiler (line 52) | def get_compiler(
method set_force_group_by (line 75) | def set_force_group_by(self, fields, model=None) -> None:
class GroupByQuerySet (line 84) | class GroupByQuerySet(QuerySet):
method __init__ (line 87) | def __init__(self, model=None, query=None, using=None, hints=None) -> ...
method group_by (line 92) | def group_by(self, *fields, model=None) -> Self:
method demote_joins (line 98) | def demote_joins(self, tables) -> Self:
class GroupByManager (line 106) | class GroupByManager(Manager.from_queryset(GroupByQuerySet)): # ty: ign...
FILE: codex/models/settings.py
function cascade_if_session_null (line 47) | def cascade_if_session_null(
function cascade_if_user_null (line 72) | def cascade_if_user_null(
class ClientChoices (line 100) | class ClientChoices(TextChoices):
class FitToChoices (line 107) | class FitToChoices(TextChoices):
class SettingsBase (line 121) | class SettingsBase(BaseModel):
class Meta (line 145) | class Meta(BaseModel.Meta):
method __repr__ (line 151) | def __repr__(self) -> str:
class SettingsBrowserShow (line 165) | class SettingsBrowserShow(BaseModel):
class Meta (line 178) | class Meta(BaseModel.Meta):
method __repr__ (line 190) | def __repr__(self) -> str:
class SettingsBrowserFilters (line 194) | class SettingsBrowserFilters(BaseModel):
class Meta (line 267) | class Meta(BaseModel.Meta):
method __repr__ (line 273) | def __repr__(self) -> str:
class SettingsBrowserLastRoute (line 277) | class SettingsBrowserLastRoute(BaseModel):
class Meta (line 298) | class Meta(BaseModel.Meta):
method __repr__ (line 304) | def __repr__(self) -> str:
class SettingsBrowser (line 316) | class SettingsBrowser(SettingsBase):
class Meta (line 364) | class Meta(SettingsBase.Meta):
class SettingsReader (line 394) | class SettingsReader(SettingsBase):
class Meta (line 452) | class Meta(SettingsBase.Meta):
FILE: codex/models/util.py
function get_sort_name (line 21) | def get_sort_name(name: str) -> str:
FILE: codex/run.py
function codex_startup (line 31) | def codex_startup() -> bool:
function _database_checkpoint (line 37) | def _database_checkpoint() -> None:
function restart (line 44) | def restart() -> None:
function codex_shutdown (line 52) | def codex_shutdown() -> None:
function _build_server (line 61) | def _build_server() -> Server:
function _watch_for_changes (line 79) | async def _watch_for_changes() -> None:
function _serve (line 90) | async def _serve(server: Server) -> None:
function run (line 101) | def run() -> None:
function main (line 111) | def main() -> None:
FILE: codex/serializers/admin/flags.py
class AdminFlagSerializer (line 7) | class AdminFlagSerializer(BaseModelSerializer):
class Meta (line 10) | class Meta(BaseModelSerializer.Meta):
FILE: codex/serializers/admin/groups.py
class GroupSerializer (line 14) | class GroupSerializer(BaseModelSerializer):
class Meta (line 19) | class Meta(BaseModelSerializer.Meta):
method update (line 27) | def update(self, instance, validated_data) -> Any:
method create (line 37) | def create(self, validated_data) -> Any:
FILE: codex/serializers/admin/libraries.py
class LibrarySerializer (line 18) | class LibrarySerializer(BaseModelSerializer):
class Meta (line 24) | class Meta(BaseModelSerializer.Meta):
method validate_path (line 48) | def validate_path(self, path):
class FailedImportSerializer (line 66) | class FailedImportSerializer(BaseModelSerializer):
class Meta (line 69) | class Meta(BaseModelSerializer.Meta):
class AdminFolderListSerializer (line 77) | class AdminFolderListSerializer(Serializer):
class AdminFolderSerializer (line 84) | class AdminFolderSerializer(Serializer):
method validate_path (line 90) | def validate_path(self, path):
method validate_show_hidden (line 98) | def validate_show_hidden(self, show_hidden) -> bool:
FILE: codex/serializers/admin/stats.py
class StatsSystemSerializer (line 19) | class StatsSystemSerializer(Serializer):
class StatsPlatformSerializer (line 26) | class StatsPlatformSerializer(Serializer):
class StatsConfigSerializer (line 37) | class StatsConfigSerializer(Serializer):
class StatsSessionsSerializer (line 48) | class StatsSessionsSerializer(Serializer):
class StatsGroupSerializer (line 59) | class StatsGroupSerializer(Serializer):
class StatsComicMetadataSerializer (line 71) | class StatsComicMetadataSerializer(Serializer):
class StatsSerializer (line 96) | class StatsSerializer(Serializer):
class AdminStatsRequestSerializer (line 107) | class AdminStatsRequestSerializer(Serializer):
class APIKeySerializer (line 124) | class APIKeySerializer(Serializer):
FILE: codex/serializers/admin/tasks.py
class AdminLibrarianTaskSerializer (line 24) | class AdminLibrarianTaskSerializer(Serializer):
FILE: codex/serializers/admin/users.py
class PasswordSerializerMixin (line 14) | class PasswordSerializerMixin(metaclass=SerializerMetaclass):
class UserChangePasswordSerializer (line 20) | class UserChangePasswordSerializer(Serializer, PasswordSerializerMixin):
class UserSerializer (line 24) | class UserSerializer(BaseModelSerializer, PasswordSerializerMixin):
class Meta (line 32) | class Meta(BaseModelSerializer.Meta):
FILE: codex/serializers/auth.py
class UserSerializer (line 18) | class UserSerializer(BaseModelSerializer):
method get_admin_flags (line 28) | def get_admin_flags(self, *_args) -> dict:
class Meta (line 40) | class Meta(BaseModelSerializer.Meta):
class TimezoneSerializerMixin (line 53) | class TimezoneSerializerMixin(metaclass=SerializerMetaclass):
class TimezoneSerializer (line 59) | class TimezoneSerializer(TimezoneSerializerMixin, Serializer):
class UserCreateSerializer (line 63) | class UserCreateSerializer(BaseModelSerializer, TimezoneSerializerMixin):
class Meta (line 66) | class Meta(BaseModelSerializer.Meta):
class UserLoginSerializer (line 74) | class UserLoginSerializer(UserCreateSerializer):
class Meta (line 80) | class Meta(UserCreateSerializer.Meta):
class AuthAdminFlagsSerializer (line 84) | class AuthAdminFlagsSerializer(Serializer):
FILE: codex/serializers/browser/choices.py
class BrowserFilterChoicesSerializer (line 28) | class BrowserFilterChoicesSerializer(Serializer):
class BrowserSettingsFilterSerializer (line 55) | class BrowserSettingsFilterSerializer(Serializer):
class BrowserChoicesIntegerPkSerializer (line 88) | class BrowserChoicesIntegerPkSerializer(Serializer):
class BrowserChoicesUniversePkSerializer (line 95) | class BrowserChoicesUniversePkSerializer(Serializer):
class BrowserChoicesCharPkSerializer (line 101) | class BrowserChoicesCharPkSerializer(BrowserChoicesIntegerPkSerializer):
class BrowserChoicesDecimalPkSerializer (line 107) | class BrowserChoicesDecimalPkSerializer(BrowserChoicesIntegerPkSerializer):
class BrowserChoicesFilterSerializer (line 126) | class BrowserChoicesFilterSerializer(Serializer):
method get_choices (line 131) | def get_choices(self, obj) -> list:
FILE: codex/serializers/browser/filters.py
class BrowserSettingsFilterInputSerializer (line 18) | class BrowserSettingsFilterInputSerializer(Serializer):
FILE: codex/serializers/browser/metadata.py
class GroupSerializer (line 17) | class GroupSerializer(Serializer):
class MetadataSerializer (line 26) | class MetadataSerializer(BrowserAggregateSerializerMixin, ComicSerializer):
class Meta (line 55) | class Meta(ComicSerializer.Meta):
FILE: codex/serializers/browser/mixins.py
class BrowserAggregateSerializerMixin (line 21) | class BrowserAggregateSerializerMixin(metaclass=SerializerMetaclass):
method _get_max_updated_at (line 39) | def _get_max_updated_at(mtime, updated_ats) -> datetime:
method get_mtime (line 56) | def get_mtime(self, obj) -> int:
FILE: codex/serializers/browser/mtime.py
class GroupsMtimeSerializer (line 10) | class GroupsMtimeSerializer(BrowserFilterChoicesInputSerializer):
class MtimeSerializer (line 20) | class MtimeSerializer(Serializer):
FILE: codex/serializers/browser/page.py
class BrowserCardSerializer (line 19) | class BrowserCardSerializer(BrowserAggregateSerializerMixin, Serializer):
class BrowserAdminFlagsSerializer (line 43) | class BrowserAdminFlagsSerializer(Serializer):
class BrowserTitleSerializer (line 50) | class BrowserTitleSerializer(Serializer):
class BrowserPageSerializer (line 58) | class BrowserPageSerializer(Serializer):
FILE: codex/serializers/browser/saved.py
class SavedSettingNameSerializer (line 9) | class SavedSettingNameSerializer(Serializer):
class SavedBrowserSettingsListSerializer (line 16) | class SavedBrowserSettingsListSerializer(Serializer):
class SavedBrowserSettingsSaveSerializer (line 26) | class SavedBrowserSettingsSaveSerializer(Serializer):
class SavedSettingsLoadSerializer (line 34) | class SavedSettingsLoadSerializer(Serializer):
FILE: codex/serializers/browser/settings.py
class BrowserSettingsShowGroupFlagsSerializer (line 22) | class BrowserSettingsShowGroupFlagsSerializer(Serializer):
class BrowserSettingsLastRouteSerializer (line 31) | class BrowserSettingsLastRouteSerializer(Serializer):
method to_representation (line 39) | def to_representation(self, instance) -> dict:
class BrowserFilterChoicesInputSerializer (line 52) | class BrowserFilterChoicesInputSerializer(JSONFieldSerializer):
class BrowserCoverInputSerializerBase (line 62) | class BrowserCoverInputSerializerBase(BrowserFilterChoicesInputSerializer):
class BrowserCoverInputSerializer (line 76) | class BrowserCoverInputSerializer(BrowserCoverInputSerializerBase):
class BrowserSettingsSerializerBase (line 86) | class BrowserSettingsSerializerBase(BrowserCoverInputSerializerBase):
method to_internal_value (line 92) | def to_internal_value(self, data) -> dict:
class OPDSSettingsSerializer (line 100) | class OPDSSettingsSerializer(BrowserSettingsSerializerBase):
class BrowserSettingsSerializer (line 108) | class BrowserSettingsSerializer(BrowserSettingsSerializerBase):
class BrowserSettingsInputSerializer (line 120) | class BrowserSettingsInputSerializer(SettingsInputSerializer):
FILE: codex/serializers/fields/auth.py
class TimestampField (line 14) | class TimestampField(IntegerField):
method to_representation (line 18) | def to_representation(self, value) -> int:
method to_internal_value (line 25) | def to_internal_value(self, data) -> datetime: # pyright: ignore[repo...
function validate_timezone (line 30) | def validate_timezone(data):
class TimezoneField (line 39) | class TimezoneField(CharField):
method __init__ (line 42) | def __init__(self, *args, **kwargs) -> None:
FILE: codex/serializers/fields/base.py
class CodexChoiceField (line 9) | class CodexChoiceField(ChoiceField, ABC):
method __init__ (line 14) | def __init__(self, **kwargs) -> None:
FILE: codex/serializers/fields/browser.py
class BookmarkFilterField (line 20) | class BookmarkFilterField(CodexChoiceField):
class PyCountryField (line 26) | class PyCountryField(SanitizedCharField, ABC):
method to_representation (line 33) | def to_representation(self, value) -> str:
class CountryField (line 54) | class CountryField(PyCountryField):
class LanguageField (line 60) | class LanguageField(PyCountryField):
class BreadcrumbsField (line 66) | class BreadcrumbsField(ListField):
FILE: codex/serializers/fields/group.py
class BrowseGroupField (line 7) | class BrowseGroupField(CodexChoiceField):
class BrowserRouteGroupField (line 13) | class BrowserRouteGroupField(CodexChoiceField):
FILE: codex/serializers/fields/reader.py
class FitToField (line 11) | class FitToField(CodexChoiceField):
class ReadingDirectionField (line 17) | class ReadingDirectionField(CodexChoiceField):
class ArcGroupField (line 23) | class ArcGroupField(CodexChoiceField):
FILE: codex/serializers/fields/sanitized.py
class SanitizedCharField (line 9) | class SanitizedCharField(CharField):
method to_internal_value (line 13) | def to_internal_value(self, data) -> str:
FILE: codex/serializers/fields/settings.py
class SettingsKeyField (line 8) | class SettingsKeyField(CodexChoiceField):
FILE: codex/serializers/fields/stats.py
class StringListMultipleChoiceField (line 9) | class StringListMultipleChoiceField(MultipleChoiceField):
method to_internal_value (line 13) | def to_internal_value(self, data) -> str:
class SerializerChoicesField (line 20) | class SerializerChoicesField(StringListMultipleChoiceField):
method __init__ (line 23) | def __init__(self, serializer=None, **kwargs) -> None:
class CountDictField (line 32) | class CountDictField(DictField):
FILE: codex/serializers/fields/vuetify.py
class VuetifyNullCodeFieldMixin (line 21) | class VuetifyNullCodeFieldMixin:
method to_internal_value (line 26) | def to_internal_value(self, data):
method to_representation (line 31) | def to_representation(self, data):
class VuetifyFileTypeChoiceField (line 37) | class VuetifyFileTypeChoiceField(VuetifyNullCodeFieldMixin, CodexChoiceF...
class VuetifyReadingDirectionChoiceField (line 43) | class VuetifyReadingDirectionChoiceField(VuetifyNullCodeFieldMixin, Code...
class VuetifyDecimalField (line 49) | class VuetifyDecimalField(VuetifyNullCodeFieldMixin, DecimalField): # p...
class VuetifyIntegerField (line 53) | class VuetifyIntegerField(VuetifyNullCodeFieldMixin, IntegerField): # p...
class VuetifyCharField (line 57) | class VuetifyCharField(VuetifyNullCodeFieldMixin, CharField): # pyright...
class VuetifyBooleanField (line 63) | class VuetifyBooleanField(VuetifyNullCodeFieldMixin, BooleanField): # p...
function validate_decade (line 67) | def validate_decade(decade) -> bool:
class VuetifyDecadeField (line 76) | class VuetifyDecadeField(VuetifyIntegerField):
method __init__ (line 81) | def __init__(self, *args, **kwargs) -> None:
class VuetifyListField (line 86) | class VuetifyListField(ListField):
method __init__ (line 92) | def __init__(
method to_representation (line 109) | def to_representation(self, value: list) -> list:
class VuetifyReadOnlyListField (line 118) | class VuetifyReadOnlyListField(VuetifyListField):
FILE: codex/serializers/homepage.py
class HomepageSerializer (line 6) | class HomepageSerializer(Serializer):
FILE: codex/serializers/mixins.py
class OKSerializer (line 18) | class OKSerializer(Serializer):
class JSONFieldSerializer (line 24) | class JSONFieldSerializer(Serializer):
method _parse_json_field (line 30) | def _parse_json_field(key, value) -> str | None:
method to_internal_value (line 42) | def to_internal_value(self, data) -> dict:
FILE: codex/serializers/models/admin.py
class LibrarianStatusSerializer (line 7) | class LibrarianStatusSerializer(BaseModelSerializer):
class Meta (line 10) | class Meta(BaseModelSerializer.Meta):
FILE: codex/serializers/models/base.py
class BaseModelSerializer (line 6) | class BaseModelSerializer(ModelSerializer):
class Meta (line 9) | class Meta(SerializerMetaclass): # pyright: ignore[reportIncompatible...
FILE: codex/serializers/models/bookmark.py
class BookmarkSerializer (line 7) | class BookmarkSerializer(BaseModelSerializer):
class Meta (line 10) | class Meta(BaseModelSerializer.Meta):
class BookmarkFinishedSerializer (line 20) | class BookmarkFinishedSerializer(BaseModelSerializer):
class Meta (line 23) | class Meta(BaseModelSerializer.Meta):
FILE: codex/serializers/models/comic.py
class ComicSerializer (line 35) | class ComicSerializer(BaseModelSerializer):
class Meta (line 79) | class Meta(BaseModelSerializer.Meta):
FILE: codex/serializers/models/groups.py
class GroupModelSerializer (line 12) | class GroupModelSerializer(NamedModelSerializer):
class Meta (line 15) | class Meta(NamedModelSerializer.Meta):
class PublisherSerializer (line 21) | class PublisherSerializer(GroupModelSerializer):
class Meta (line 24) | class Meta(GroupModelSerializer.Meta):
class ImprintSerializer (line 30) | class ImprintSerializer(GroupModelSerializer):
class Meta (line 33) | class Meta(GroupModelSerializer.Meta):
class SeriesSerializer (line 39) | class SeriesSerializer(GroupModelSerializer):
class Meta (line 42) | class Meta(GroupModelSerializer.Meta):
class VolumeSerializer (line 48) | class VolumeSerializer(GroupModelSerializer):
class Meta (line 51) | class Meta(GroupModelSerializer.Meta):
FILE: codex/serializers/models/named.py
class NamedModelSerializer (line 29) | class NamedModelSerializer(BaseModelSerializer):
class Meta (line 32) | class Meta(BaseModelSerializer.Meta):
class URLNamedModelSerializer (line 39) | class URLNamedModelSerializer(NamedModelSerializer):
class Meta (line 44) | class Meta(NamedModelSerializer.Meta):
class CreditPersonSerializer (line 51) | class CreditPersonSerializer(URLNamedModelSerializer):
class Meta (line 54) | class Meta(URLNamedModelSerializer.Meta):
class CreditRoleSerializer (line 60) | class CreditRoleSerializer(URLNamedModelSerializer):
class Meta (line 63) | class Meta(URLNamedModelSerializer.Meta):
class CreditSerializer (line 69) | class CreditSerializer(BaseModelSerializer):
class Meta (line 75) | class Meta(BaseModelSerializer.Meta):
class CharacterSerializer (line 83) | class CharacterSerializer(URLNamedModelSerializer):
class Meta (line 86) | class Meta(URLNamedModelSerializer.Meta):
class GenreSerializer (line 92) | class GenreSerializer(URLNamedModelSerializer):
class Meta (line 95) | class Meta(URLNamedModelSerializer.Meta):
class IdentifierSourceSerializer (line 101) | class IdentifierSourceSerializer(NamedModelSerializer):
class Meta (line 104) | class Meta(NamedModelSerializer.Meta):
class IdentifierSeralizer (line 110) | class IdentifierSeralizer(BaseModelSerializer):
class Meta (line 115) | class Meta(BaseModelSerializer.Meta):
class LocationSerializer (line 123) | class LocationSerializer(URLNamedModelSerializer):
class Meta (line 126) | class Meta(URLNamedModelSerializer.Meta):
class SeriesGroupSerializer (line 132) | class SeriesGroupSerializer(NamedModelSerializer):
class Meta (line 135) | class Meta(NamedModelSerializer.Meta):
class StorySerializer (line 141) | class StorySerializer(URLNamedModelSerializer):
class Meta (line 144) | class Meta(URLNamedModelSerializer.Meta):
class StoryArcSerializer (line 150) | class StoryArcSerializer(URLNamedModelSerializer):
class Meta (line 153) | class Meta(URLNamedModelSerializer.Meta):
class StoryArcNumberSerializer (line 159) | class StoryArcNumberSerializer(BaseModelSerializer):
class Meta (line 165) | class Meta(BaseModelSerializer.Meta):
class TaggerSerializer (line 173) | class TaggerSerializer(NamedModelSerializer):
class Meta (line 176) | class Meta(NamedModelSerializer.Meta):
class AgeRatingSerializer (line 182) | class AgeRatingSerializer(NamedModelSerializer):
class Meta (line 185) | class Meta(NamedModelSerializer.Meta):
class TagSerializer (line 191) | class TagSerializer(URLNamedModelSerializer):
class Meta (line 194) | class Meta(URLNamedModelSerializer.Meta):
class OriginalFormatSerializer (line 200) | class OriginalFormatSerializer(NamedModelSerializer):
class Meta (line 203) | class Meta(NamedModelSerializer.Meta):
class ScanInfoSerializer (line 209) | class ScanInfoSerializer(NamedModelSerializer):
class Meta (line 212) | class Meta(NamedModelSerializer.Meta):
class TeamSerializer (line 218) | class TeamSerializer(URLNamedModelSerializer):
class Meta (line 221) | class Meta(URLNamedModelSerializer.Meta):
class UniverseSerializer (line 227) | class UniverseSerializer(URLNamedModelSerializer):
class Meta (line 230) | class Meta(URLNamedModelSerializer.Meta):
FILE: codex/serializers/models/pycountry.py
class CountrySerializer (line 11) | class CountrySerializer(NamedModelSerializer):
class Meta (line 16) | class Meta(NamedModelSerializer.Meta):
class LanguageSerializer (line 22) | class LanguageSerializer(NamedModelSerializer):
class Meta (line 27) | class Meta(NamedModelSerializer.Meta):
FILE: codex/serializers/opds/authentication.py
class OPDSAuth1LinksSerializer (line 11) | class OPDSAuth1LinksSerializer(Serializer):
class OPDSAuthetication1LabelsSerializer (line 21) | class OPDSAuthetication1LabelsSerializer(Serializer):
class OPDSAuthentication1FlowSerializer (line 28) | class OPDSAuthentication1FlowSerializer(Serializer):
class OPDSAuthentication1Serializer (line 36) | class OPDSAuthentication1Serializer(Serializer):
FILE: codex/serializers/opds/urls.py
class OPDSURLsSerializer (line 7) | class OPDSURLsSerializer(Serializer):
FILE: codex/serializers/opds/v1.py
class OPDS1TemplateLinkSerializer (line 21) | class OPDS1TemplateLinkSerializer(Serializer):
class OPDS1CreditSerializer (line 37) | class OPDS1CreditSerializer(Serializer):
class OPDS1TemplateEntrySerializer (line 44) | class OPDS1TemplateEntrySerializer(Serializer):
class OPDS1TemplateSerializer (line 61) | class OPDS1TemplateSerializer(Serializer):
FILE: codex/serializers/opds/v2/facet.py
class OPDS2FacetSerializer (line 9) | class OPDS2FacetSerializer(Serializer):
FILE: codex/serializers/opds/v2/feed.py
class OPDS2GroupSerializer (line 14) | class OPDS2GroupSerializer(Serializer):
class OPDS2FeedSerializer (line 29) | class OPDS2FeedSerializer(OPDS2GroupSerializer):
FILE: codex/serializers/opds/v2/links.py
class OPDS2LinkBaseSerializer (line 15) | class OPDS2LinkBaseSerializer(Serializer):
method get_rel (line 22) | def get_rel(self, obj) -> str | list[str]:
method to_representation (line 31) | def to_representation(self, instance) -> dict:
class OPSD2AuthenticateSerializer (line 41) | class OPSD2AuthenticateSerializer(OPDS2LinkBaseSerializer):
class OPDS2LinkPropertiesSerializer (line 49) | class OPDS2LinkPropertiesSerializer(Serializer):
class OPDS2LinkSerializer (line 72) | class OPDS2LinkSerializer(OPDS2LinkBaseSerializer):
class OPDS2LinkListField (line 100) | class OPDS2LinkListField(ListField):
FILE: codex/serializers/opds/v2/metadata.py
class OPDS2MetadataSerializer (line 7) | class OPDS2MetadataSerializer(Serializer):
FILE: codex/serializers/opds/v2/progression.py
class OPDS2ProgressionDeviceSerializer (line 17) | class OPDS2ProgressionDeviceSerializer(Serializer):
class OPDS2ProgressionLocationsSerializer (line 24) | class OPDS2ProgressionLocationsSerializer(Serializer):
class OPDS2ProgressionLocatorSerializer (line 33) | class OPDS2ProgressionLocatorSerializer(Serializer):
class OPDS2ProgressionSerializer (line 42) | class OPDS2ProgressionSerializer(Serializer):
FILE: codex/serializers/opds/v2/publication.py
class OPDS2SubjectSerializer (line 11) | class OPDS2SubjectSerializer(Serializer):
class OPDS2ContributorSerializer (line 25) | class OPDS2ContributorSerializer(OPDS2SubjectSerializer):
class OPDS2BelongsToObjectSerializer (line 38) | class OPDS2BelongsToObjectSerializer(Serializer):
class OPDS2BelongsTo (line 46) | class OPDS2BelongsTo(Serializer):
class OPDS2PublicationMetadataSerializer (line 66) | class OPDS2PublicationMetadataSerializer(OPDS2MetadataSerializer):
class OPDS2PublicationSerializer (line 111) | class OPDS2PublicationSerializer(OPDS2FacetSerializer):
class OPDS2PublicationDivinaMetadataSerializer (line 126) | class OPDS2PublicationDivinaMetadataSerializer(OPDS2PublicationMetadataS...
class OPDS2PublicationDivinaManifestSerializer (line 138) | class OPDS2PublicationDivinaManifestSerializer(OPDS2PublicationSerializer):
FILE: codex/serializers/opds/v2/unused.py
class RecursiveField (line 11) | class RecursiveField(Serializer):
method to_representation (line 20) | def to_representation(self, instance) -> Any:
class OPDS2PriceSerializer (line 28) | class OPDS2PriceSerializer(Serializer):
class OPDS2HoldsSerializer (line 40) | class OPDS2HoldsSerializer(Serializer):
class OPDS2CopiesSerializer (line 51) | class OPDS2CopiesSerializer(Serializer):
class OPDS2AcquisitionObjectSerializer (line 62) | class OPDS2AcquisitionObjectSerializer(Serializer):
class OPDS2ProfileSerializer (line 73) | class OPDS2ProfileSerializer(Serializer):
class OPDS2AvailabilitySerializer (line 87) | class OPDS2AvailabilitySerializer(Serializer):
FILE: codex/serializers/reader.py
class ReaderSettingsSerializer (line 25) | class ReaderSettingsSerializer(Serializer):
class ReaderScopedUpdateSerializer (line 40) | class ReaderScopedUpdateSerializer(ReaderSettingsSerializer):
class ReaderBookmarkSerializer (line 47) | class ReaderBookmarkSerializer(Serializer):
class ReaderComicSerializer (line 54) | class ReaderComicSerializer(Serializer):
class ReaderArcInfoSerializer (line 66) | class ReaderArcInfoSerializer(Serializer):
class ReaderSelectedArcSerializer (line 73) | class ReaderSelectedArcSerializer(Serializer):
class ReaderViewInputSerializer (line 82) | class ReaderViewInputSerializer(JSONFieldSerializer):
class ReaderCurrentComicSerializer (line 90) | class ReaderCurrentComicSerializer(ReaderComicSerializer):
class ReaderBooksSerializer (line 111) | class ReaderBooksSerializer(Serializer):
class ArcsIdsField (line 119) | class ArcsIdsField(DictField):
method to_representation (line 123) | def to_representation(self, value):
class ArcsField (line 135) | class ArcsField(DictField):
class ReaderComicsSerializer (line 141) | class ReaderComicsSerializer(Serializer):
FILE: codex/serializers/redirect.py
class ReaderRedirectSerializer (line 10) | class ReaderRedirectSerializer(Serializer):
class BrowserRedirectSerializer (line 17) | class BrowserRedirectSerializer(ReaderRedirectSerializer):
FILE: codex/serializers/route.py
class SimpleRouteSerializer (line 14) | class SimpleRouteSerializer(Serializer):
method to_representation (line 21) | def to_representation(self, instance) -> dict:
method to_internal_value (line 32) | def to_internal_value(self, data) -> dict:
class RouteSerializer (line 47) | class RouteSerializer(SimpleRouteSerializer):
FILE: codex/serializers/settings.py
class SettingsInputSerializer (line 9) | class SettingsInputSerializer(JSONFieldSerializer):
FILE: codex/serializers/versions.py
class VersionsSerializer (line 7) | class VersionsSerializer(Serializer):
FILE: codex/settings/__init__.py
function not_falsy_env (line 43) | def not_falsy_env(name):
function _get_installed_apps (line 245) | def _get_installed_apps() -> tuple:
function _get_middleware (line 284) | def _get_middleware() -> tuple:
function create_custom_cover_group_dirs (line 607) | def create_custom_cover_group_dirs() -> None:
FILE: codex/settings/config.py
function _deep_get (line 64) | def _deep_get(data: Mapping, keypath: str, default=None):
function _deep_set (line 77) | def _deep_set(data: MutableMapping, keypath: str, value) -> None:
function _ensure_config (line 85) | def _ensure_config(config_toml: Path, config_toml_default: Path) -> None:
function _apply_env_overrides (line 95) | def _apply_env_overrides(config: MutableMapping) -> None:
function load_codex_config (line 103) | def load_codex_config(config_toml: Path, config_toml_default: Path) -> M...
function get_str (line 115) | def get_str(config: Mapping, keypath: str, default: str = "") -> str:
function get_int (line 121) | def get_int(config: Mapping, keypath: str, default: int = 0) -> int:
function get_float (line 127) | def get_float(config: Mapping, keypath: str, default: float = 0.0) -> fl...
function get_bool (line 133) | def get_bool(config: Mapping, keypath: str, *, default: bool = False) ->...
FILE: codex/settings/hypercorn_migrate.py
function _parse_bind (line 20) | def _parse_bind(bind_list: list[str]) -> tuple[str, int]:
function _toml_value (line 38) | def _toml_value(val: object) -> str:
function _transform_hypercorn_config (line 54) | def _transform_hypercorn_config(old: dict):
function _build_codex_toml_line (line 73) | def _build_codex_toml_line(lines: list[str], key: str, value, default):
function _append_granian_ssl_comment (line 81) | def _append_granian_ssl_comment(lines: list[str], old: dict[str, Any]):
function _build_codex_toml (line 97) | def _build_codex_toml(old: dict, default_toml: Path) -> str:
function migrate_hypercorn_config (line 158) | def migrate_hypercorn_config(codex_toml: Path, default_toml: Path) -> None:
FILE: codex/settings/logging.py
class LoguruHandler (line 9) | class LoguruHandler(Handler):
method emit (line 13) | def emit(self, record):
function get_logging_settings (line 26) | def get_logging_settings(loglevel: str | int, *, debug: bool) -> dict[st...
FILE: codex/settings/secret_key.py
function get_secret_key (line 6) | def get_secret_key(config_path) -> str:
FILE: codex/settings/servestatic.py
function immutable_file_test (line 8) | def immutable_file_test(_path, url):
FILE: codex/settings/timezone.py
function get_time_zone (line 6) | def get_time_zone(tz):
FILE: codex/signals/django_signals.py
function connect_signals (line 5) | def connect_signals() -> None:
FILE: codex/signals/os_signals.py
function _shutdown_signal_handler (line 26) | def _shutdown_signal_handler(*_args) -> None:
function _restart_signal_handler (line 34) | def _restart_signal_handler(*_args) -> None:
function bind_signals_to_loop_aux (line 43) | def bind_signals_to_loop_aux(sig_add, signal_names, handler) -> None:
function bind_signals_to_loop (line 50) | def bind_signals_to_loop() -> None:
FILE: codex/startup/__init__.py
function ensure_superuser (line 28) | def ensure_superuser() -> None:
function _delete_orphans (line 41) | def _delete_orphans(model, field, names) -> None:
function init_admin_flags (line 50) | def init_admin_flags() -> None:
function init_timestamps (line 61) | def init_timestamps() -> None:
function init_librarian_statuses (line 75) | def init_librarian_statuses() -> None:
function init_libraries (line 108) | def init_libraries() -> None:
function init_custom_cover_dir (line 117) | def init_custom_cover_dir() -> None:
function update_custom_covers_for_config_dir (line 134) | def update_custom_covers_for_config_dir() -> None:
function create_missing_auth_tokens (line 183) | def create_missing_auth_tokens() -> None:
function ensure_db_rows (line 193) | def ensure_db_rows() -> None:
function codex_init (line 205) | def codex_init() -> bool:
FILE: codex/startup/custom_cover_libraries.py
function _repair_extra_custom_cover_libraries (line 10) | def _repair_extra_custom_cover_libraries(library_model, log) -> None:
function cleanup_custom_cover_libraries (line 22) | def cleanup_custom_cover_libraries(log) -> None:
FILE: codex/startup/db.py
function _has_unapplied_migrations (line 37) | def _has_unapplied_migrations() -> bool:
function _get_backup_db_path (line 54) | def _get_backup_db_path(prefix):
function _backup_db_before_migration (line 59) | def _backup_db_before_migration() -> None:
function _repair_db (line 67) | def _repair_db(log) -> None:
function _rebuild_db (line 80) | def _rebuild_db() -> bool:
function ensure_db_schema (line 112) | def ensure_db_schema() -> bool:
FILE: codex/startup/loguru.py
function _log_format (line 11) | def _log_format() -> str:
function loguru_init (line 23) | def loguru_init() -> None:
FILE: codex/startup/registration.py
function patch_registration_setting (line 9) | def patch_registration_setting() -> None:
FILE: codex/templates/pwa/serviceworker.js
constant CACHE_PREFIX (line 2) | const CACHE_PREFIX = "codex-pwa-v";
constant STATIC_CACHE_NAME (line 3) | const STATIC_CACHE_NAME = CACHE_PREFIX + new Date().getSeconds();
constant OFFLINE_PATH (line 4) | const OFFLINE_PATH = "{% static 'pwa/offline.html' %}";
constant FILES_TO_CACHE (line 5) | const FILES_TO_CACHE = [
FILE: codex/urls/converters.py
class GroupConverter (line 7) | class GroupConverter(StringConverter):
class IntListConverter (line 13) | class IntListConverter:
method to_python (line 19) | def to_python(self, value) -> tuple:
method to_url (line 36) | def to_url(self, value) -> str:
FILE: codex/urls/spectacular.py
function allow_list (line 6) | def allow_list(endpoints) -> list:
FILE: codex/util.py
function max_none (line 6) | def max_none(*args):
function mapping_to_dict (line 11) | def mapping_to_dict(data) -> dict | set | frozenset | tuple | list:
function flatten (line 20) | def flatten(seq: tuple | list | frozenset | set):
FILE: codex/version.py
function get_version (line 8) | def get_version() -> str:
FILE: codex/views/admin/api_key.py
class AdminAPIKey (line 11) | class AdminAPIKey(AdminGenericAPIView):
method put (line 18) | def put(self, *_args, **_kwargs) -> Response:
FILE: codex/views/admin/auth.py
class AdminAuthMixin (line 9) | class AdminAuthMixin:
class AdminAPIView (line 15) | class AdminAPIView(AdminAuthMixin, APIView):
class AdminGenericAPIView (line 19) | class AdminGenericAPIView(AdminAuthMixin, GenericAPIView):
class AdminModelViewSet (line 23) | class AdminModelViewSet(AdminAuthMixin, ModelViewSet):
class AdminReadOnlyModelViewSet (line 27) | class AdminReadOnlyModelViewSet(AdminAuthMixin, ReadOnlyModelViewSet):
FILE: codex/views/admin/flag.py
class AdminFlagViewSet (line 24) | class AdminFlagViewSet(AdminModelViewSet):
method _on_change (line 31) | def _on_change(self) -> None:
method perform_update (line 45) | def perform_update(self, serializer) -> None:
FILE: codex/views/admin/group.py
class AdminGroupViewSet (line 14) | class AdminGroupViewSet(AdminModelViewSet):
method _on_change (line 24) | def _on_change(self, validated_data=None) -> None:
method get_serializer (line 33) | def get_serializer(self, *args, **kwargs):
method perform_update (line 39) | def perform_update(self, serializer) -> None:
method perform_create (line 46) | def perform_create(self, serializer) -> None:
method perform_destroy (line 53) | def perform_destroy(self, instance) -> None:
FILE: codex/views/admin/library.py
class AdminLibraryViewSet (line 41) | class AdminLibraryViewSet(AdminModelViewSet):
method _sync_watcher (line 63) | def _sync_watcher(cls, validated_keys=None) -> None:
method _on_change (line 71) | def _on_change() -> None:
method _create_library_folder (line 76) | def _create_library_folder(self, library) -> None:
method _poll (line 83) | def _poll(pk, force) -> None:
method perform_create (line 88) | def perform_create(self, serializer) -> None:
method perform_update (line 101) | def perform_update(self, serializer) -> None:
method perform_destroy (line 115) | def perform_destroy(self, instance) -> None:
class AdminFailedImportViewSet (line 124) | class AdminFailedImportViewSet(AdminModelViewSet):
class AdminFolderListView (line 131) | class AdminFolderListView(AdminGenericAPIView):
method _get_dirs (line 138) | def _get_dirs(root_path, show_hidden) -> tuple:
method get (line 153) | def get(self, *_args, **_kwargs) -> Response:
FILE: codex/views/admin/permissions.py
class HasAPIKeyOrIsAdminUser (line 10) | class HasAPIKeyOrIsAdminUser(BasePermission):
method has_permission (line 14) | def has_permission(self, request, view) -> bool:
FILE: codex/views/admin/stats.py
class AdminStatsView (line 21) | class AdminStatsView(AdminGenericAPIView):
method __init__ (line 28) | def __init__(self, *args, **kwargs) -> None:
method params (line 34) | def params(self) -> MappingProxyType[str, Any]:
method _add_api_key (line 51) | def _add_api_key(self, obj) -> None:
method get_object (line 62) | def get_object(self) -> dict:
method get (line 70) | def get(self, *_args, **_kwargs) -> Response:
FILE: codex/views/admin/tasks.py
class AdminLibrarianStatusActiveViewSet (line 120) | class AdminLibrarianStatusActiveViewSet(AdminReadOnlyModelViewSet):
class AdminLibrarianStatusAllViewSet (line 129) | class AdminLibrarianStatusAllViewSet(AdminReadOnlyModelViewSet):
class AdminLibrarianTaskView (line 136) | class AdminLibrarianTaskView(AdminAPIView):
method _get_task (line 142) | def _get_task(self, name, pk) -> LibrarianTask | None:
method post (line 155) | def post(self, *_args, **_kwargs) -> Response:
FILE: codex/views/admin/user.py
class AdminUserViewSet (line 25) | class AdminUserViewSet(AdminModelViewSet):
method _on_change (line 37) | def _on_change(uid: int) -> None:
method get_serializer (line 50) | def get_serializer(self, *args, **kwargs):
method _is_change_to_current_user (line 56) | def _is_change_to_current_user(self) -> bool:
method destroy (line 61) | def destroy(self, request, *args, **kwargs) -> Response:
method perform_update (line 71) | def perform_update(self, serializer) -> None:
method perform_create (line 86) | def perform_create(self, serializer) -> None:
class AdminUserChangePasswordView (line 100) | class AdminUserChangePasswordView(AdminGenericAPIView):
method put (line 105) | def put(self, request, *args, **kwargs) -> Response:
FILE: codex/views/auth.py
class IsAuthenticatedOrEnabledNonUsers (line 21) | class IsAuthenticatedOrEnabledNonUsers(IsAuthenticated):
method has_permission (line 27) | def has_permission(self, request, view) -> bool:
class AuthMixin (line 37) | class AuthMixin:
class AuthAPIView (line 45) | class AuthAPIView(AuthMixin, APIView): # pyright: ignore[reportIncompat...
class AuthGenericAPIView (line 49) | class AuthGenericAPIView(AuthMixin, GenericAPIView): # pyright: ignore[...
class GroupACLMixin (line 53) | class GroupACLMixin:
method init_group_acl (line 56) | def init_group_acl(self) -> None:
method is_admin (line 61) | def is_admin(self) -> bool:
method get_rel_prefix (line 69) | def get_rel_prefix(model) -> str:
method get_group_acl_filter (line 80) | def get_group_acl_filter(cls, model, user) -> Q:
class AuthFilterGenericAPIView (line 112) | class AuthFilterGenericAPIView(AuthGenericAPIView, GroupACLMixin):
method __init__ (line 115) | def __init__(self, *args, **kwargs) -> None:
class AuthFilterAPIView (line 121) | class AuthFilterAPIView(AuthAPIView, GroupACLMixin):
method __init__ (line 124) | def __init__(self, *args, **kwargs) -> None:
class AuthToken (line 130) | class AuthToken(AuthGenericAPIView):
method get (line 135) | def get(self, *args, **kwargs) -> Response:
method put (line 149) | def put(self, *args, **kwargs) -> Response:
FILE: codex/views/bookmark.py
class BookmarkFilterMixin (line 20) | class BookmarkFilterMixin(GroupACLMixin, ABC):
method init_bookmark_filter (line 23) | def init_bookmark_filter(self) -> None:
method get_bm_rel (line 30) | def get_bm_rel(self, model):
method get_my_bookmark_filter (line 37) | def get_my_bookmark_filter(self, bm_rel) -> Q:
class BookmarkAuthMixin (line 49) | class BookmarkAuthMixin:
method get_bookmark_auth_filter (line 52) | def get_bookmark_auth_filter(self) -> dict[str, int | str | None]:
class BookmarkPageMixin (line 68) | class BookmarkPageMixin(BookmarkAuthMixin):
method update_bookmark (line 71) | def update_bookmark(self) -> None:
class BookmarkPageView (line 87) | class BookmarkPageView(BookmarkPageMixin, AuthAPIView):
method put (line 90) | def put(self, *_args, **_kwargs) -> Response:
FILE: codex/views/browser/annotate/bookmark.py
class BrowserAnnotateBookmarkView (line 21) | class BrowserAnnotateBookmarkView(BrowserAnnotateOrderView):
method _get_group_bookmark_page_annotation (line 24) | def _get_group_bookmark_page_annotation(
method _get_group_bookmark_finished_annotation (line 48) | def _get_group_bookmark_finished_annotation(
method annotate_bookmarks (line 69) | def annotate_bookmarks(self, qs):
method annotate_progress (line 103) | def annotate_progress(self, qs):
FILE: codex/views/browser/annotate/card.py
class BrowserAnnotateCardView (line 30) | class BrowserAnnotateCardView(BrowserAnnotateBookmarkView):
method add_group_by (line 33) | def add_group_by(self, qs):
method _annotate_group (line 40) | def _annotate_group(self, qs):
method _annotate_file_name (line 45) | def _annotate_file_name(self, qs):
method _annotate_has_metadata (line 55) | def _annotate_has_metadata(self, qs):
method annotate_card_aggregates (line 61) | def annotate_card_aggregates(self, qs):
FILE: codex/views/browser/annotate/order.py
class BrowserAnnotateOrderView (line 58) | class BrowserAnnotateOrderView(BrowserOrderByView, SharedAnnotationsMixin):
method __init__ (line 66) | def __init__(self, *args, **kwargs) -> None:
method opds_acquisition_groups (line 76) | def opds_acquisition_groups(self):
method is_opds_acquisition (line 85) | def is_opds_acquisition(self) -> bool:
method order_agg_func (line 99) | def order_agg_func(self):
method _alias_sort_names (line 106) | def _alias_sort_names(self, qs):
method get_filename_func (line 124) | def get_filename_func(self, model) -> Right:
method _alias_filename (line 139) | def _alias_filename(self, qs):
method _alias_story_arc_number (line 150) | def _alias_story_arc_number(self, qs):
method _annotate_page_count (line 176) | def _annotate_page_count(self, qs):
method _annotate_bookmark_updated_at (line 193) | def _annotate_bookmark_updated_at(self, qs) -> QuerySet:
method _annotate_search_scores (line 205) | def _annotate_search_scores(self, qs):
method annotate_child_count (line 217) | def annotate_child_count(self, qs):
method _annotate_order_child_count (line 228) | def _annotate_order_child_count(self, qs):
method annotate_order_value (line 234) | def annotate_order_value(self, qs):
method annotate_order_aggregates (line 261) | def annotate_order_aggregates(self, qs: QuerySet):
FILE: codex/views/browser/bookmark.py
class BookmarkView (line 20) | class BookmarkView(BookmarkUpdateMixin, BookmarkAuthMixin, BrowserFilter...
method __init__ (line 27) | def __init__(self, *args, **kwargs) -> None:
method _parse_params (line 32) | def _parse_params(self):
method _get_comic_query (line 46) | def _get_comic_query(self):
method patch (line 53) | def patch(self, *_args, **_kwargs) -> Response:
method params (line 65) | def params(self):
FILE: codex/views/browser/breadcrumbs.py
class BrowserBreadcrumbsView (line 52) | class BrowserBreadcrumbsView(BrowserPaginateView):
method __init__ (line 55) | def __init__(self, *args, **kwargs) -> None:
method _get_group_query (line 61) | def _get_group_query(self, model):
method _handle_group_query_missing_model (line 70) | def _handle_group_query_missing_model(self, model) -> QuerySet:
method group_instance (line 86) | def group_instance(self) -> BrowserGroupModel | None:
method _build_group_breadcrumbs (line 104) | def _build_group_breadcrumbs(self) -> tuple[Route, ...]:
method _build_folder_breadcrumbs (line 133) | def _build_folder_breadcrumbs(self) -> tuple[Route, ...]:
method _build_story_arc_breadcrumbs (line 154) | def _build_story_arc_breadcrumbs(self) -> tuple[Route, ...]:
method get_breadcrumbs (line 169) | def get_breadcrumbs(self) -> tuple[Route, ...]:
FILE: codex/views/browser/browser.py
class BrowserView (line 30) | class BrowserView(BrowserTitleView):
method model_group (line 47) | def model_group(self):
method _get_limit (line 72) | def _get_limit(self):
method _get_common_queryset (line 83) | def _get_common_queryset(self, model) -> tuple:
method _get_group_queryset (line 110) | def _get_group_queryset(self) -> tuple:
method _get_book_queryset (line 120) | def _get_book_queryset(self) -> tuple:
method _get_zero_pad (line 130) | def _get_zero_pad(book_qs) -> int:
method _get_page_mtime (line 140) | def _get_page_mtime(self):
method _debug_queries (line 143) | def _debug_queries(self, group_count, book_count, group_qs, book_qs) -...
method get_book_qs (line 152) | def get_book_qs(self) -> tuple:
method _get_group_and_books (line 165) | def _get_group_and_books(self) -> tuple:
method get_object (line 195) | def get_object(self) -> MappingProxyType:
method get (line 227) | def get(self, *_args, **_kwargs) -> Response:
FILE: codex/views/browser/choices.py
class BrowserChoicesViewBase (line 60) | class BrowserChoicesViewBase(BrowserFilterView):
method get_field_choices_query (line 69) | def get_field_choices_query(comic_qs, field_name):
method get_m2m_field_query (line 73) | def get_m2m_field_query(self, model, comic_qs: QuerySet):
method does_m2m_null_exist (line 80) | def does_m2m_null_exist(comic_qs, rel):
method get_rel_and_model (line 85) | def get_rel_and_model(self, field_name) -> tuple:
method get_object (line 100) | def get_object(self) -> QuerySet:
method get (line 105) | def get(self, *_args, **_kwargs) -> Response:
class BrowserChoicesAvailableView (line 112) | class BrowserChoicesAvailableView(BrowserChoicesViewBase):
method _is_field_choices_exists (line 118) | def _is_field_choices_exists(cls, comic_qs, field_name) -> bool:
method _is_m2m_field_choices_exists (line 123) | def _is_m2m_field_choices_exists(self, model, comic_qs, rel) -> bool:
method _is_filter_field_choices_exists (line 137) | def _is_filter_field_choices_exists(self, qs: QuerySet, field_name: st...
method get_object (line 151) | def get_object(self) -> dict[str, Any]: # pyright: ignore[reportIncom...
class BrowserChoicesView (line 169) | class BrowserChoicesView(BrowserChoicesViewBase):
method _get_m2m_field_choices (line 174) | def _get_m2m_field_choices(self, model, comic_qs, rel):
method _get_field_name (line 193) | def _get_field_name(self):
method get_object (line 198) | def get_object(self) -> dict[str, Any]: # pyright: ignore[reportIncom...
FILE: codex/views/browser/cover.py
class WEBPRenderer (line 31) | class WEBPRenderer(BaseRenderer):
method render (line 40) | def render(self, data, *_args, **_kwargs) -> Any:
class CoverView (line 45) | class CoverView(BrowserAnnotateOrderView):
method get_group_filter (line 56) | def get_group_filter(self, group=None, pks=None, *, page_mtime=False) ...
method _get_comic_cover (line 78) | def _get_comic_cover(self) -> tuple:
method _get_custom_cover (line 82) | def _get_custom_cover(self) -> CustomCover | None:
method _get_dynamic_cover (line 94) | def _get_dynamic_cover(self) -> tuple:
method _get_cover_pk (line 104) | def _get_cover_pk(self) -> tuple[int, bool]:
method _get_missing_cover_path (line 115) | def _get_missing_cover_path(self) -> tuple:
method _get_cover_data (line 128) | def _get_cover_data(self, pk, *, custom: bool) -> tuple:
method get (line 148) | def get(self, *args, **kwargs) -> HttpResponse:
FILE: codex/views/browser/download.py
class GroupDownloadView (line 14) | class GroupDownloadView(BrowserFilterView):
method get_object (line 22) | def get_object(self) -> tuple[str, ...]:
method get (line 45) | def get(self, *_args, **kwargs) -> FileResponse:
FILE: codex/views/browser/filters/bookmark.py
class BrowserFilterBookmarkView (line 9) | class BrowserFilterBookmarkView(BookmarkFilterMixin, BrowserValidateView):
method __init__ (line 12) | def __init__(self, *args, **kwargs) -> None:
method get_bookmark_filter (line 17) | def get_bookmark_filter(self, model):
FILE: codex/views/browser/filters/field.py
class ComicFieldFilterView (line 25) | class ComicFieldFilterView(GroupFilterView):
method _filter_by_comic_field (line 29) | def _filter_by_comic_field(field, rel_prefix, filter_list) -> Q:
method get_all_comic_field_filters (line 47) | def get_all_comic_field_filters(cls, rel_prefix, filters) -> Q:
method get_comic_field_filter (line 57) | def get_comic_field_filter(self, model) -> Q:
FILE: codex/views/browser/filters/filter.py
class BrowserFilterView (line 10) | class BrowserFilterView(BrowserFilterBookmarkView):
method force_inner_joins (line 13) | def force_inner_joins(self, qs):
method _get_query_filters (line 23) | def _get_query_filters(
method get_filtered_queryset (line 47) | def get_filtered_queryset(
FILE: codex/views/browser/filters/group.py
class GroupFilterView (line 16) | class GroupFilterView(BrowserParamsView):
method _get_rel_for_pks (line 21) | def _get_rel_for_pks(self, group, *, page_mtime: bool):
method get_group_filter (line 37) | def get_group_filter(self, group=None, pks=None, *, page_mtime=False) ...
FILE: codex/views/browser/filters/search/field/column.py
function _parse_field_rel (line 32) | def _parse_field_rel(field_name, rel_class) -> tuple:
function parse_field (line 49) | def parse_field(field_name: str) -> tuple:
FILE: codex/views/browser/filters/search/field/expression.py
function parse_size (line 34) | def parse_size(s: str) -> int:
function _parse_issue_value (line 43) | def _parse_issue_value(value) -> tuple | tuple[None, None]:
function _parse_issue_values (line 56) | def _parse_issue_values(rel, value, to_value=None) -> dict:
function _cast_value (line 81) | def _cast_value(rel, rel_class, value) -> int | Decimal | bool | date | ...
function _glob_to_lookup (line 99) | def _glob_to_lookup(value) -> tuple[str, str]:
function _parse_operator_numeric (line 123) | def _parse_operator_numeric(rel, rel_class, value) -> dict[Any, int | No...
function _parse_operator_text (line 130) | def _parse_operator_text(rel, exp) -> dict[Any, str] | dict:
function _parse_operator (line 140) | def _parse_operator(operator, rel, rel_class, exp) -> dict:
function _parse_operator_range (line 150) | def _parse_operator_range(rel, rel_class, value) -> dict:
function parse_expression (line 167) | def parse_expression(rel, rel_class, exp) -> dict:
FILE: codex/views/browser/filters/search/field/filter.py
class BrowserFieldQueryFilter (line 14) | class BrowserFieldQueryFilter(ComicFieldFilterView):
method _combine_q (line 18) | def _combine_q(q: Q, other_q: tuple[str, Any] | Q, op: str) -> Q:
method _hoist_filters (line 33) | def _hoist_filters(
method _parse_field_query (line 50) | def _parse_field_query(
method _parse_compound_field_query (line 71) | def _parse_compound_field_query(
method get_search_field_filters (line 96) | def get_search_field_filters(self, model, field_token_pairs) -> tuple[...
FILE: codex/views/browser/filters/search/field/optimize.py
function _like_to_regex (line 12) | def _like_to_regex(like):
function _regex_like (line 42) | def _regex_like(regex, lookahead):
function like_qs_to_regex_q (line 49) | def like_qs_to_regex_q(q: Q, regex_op: str, *, many_to_many: bool) -> Q:
FILE: codex/views/browser/filters/search/field/parse.py
class FieldQueryTransformer (line 49) | class FieldQueryTransformer(Transformer):
method __init__ (line 52) | def __init__(
method _prefix_q_dict (line 67) | def _prefix_q_dict(self, q_dict: dict) -> dict:
method _make_operand_q (line 81) | def _make_operand_q(self, token: Token) -> Q:
method QUOTED (line 89) | def QUOTED(self, token: Token) -> Q: # noqa: N802
method WORD (line 93) | def WORD(self, token: Token) -> Q: # noqa: N802
method not_op (line 97) | def not_op(self, args: list[Any]) -> Q:
method or_expr (line 101) | def or_expr(self, args: list[Any]) -> Q:
method and_expr (line 109) | def and_expr(self, args: list[Any]) -> Q:
function get_field_query (line 118) | def get_field_query(
FILE: codex/views/browser/filters/search/fts.py
class BrowserFTSFilter (line 8) | class BrowserFTSFilter(BrowserFieldQueryFilter):
method get_fts_filter (line 11) | def get_fts_filter(self, model, text) -> dict:
FILE: codex/views/browser/filters/search/parse.py
class SearchFilterView (line 76) | class SearchFilterView(BrowserFTSFilter):
method __init__ (line 81) | def __init__(self, *args, **kwargs) -> None:
method admin_flags (line 90) | def admin_flags(self) -> MappingProxyType[str, bool]:
method _is_path_column_allowed (line 106) | def _is_path_column_allowed(self) -> bool:
method _is_column_operators_used (line 111) | def _is_column_operators_used(exp) -> bool:
method _add_field_token (line 118) | def _add_field_token(self, preop, col, exp, field_tokens) -> None:
method _parse_column_match (line 126) | def _parse_column_match(
method _add_fts_token (line 147) | def _add_fts_token(fts_tokens, token) -> None:
method _preparse_search_query_token (line 161) | def _preparse_search_query_token(self, match, field_tokens, fts_tokens...
method _preparse_search_query (line 182) | def _preparse_search_query(self) -> tuple[dict, str] | tuple:
method _create_search_filters (line 200) | def _create_search_filters(self, model) -> tuple[list, list, Q]:
method _create_search_filter (line 229) | def _create_search_filter(self, filter_list) -> Q:
method get_search_filters (line 240) | def get_search_filters(self, model) -> tuple[Q, Q, Q]:
method get_search_limit (line 261) | def get_search_limit(self) -> int:
FILE: codex/views/browser/group_mtime.py
class BrowserGroupMtimeView (line 20) | class BrowserGroupMtimeView(BrowserFilterView):
method __init__ (line 23) | def __init__(self, *args, **kwargs) -> None:
method is_bookmark_filtered (line 29) | def is_bookmark_filtered(self) -> bool:
method _handle_operational_error (line 37) | def _handle_operational_error(self, err) -> None:
method get_max_bookmark_updated_at_aggregate (line 48) | def get_max_bookmark_updated_at_aggregate(
method get_group_mtime (line 65) | def get_group_mtime(self, model, group=None, pks=None, *, page_mtime=F...
FILE: codex/views/browser/metadata/__init__.py
class MetadataView (line 21) | class MetadataView(MetadataCopyIntersectionsView):
method _get_valid_browse_nav_groups (line 32) | def _get_valid_browse_nav_groups(self, valid_top_groups) -> tuple:
method _raise_not_found (line 38) | def _raise_not_found(self, exc=None) -> None:
method _get_first_object (line 45) | def _get_first_object(self, qs: QuerySet):
method _aggregate_multi_pk_sums (line 52) | def _aggregate_multi_pk_sums(self, filtered_qs, obj):
method get_object (line 72) | def get_object(self) -> Any:
method get (line 108) | def get(self, *_args, **_kwargs) -> Response:
FILE: codex/views/browser/metadata/annotate.py
class MetadataAnnotateView (line 19) | class MetadataAnnotateView(BrowserAnnotateCardView):
method _get_comic_value_fields (line 22) | def _get_comic_value_fields(self) -> tuple:
method _intersection_annotate_separate_sum_fields (line 34) | def _intersection_annotate_separate_sum_fields(
method _intersection_annotate_count_sum_fields (line 47) | def _intersection_annotate_count_sum_fields(
method _intersection_annotate_count_intersection_fields (line 57) | def _intersection_annotate_count_intersection_fields(
method _intersection_annotate_fetch_intersecting_values (line 77) | def _intersection_annotate_fetch_intersecting_values(
method _intersection_annotate (line 94) | def _intersection_annotate(
method annotate_values_and_fks (line 144) | def annotate_values_and_fks(self, qs, filtered_qs):
FILE: codex/views/browser/metadata/copy_intersections.py
class MetadataCopyIntersectionsView (line 17) | class MetadataCopyIntersectionsView(MetadataQueryIntersectionsView):
method _path_security (line 20) | def _path_security(self, obj) -> None:
method _highlight_current_group (line 32) | def _highlight_current_group(self, obj) -> None:
method _copy_m2m_intersections (line 42) | def _copy_m2m_intersections(cls, obj, m2m_intersections) -> None:
method _copy_groups (line 58) | def _copy_groups(obj, groups) -> None:
method _copy_fks (line 63) | def _copy_fks(obj, fks) -> None:
method _copy_conflicting_simple_fields (line 68) | def _copy_conflicting_simple_fields(obj) -> None:
method copy_intersections_into_comic_fields (line 75) | def copy_intersections_into_comic_fields(
FILE: codex/views/browser/metadata/query_intersections.py
class MetadataQueryIntersectionsView (line 19) | class MetadataQueryIntersectionsView(MetadataAnnotateView):
method _query_groups (line 22) | def _query_groups(self) -> dict:
method _get_comic_pks (line 48) | def _get_comic_pks(self, filtered_qs: QuerySet) -> frozenset[int]:
method _get_fk_intersection_query (line 55) | def _get_fk_intersection_query(
method _query_fk_intersections (line 71) | def _query_fk_intersections(self, comic_pks: frozenset[int]) -> dict:
method _get_m2m_intersection_query (line 82) | def _get_m2m_intersection_query(
method _get_optimized_m2m_query (line 96) | def _get_optimized_m2m_query(qs):
method _query_m2m_intersections (line 105) | def _query_m2m_intersections(self, comic_pks: frozenset[int]) -> dict:
method query_intersections (line 131) | def query_intersections(self, filtered_qs) -> tuple[dict, dict, dict]:
FILE: codex/views/browser/mtime.py
class MtimeView (line 14) | class MtimeView(BrowserGroupMtimeView):
method _get_group_mtime (line 22) | def _get_group_mtime(self, item):
method get_max_groups_mtime (line 33) | def get_max_groups_mtime(self):
method get (line 43) | def get(self, *args, **kwargs) -> Response:
FILE: codex/views/browser/order_by.py
class BrowserOrderByView (line 8) | class BrowserOrderByView(BrowserGroupMtimeView):
method __init__ (line 11) | def __init__(self, *args, **kwargs) -> None:
method order_key (line 18) | def order_key(self) -> str:
method _add_comic_order_by (line 30) | def _add_comic_order_by(self, order_key, comic_sort_names) -> list:
method add_order_by (line 58) | def add_order_by(self, qs, order_key="", comic_sort_names=None):
FILE: codex/views/browser/page_in_bounds.py
class BrowserPageInBoundsView (line 10) | class BrowserPageInBoundsView(BrowserAnnotateCardView):
method _get_back_one_page_route (line 13) | def _get_back_one_page_route(self, num_pages) -> dict[str, Any]:
method _get_up_page_redirect (line 23) | def _get_up_page_redirect(self) -> tuple[dict, None]:
method _handle_page_out_of_bounds (line 32) | def _handle_page_out_of_bounds(self, num_pages) -> None:
method check_page_in_bounds (line 50) | def check_page_in_bounds(self, num_pages: int) -> None:
FILE: codex/views/browser/paginate.py
class BrowserPaginateView (line 13) | class BrowserPaginateView(BrowserPageInBoundsView):
method _paginate_section (line 16) | def _paginate_section(self, qs: QuerySet, page: int) -> QuerySet:
method _paginate_groups (line 31) | def _paginate_groups(self, group_qs: QuerySet):
method _paginate_books (line 36) | def _paginate_books(self, book_qs, total_group_count, page_group_count...
method paginate (line 59) | def paginate(
FILE: codex/views/browser/params.py
class BrowserParamsView (line 16) | class BrowserParamsView(BrowserSettingsBaseView):
method __init__ (line 23) | def __init__(self, *args, **kwargs) -> None:
method init_params (line 28) | def init_params(self) -> MutableMapping[str, Any]:
method _update_last_route (line 37) | def _update_last_route(self, data: MutableMapping) -> None:
method set_params (line 49) | def set_params(self, params: Mapping) -> None:
method params (line 54) | def params(self) -> MappingProxyType:
FILE: codex/views/browser/saved_settings.py
function _validate_filter_field (line 70) | def _validate_filter_field(
function _validate_filter_pks (line 94) | def _validate_filter_pks(
class _SavedSettingsOwnerMixin (line 119) | class _SavedSettingsOwnerMixin:
method _get_user_and_session (line 122) | def _get_user_and_session(self):
method _owner_kwargs (line 130) | def _owner_kwargs(self):
class SavedBrowserSettingsListView (line 137) | class SavedBrowserSettingsListView(_SavedSettingsOwnerMixin, AuthFilterG...
method get (line 142) | def get(self, *args, **kwargs) -> Response:
method _copy_settings (line 155) | def _copy_settings(source: SettingsBrowser, target: SettingsBrowser):
method post (line 178) | def post(self, *args, **kwargs) -> Response:
class SavedBrowserSettingsLoadView (line 248) | class SavedBrowserSettingsLoadView(SettingsBaseView):
method _owner_kwargs (line 258) | def _owner_kwargs(self):
method get (line 265) | def get(self, *args, **kwargs) -> Response:
method delete (line 293) | def delete(self, *args, **kwargs) -> Response:
FILE: codex/views/browser/settings.py
class BrowserSettingsBaseView (line 26) | class BrowserSettingsBaseView(SettingsBaseView):
method set_order_by_default (line 34) | def set_order_by_default(self, params: MutableMapping) -> None:
method reset_browser_settings (line 48) | def reset_browser_settings(self) -> dict:
class BrowserSettingsView (line 81) | class BrowserSettingsView(BrowserSettingsBaseView):
method _validate_browse_top_group (line 92) | def _validate_browse_top_group(params, group: str, top_group: str) -> ...
method _validate_top_group (line 110) | def _validate_top_group(cls, params, group: str, top_group: str) -> None:
method _validate_settings_get (line 120) | def _validate_settings_get(self, validated_data, params: dict) -> dict:
method get (line 133) | def get(self, *args, **kwargs) -> Response:
method patch (line 145) | def patch(self, *args, **kwargs) -> Response:
method delete (line 158) | def delete(self, *args, **kwargs) -> Response:
FILE: codex/views/browser/title.py
class BrowserTitleView (line 9) | class BrowserTitleView(BrowserBreadcrumbsView):
method _get_root_group_name (line 12) | def _get_root_group_name(self) -> tuple:
method _get_group_name (line 22) | def _get_group_name(self) -> tuple:
method get_browser_page_title (line 36) | def get_browser_page_title(self) -> Mapping:
FILE: codex/views/browser/validate.py
class BrowserValidateView (line 25) | class BrowserValidateView(SearchFilterView):
method __init__ (line 32) | def __init__(self, *args, **kwargs) -> None:
method model_group (line 42) | def model_group(self) -> str:
method model (line 52) | def model(self) -> type[BrowserGroupModel] | None:
method rel_prefix (line 65) | def rel_prefix(self) -> str:
method raise_redirect (line 71) | def raise_redirect(
method _get_valid_browse_top_groups (line 84) | def _get_valid_browse_top_groups(self) -> list:
method _validate_top_group (line 101) | def _validate_top_group(self, valid_top_groups) -> None:
method _get_valid_browse_nav_groups (line 119) | def _get_valid_browse_nav_groups(self, valid_top_groups) -> tuple:
method _validate_folder_settings (line 145) | def _validate_folder_settings(self) -> tuple:
method _validate_browser_group_settings (line 158) | def _validate_browser_group_settings(self) -> tuple:
method _validate_story_arc_settings (line 177) | def _validate_story_arc_settings(self) -> tuple[str, ...]:
method valid_nav_groups (line 184) | def valid_nav_groups(self) -> tuple[str, ...]:
FILE: codex/views/download.py
class DownloadView (line 13) | class DownloadView(AuthFilterAPIView):
method get (line 21) | def get(self, *_args, **kwargs) -> FileResponse:
class FileView (line 55) | class FileView(DownloadView):
FILE: codex/views/error.py
function codex_exception_handler (line 10) | def codex_exception_handler(
FILE: codex/views/exceptions.py
class SeeOtherRedirectError (line 35) | class SeeOtherRedirectError(APIException):
method _copy_params_into (line 43) | def _copy_params_into(
method __init__ (line 53) | def __init__(self, detail) -> None:
method _get_query_params (line 82) | def _get_query_params(self) -> dict:
method get_response (line 91) | def get_response(self, url_name) -> HttpResponseRedirect:
class NoContent (line 99) | class NoContent(APIException):
FILE: codex/views/frontend.py
class IndexView (line 15) | class IndexView(BrowserSettingsBaseView, UserActiveMixin):
method _get_last_route (line 22) | def _get_last_route(self) -> ReturnDict:
method get (line 28) | def get(self, *_args, **_kwargs) -> Response:
FILE: codex/views/healthcheck.py
function health_check_view (line 6) | def health_check_view(request) -> HttpResponse: # noqa: ARG001
FILE: codex/views/lazy_import.py
class LazyImportView (line 11) | class LazyImportView(AuthGenericAPIView):
method get (line 16) | def get(self, *args, **kwargs) -> Response:
FILE: codex/views/mixins.py
class SharedAnnotationsMixin (line 23) | class SharedAnnotationsMixin: # (BrowserFilterView):
method _get_order_group (line 27) | def _get_order_group(
method _get_order_groups (line 42) | def _get_order_groups(cls, parent_group, pks, show) -> tuple:
method get_sort_name_annotations (line 57) | def get_sort_name_annotations(cls, model, parent_group, pks, show) -> ...
method _volume_name_annotation (line 73) | def _volume_name_annotation(model) -> Case:
method annotate_group_names (line 89) | def annotate_group_names(cls, qs):
class UserActiveMixin (line 114) | class UserActiveMixin:
method mark_user_active (line 117) | def mark_user_active(self) -> None:
FILE: codex/views/opds/auth.py
class OPDSAuthMixin (line 13) | class OPDSAuthMixin(AuthMixin):
method user_agent_name (line 23) | def user_agent_name(self) -> str:
FILE: codex/views/opds/authentication/v1.py
class OPDSAuthentication1View (line 51) | class OPDSAuthentication1View(GenericAPIView):
method _absolute_doc (line 57) | def _absolute_doc(request) -> dict:
method static_get (line 67) | def static_get(cls, request, status_code=status.HTTP_200_OK) -> JsonRe...
method get (line 77) | def get(self, *args, **kwargs) -> JsonResponse:
FILE: codex/views/opds/binary.py
class IgnoreClientContentNegotiation (line 14) | class IgnoreClientContentNegotiation(BaseContentNegotiation):
method select_parser (line 18) | def select_parser(self, request, parsers):
method select_renderer (line 23) | def select_renderer(
class OPDSCoverView (line 34) | class OPDSCoverView(OPDSBrowserSettingsMixin, CoverView):
class OPDSDownloadView (line 38) | class OPDSDownloadView(OPDSAuthMixin, DownloadView):
class OPDSPageView (line 42) | class OPDSPageView(OPDSAuthMixin, ReaderPageView):
FILE: codex/views/opds/const.py
class BookmarkFilters (line 20) | class BookmarkFilters:
class Rel (line 29) | class Rel:
class MimeType (line 56) | class MimeType:
class UserAgentNames (line 100) | class UserAgentNames:
class TopRoutes (line 109) | class TopRoutes:
FILE: codex/views/opds/error.py
function _get_url_name (line 30) | def _get_url_name(request: HttpRequest, name_suffix: str) -> str:
function _get_redirect_to_start_response (line 35) | def _get_redirect_to_start_response(request: HttpRequest) -> HttpRespons...
function codex_opds_exception_handler (line 42) | def codex_opds_exception_handler(
FILE: codex/views/opds/feed.py
class OPDSBrowserView (line 12) | class OPDSBrowserView(OPDSBrowserSettingsMixin, UserActiveMixin, Browser...
method __init__ (line 18) | def __init__(self, *args, **kwargs) -> None:
FILE: codex/views/opds/metadata.py
function get_credit_people (line 26) | def get_credit_people(comic_pks: Sequence[int], roles: Iterable[str], *,...
function get_credits (line 38) | def get_credits(
function get_m2m_objects (line 50) | def get_m2m_objects(pks: Sequence[int]) -> dict:
FILE: codex/views/opds/opensearch/v1.py
class OpenSearch1View (line 14) | class OpenSearch1View(OPDSAuthMixin, CodexXMLTemplateMixin, CodexAPIView...
FILE: codex/views/opds/settings.py
class OPDSSettingsMixin (line 9) | class OPDSSettingsMixin(OPDSAuthMixin, ABC):
class OPDSBrowserSettingsMixin (line 16) | class OPDSBrowserSettingsMixin(OPDSSettingsMixin):
FILE: codex/views/opds/start.py
class OPDSStartViewMixin (line 7) | class OPDSStartViewMixin:
method init_params (line 12) | def init_params(self) -> MutableMapping[str, Any]:
method _get_group_queryset (line 16) | def _get_group_queryset(self) -> tuple:
FILE: codex/views/opds/urls.py
class OPDSURLsView (line 15) | class OPDSURLsView(AuthGenericAPIView):
method get (line 20) | def get(self, *args, **kwargs) -> Response:
FILE: codex/views/opds/user_agent.py
function get_user_agent_name (line 6) | def get_user_agent_name(request: Request) -> str:
FILE: codex/views/opds/v1/const.py
class TopLink (line 21) | class TopLink:
class TopLinks (line 34) | class TopLinks:
class RootTopLinks (line 50) | class RootTopLinks:
class OPDS1Link (line 97) | class OPDS1Link:
class FacetGroup (line 114) | class FacetGroup:
class Facet (line 124) | class Facet:
class FacetGroups (line 131) | class FacetGroups:
class RootFacetGroups (line 153) | class RootFacetGroups:
class OpdsNs (line 170) | class OpdsNs:
class OPDS1EntryObject (line 178) | class OPDS1EntryObject:
class OPDS1EntryData (line 190) | class OPDS1EntryData:
FILE: codex/views/opds/v1/entry/entry.py
class OPDS1Entry (line 24) | class OPDS1Entry(OPDS1EntryLinksMixin):
method id_tag (line 28) | def id_tag(self) -> str:
method title (line 34) | def title(self) -> str:
method issued (line 67) | def issued(self) -> str:
method publisher (line 77) | def publisher(self):
method _get_datefield (line 81) | def _get_datefield(self, key) -> datetime | None:
method updated (line 94) | def updated(self) -> datetime | None:
method published (line 99) | def published(self) -> datetime | None:
method language (line 104) | def language(self):
method summary (line 109) | def summary(self):
method _add_url_to_obj (line 119) | def _add_url_to_obj(objs, filter_key) -> list:
method authors (line 132) | def authors(self) -> list:
method contributors (line 140) | def contributors(self) -> list:
method category_groups (line 148) | def category_groups(self) -> dict:
FILE: codex/views/opds/v1/entry/links.py
class OPDS1EntryLinksMixin (line 17) | class OPDS1EntryLinksMixin:
method __init__ (line 20) | def __init__(
method _cover_link (line 38) | def _cover_link(self, rel) -> OPDS1Link | None:
method _nav_href (line 54) | def _nav_href(self, *, metadata: bool) -> str:
method _nav_link (line 80) | def _nav_link(self, *, metadata: bool) -> OPDS1Link:
method _download_link (line 96) | def _download_link(self) -> OPDS1Link | None:
method lazy_metadata (line 106) | def lazy_metadata(self) -> bool:
method _stream_link (line 116) | def _stream_link(self) -> OPDS1Link | None:
method _links_comic (line 138) | def _links_comic(self) -> list:
method links (line 150) | def links(self) -> list:
FILE: codex/views/opds/v1/facets.py
class OPDS1FacetsView (line 25) | class OPDS1FacetsView(CodexXMLTemplateMixin, OPDSBrowserView):
method __init__ (line 31) | def __init__(self, *args, **kwargs) -> None:
method mime_type_map (line 40) | def mime_type_map(self) -> MappingProxyType[str, str]:
method use_facets (line 51) | def use_facets(self) -> bool:
method obj (line 58) | def obj(self) -> MappingProxyType[str, Any]:
method _facet (line 80) | def _facet(self, kwargs, facet_group, facet_title, new_query_params) -...
method _facet_entry (line 102) | def _facet_entry(self, item, facet_group, facet, query_params) -> OPDS...
method _is_facet_active (line 122) | def _is_facet_active(self, facet_group, facet) -> bool:
method _did_special_group_change (line 130) | def _did_special_group_change(group, facet_group) -> bool:
method _facet_or_facet_entry (line 139) | def _facet_or_facet_entry(self, facet_group, facet, *, entries: bool):
method _facet_group (line 156) | def _facet_group(self, facet_group, *, entries: bool) -> list:
method facets (line 173) | def facets(self, *, entries: bool) -> list:
FILE: codex/views/opds/v1/feed.py
class OPDS1FeedView (line 28) | class OPDS1FeedView(OPDS1LinksView):
method version (line 38) | def version(self):
method opds_ns (line 43) | def opds_ns(self):
method is_acquisition (line 51) | def is_acquisition(self) -> bool:
method id_tag (line 56) | def id_tag(self):
method title (line 64) | def title(self) -> str:
method updated (line 84) | def updated(self) -> str:
method items_per_page (line 96) | def items_per_page(self) -> int | None:
method total_results (line 105) | def total_results(self):
method _get_entries_section (line 113) | def _get_entries_section(self, key, metadata) -> list:
method entries (line 139) | def entries(self) -> list:
method get (line 162) | def get(self, *_args, **_kwargs) -> Response:
class OPDS1StartView (line 169) | class OPDS1StartView(OPDSStartViewMixin, OPDS1FeedView):
method get (line 177) | def get(self, *args, **kwargs) -> Response:
FILE: codex/views/opds/v1/links.py
class OPDS1LinksView (line 20) | class OPDS1LinksView(OPDS1FacetsView):
method is_top_link_displayed (line 23) | def is_top_link_displayed(self, top_link) -> bool:
method _link (line 35) | def _link(
method _top_link (line 45) | def _top_link(self, top_link):
method _root_links (line 51) | def _root_links(self) -> list:
method _links_start_page_links (line 69) | def _links_start_page_links(self) -> list:
method _links_facets (line 78) | def _links_facets(self) -> list:
method links (line 90) | def links(self) -> list:
method _top_link_entry (line 116) | def _top_link_entry(self, top_link) -> OPDS1Entry:
method add_start_link (line 136) | def add_start_link(self) -> list[OPDS1Entry]:
method add_top_links (line 149) | def add_top_links(self, top_links) -> list:
FILE: codex/views/opds/v2/const.py
class HrefData (line 14) | class HrefData:
class LinkData (line 27) | class LinkData:
class Link (line 44) | class Link:
class LinkGroup (line 56) | class LinkGroup:
FILE: codex/views/opds/v2/feed/__init__.py
class OPDS2FeedView (line 27) | class OPDS2FeedView(OPDS2FeedGroupsView):
method _subtitle_filters (line 35) | def _subtitle_filters(self, qps: Mapping) -> list[str]:
method _subtitle (line 57) | def _subtitle(self) -> str:
method _title (line 75) | def _title(self, browser_title: Mapping[str, str]):
method _feed_metadata (line 91) | def _feed_metadata(self, title: str, mtime: datetime | None) -> Mappin...
method _feed_navigation_and_groups (line 106) | def _feed_navigation_and_groups(
method _update_feed_modified (line 138) | def _update_feed_modified(
method get_object (line 145) | def get_object(self) -> MappingProxyType:
method get (line 180) | def get(self, *_args, **_kwargs) -> Response:
class OPDS2StartView (line 188) | class OPDS2StartView(OPDSStartViewMixin, OPDS2FeedView):
method _update_feed_modified (line 193) | def _update_feed_modified(
method get (line 211) | def get(self, *args, **kwargs) -> Response:
FILE: codex/views/opds/v2/feed/feed_links.py
class OPDS2FeedLinksView (line 13) | class OPDS2FeedLinksView(OPDS2LinksView):
method _link_auth (line 16) | def _link_auth(self):
method _link_search (line 28) | def _link_search(self):
method _get_static_links (line 38) | def _get_static_links(self):
method _top_route (line 67) | def _top_route(self) -> dict[str, Any]:
method _link_page (line 71) | def _link_page(self, rel, page):
method get_links (line 78) | def get_links(self, up_route):
FILE: codex/views/opds/v2/feed/groups.py
class OPDS2FeedGroupsView (line 23) | class OPDS2FeedGroupsView(OPDS2PublicationsView):
method _create_link_kwargs (line 30) | def _create_link_kwargs(
method _create_link_query_params (line 46) | def _create_link_query_params(
method _create_links_from_link_spec (line 65) | def _create_links_from_link_spec(
method _create_group_from_group_spec (line 102) | def _create_group_from_group_spec(
method _create_group (line 131) | def _create_group(self, group_specs, *, paginate: bool = False) -> list:
method get_top_groups (line 138) | def get_top_groups(self):
method get_ordered_groups (line 142) | def get_ordered_groups(self) -> list:
method get_start_groups (line 152) | def get_start_groups(self):
method get_groups (line 156) | def get_groups(self, group_qs, book_qs, title: str, zero_pad: int):
method get_facets (line 172) | def get_facets(self):
FILE: codex/views/opds/v2/feed/links.py
class OPDS2LinksView (line 20) | class OPDS2LinksView(OPDS2HrefMixin, OPDSBrowserView):
method __init__ (line 25) | def __init__(self, *args, **kwargs) -> None:
method group_and_books (line 35) | def group_and_books(
method num_pages (line 46) | def num_pages(self) -> int:
method _link_attributes (line 53) | def _link_attributes(data, link) -> None:
method _link_properties (line 67) | def _link_properties(data, link) -> None:
method link (line 76) | def link(self, data: LinkData) -> dict | None:
method _normalize_query_params (line 93) | def _normalize_query_params(qps_dict) -> frozenset:
method _is_self_link (line 102) | def _is_self_link(self, href) -> bool:
method link_aggregate (line 115) | def link_aggregate(self, link_dict, link) -> None:
method get_links_from_dict (line 131) | def get_links_from_dict(link_dict) -> list:
method link_self (line 142) | def link_self(self):
FILE: codex/views/opds/v2/feed/publications.py
class OPDS2PublicationBaseView (line 27) | class OPDS2PublicationBaseView(OPDS2FeedLinksView):
method __init__ (line 30) | def __init__(self, *args, **kwargs) -> None:
method is_allowed (line 36) | def is_allowed(link_spec: Link | BrowserGroupModel) -> bool:
method _publication_metadata (line 58) | def _publication_metadata(self, obj, zero_pad) -> dict:
method auth_link (line 83) | def auth_link(self):
method _publication_link (line 95) | def _publication_link(self, kwargs, url_name, rel, mime_type, size=None):
method _publication (line 102) | def _publication(self, obj, zero_pad) -> dict:
method _thumb (line 141) | def _thumb(self, obj) -> list:
class OPDS2PublicationsView (line 171) | class OPDS2PublicationsView(OPDS2PublicationBaseView):
method _publication (line 175) | def _publication(self, obj, zero_pad) -> dict:
method _get_publications_links (line 181) | def _get_publications_links(self, link_spec) -> list:
method _get_publication_section_metadata (line 190) | def _get_publication_section_metadata(
method get_publications (line 209) | def get_publications(
method _get_publications_preview_feed_view (line 241) | def _get_publications_preview_feed_view(self, link_spec: Link):
method get_publications_preview (line 256) | def get_publications_preview(self, link_spec: Link) -> list:
FILE: codex/views/opds/v2/href.py
class OPDS2HrefMixin (line 15) | class OPDS2HrefMixin:
method num_pages (line 19) | def num_pages(self) -> int:
method _href_page_validate (line 23) | def _href_page_validate(self, kwargs, data) -> bool:
method _href_update_query_params (line 30) | def _href_update_query_params(self, data) -> dict:
method href (line 48) | def href(self, data) -> str | None:
FILE: codex/views/opds/v2/manifest.py
class OPDS2ManifestMetadataView (line 55) | class OPDS2ManifestMetadataView(OPDS2PublicationBaseView):
method _publication_identifier (line 58) | def _publication_identifier(self, obj) -> str:
method _publication_belongs_to_link (line 73) | def _publication_belongs_to_link(
method _publication_belongs_to_series (line 92) | def _publication_belongs_to_series(self, obj):
method _publication_belongs_to_folder (line 109) | def _publication_belongs_to_folder(self, obj) -> list:
method _publication_belongs_to_story_arcs (line 122) | def _publication_belongs_to_story_arcs(self, obj) -> list:
method _publication_belongs_to (line 146) | def _publication_belongs_to(self, obj) -> dict:
method _add_tag_link (line 157) | def _add_tag_link(self, obj: BaseModel, filter_key: str, subfield: str...
method _publication_subject (line 176) | def _publication_subject(self, obj) -> tuple[NamedModel, ...]:
method _add_credits (line 186) | def _add_credits(self, pks, roles) -> QuerySet[Credit] | None:
method _publication_credits (line 194) | def _publication_credits(self, obj) -> Mapping[str, tuple[Credit, ...]]:
method _publication_metadata (line 202) | def _publication_metadata(self, obj, zero_pad) -> dict:
class OPDS2ManifestView (line 226) | class OPDS2ManifestView(OPDS2ManifestMetadataView):
method _publication_reading_order (line 231) | def _publication_reading_order(self, obj) -> list:
method _cover (line 261) | def _cover(self, obj) -> list:
method _publication (line 290) | def _publication(self, obj, zero_pad) -> dict:
method get_object (line 300) | def get_object(self) -> MappingProxyType:
FILE: codex/views/opds/v2/progression.py
class ReadiumProgressionParser (line 44) | class ReadiumProgressionParser(JSONParser):
class ReadiumProgressionAPIRenderer (line 50) | class ReadiumProgressionAPIRenderer(JSONRenderer):
class OPDS2ProgressionView (line 59) | class OPDS2ProgressionView(
method __init__ (line 79) | def __init__(self, *args, **kwargs) -> None:
method modified (line 87) | def modified(self):
method device (line 92) | def device(self):
method title (line 97) | def title(self) -> str:
method _progression_href (line 102) | def _progression_href(self):
method _locations (line 115) | def _locations(self) -> dict[str, Any]:
method locator (line 126) | def locator(self) -> dict[str, Any]:
method _get_bookmark_query (line 135) | def _get_bookmark_query(self) -> QuerySet:
method get_object (line 159) | def get_object(self) -> dict[str, Any]:
method get (line 185) | def get(self, *args, **kwargs) -> Response:
method put (line 200) | def put(self, *_args, **_kwargs) -> Response:
FILE: codex/views/public.py
class AdminFlagsView (line 22) | class AdminFlagsView(GenericAPIView, RetrieveModelMixin):
method get_object (line 31) | def get_object(self) -> dict:
method get (line 40) | def get(self, *args, **kwargs) -> Response:
FILE: codex/views/pwa.py
class WebManifestView (line 6) | class WebManifestView(CodexTemplateView):
class ServiceWorkerRegisterView (line 13) | class ServiceWorkerRegisterView(CodexTemplateView):
class ServiceWorkerView (line 20) | class ServiceWorkerView(CodexTemplateView):
FILE: codex/views/reader/arcs.py
class ReaderArcsView (line 26) | class ReaderArcsView(ReaderParamsView):
method _get_field_names (line 29) | def _get_field_names(self) -> tuple:
method _get_group_arc (line 51) | def _get_group_arc(
method _get_story_arcs (line 67) | def _get_story_arcs(self, comic: Comic, arcs, max_mtime: int | None):
method _set_selected_arc (line 97) | def _set_selected_arc(self, arcs) -> None:
method get_arcs (line 118) | def get_arcs(self) -> tuple[dict, int | None]:
FILE: codex/views/reader/books.py
class ReaderBooksView (line 40) | class ReaderBooksView(ReaderArcsView, SharedAnnotationsMixin, BookmarkAu...
method _get_reader_settings_auth_filter (line 43) | def _get_reader_settings_auth_filter(self) -> dict:
method _append_with_settings (line 49) | def _append_with_settings(self, book):
method _raise_not_found (line 61) | def _raise_not_found(self) -> None:
method _get_comics_filter (line 71) | def _get_comics_filter(self, rel):
method _get_comics_annotation_and_ordering (line 83) | def _get_comics_annotation_and_ordering(
method _get_comics_list (line 99) | def _get_comics_list(self) -> QuerySet:
method get_book_collection (line 142) | def get_book_collection(self) -> dict:
FILE: codex/views/reader/page.py
class ReaderPageView (line 25) | class ReaderPageView(BookmarkAuthMixin, AuthFilterAPIView):
method _update_bookmark (line 31) | def _update_bookmark(self) -> None:
method _get_page_image (line 48) | def _get_page_image(self) -> tuple:
method get (line 89) | def get(self, *_args, **_kwargs) -> HttpResponse:
FILE: codex/views/reader/params.py
class ReaderParamsView (line 13) | class ReaderParamsView(ReaderSettingsBaseView):
method __init__ (line 18) | def __init__(self, *args, **kwargs) -> None:
method _ensure_arc_group (line 24) | def _ensure_arc_group(self, params: dict[str, Any]) -> None:
method _ensure_arc_ids (line 37) | def _ensure_arc_ids(params: dict[str, Any]) -> None:
method _ensure_arc (line 45) | def _ensure_arc(self, params: dict[str, Any]) -> None:
method params (line 54) | def params(self):
FILE: codex/views/reader/reader.py
class ReaderView (line 14) | class ReaderView(ReaderBooksView):
method get_object (line 22) | def get_object(self) -> dict[str, Mapping | int | None]:
method get (line 45) | def get(self, *args, **kwargs) -> Response:
FILE: codex/views/reader/settings.py
class ReaderSettingsBaseView (line 44) | class ReaderSettingsBaseView(SettingsBaseView):
method get_reader_default_params (line 64) | def get_reader_default_params(cls) -> dict:
method reset_reader_settings (line 72) | def reset_reader_settings(cls, instance: SettingsReader) -> dict:
method _get_bookmark_auth_filter (line 83) | def _get_bookmark_auth_filter(self) -> dict[str, int | str | None]:
method _get_settings_lookup (line 94) | def _get_settings_lookup(self, **extra):
method _instance_to_dict (line 100) | def _instance_to_dict(instance: SettingsReader | None) -> dict | None:
method _get_global_settings (line 106) | def _get_global_settings(self) -> SettingsReader:
method _get_scoped_settings (line 119) | def _get_scoped_settings(self, scope_fk_field: str, scope_pk: int):
method _get_or_create_scoped_settings (line 124) | def _get_or_create_scoped_settings(
class ReaderSettingsView (line 135) | class ReaderSettingsView(ReaderSettingsBaseView):
method _resolve_scope_pk (line 156) | def _resolve_scope_pk(
method _canonical_scope (line 171) | def _canonical_scope(scope: str) -> str:
method _get_scope (line 177) | def _get_scope(self, scope, scopes_out, comic, scope_info) -> None:
method get (line 213) | def get(self, *args, **kwargs) -> Response:
method patch (line 242) | def patch(self, *args, **kwargs) -> Response:
method delete (line 280) | def delete(self, *args, **kwargs) -> Response:
FILE: codex/views/settings.py
class SettingsBaseView (line 35) | class SettingsBaseView(AuthFilterGenericAPIView, ABC):
method _ensure_session_key (line 58) | def _ensure_session_key(self) -> str | None:
method _get_request_user (line 64) | def _get_request_user(self):
method _get_field_default (line 74) | def _get_field_default(model, field_name):
method _get_or_create_settings_user (line 82) | def _get_or_create_settings_user(
method _get_or_create_settings_session (line 109) | def _get_or_create_settings_session(
method _create_browser_settings (line 132) | def _create_browser_settings(user, session_key, client, create_args):
method _get_or_create_settings (line 154) | def _get_or_create_settings(
method browser_instance_to_dict (line 208) | def browser_instance_to_dict(instance: SettingsBrowser) -> dict:
method _reader_instance_to_dict (line 240) | def _reader_instance_to_dict(instance: SettingsReader) -> dict:
method _load_settings_data (line 246) | def _load_settings_data(self, only: Sequence[str] | None = None) -> dict:
method _load_browser_settings_data (line 259) | def _load_browser_settings_data(self, only: Sequence[str] | None = Non...
method get_from_settings (line 270) | def get_from_settings(self, key: str, default=None, *, browser: bool =...
method get_last_route (line 283) | def get_last_route(self) -> Mapping:
method get_browser_default_params (line 290) | def get_browser_default_params(cls) -> dict:
method load_params_from_settings (line 313) | def load_params_from_settings(self, only: Sequence[str] | None = None)...
method _get_browser_order_defaults (line 323) | def _get_browser_order_defaults(self) -> dict:
method _save_browser_show (line 339) | def _save_browser_show(instance: SettingsBrowser, show_data: dict) -> ...
method _save_browser_filters (line 346) | def _save_browser_filters(
method _save_browser_last_route (line 359) | def _save_browser_last_route(
method _save_browser_settings_data (line 374) | def _save_browser_settings_data(cls, instance: SettingsBrowser, data: ...
method _save_reader_settings_data (line 391) | def _save_reader_settings_data(instance: SettingsReader, data: dict) -...
method _save_settings_data (line 398) | def _save_settings_data(self, data: dict) -> None:
method save_params_to_settings (line 411) | def save_params_to_settings(self, params) -> None: # reader session &...
FILE: codex/views/template.py
class TemplateXMLRenderer (line 11) | class TemplateXMLRenderer(TemplateHTMLRenderer):
class CodexAPIView (line 18) | class CodexAPIView(APIView):
method get (line 24) | def get(self, *args, **kwargs) -> Response:
class CodexTemplateView (line 31) | class CodexTemplateView(CodexAPIView):
class CodexXMLTemplateMixin (line 39) | class CodexXMLTemplateMixin:
FILE: codex/views/timezone.py
class TimezoneView (line 13) | class TimezoneView(AuthGenericAPIView):
method _save_timezone (line 19) | def _save_timezone(self, django_timezone) -> None:
method put (line 28) | def put(self, *args, **kwargs) -> Response:
FILE: codex/views/util.py
class Route (line 9) | class Route:
method __hash__ (line 18) | def __hash__(self) -> int:
method __eq__ (line 25) | def __eq__(self, cmp) -> bool:
function pop_name (line 30) | def pop_name(kwargs: Mapping) -> Mapping:
FILE: codex/views/version.py
class VersionView (line 16) | class VersionView(AuthGenericAPIView):
method get_object (line 22) | def get_object(self) -> dict[str, str]:
method get (line 36) | def get(self, *args, **kwargs) -> Response:
FILE: codex/websockets/consumers.py
class NotifierConsumer (line 12) | class NotifierConsumer(AsyncWebsocketConsumer):
method _get_groups (line 15) | def _get_groups(self) -> list[str]:
method websocket_connect (line 33) | async def websocket_connect(self, message) -> None:
method disconnect (line 42) | async def disconnect(self, code) -> None:
method send_text (line 46) | async def send_text(self, event) -> None:
FILE: codex/websockets/listener.py
class BroadcastListener (line 15) | class BroadcastListener:
method __init__ (line 28) | def __init__(self, logger_, queue) -> None:
method broadcast_group (line 34) | async def broadcast_group(self, event) -> None:
method shutdown (line 43) | async def shutdown(self) -> None:
method listen (line 55) | async def listen(self) -> None:
FILE: frontend/bin/roman.py
function build_ignore_spec (line 46) | def build_ignore_spec(ignore_path: Path | None) -> PathSpec:
function read_first_two_lines (line 56) | def read_first_two_lines(path: Path) -> tuple[str, str]:
function is_shell_script (line 70) | def is_shell_script(line1: str) -> bool:
function has_description_comment (line 75) | def has_description_comment(line2: str) -> bool:
function iter_files (line 80) | def iter_files(path_strs: Sequence[str], spec: PathSpec) -> Generator[Pa...
function build_parser (line 120) | def build_parser() -> ArgumentParser:
function _parse_ignore_file (line 145) | def _parse_ignore_file(args: Namespace) -> PathSpec:
function main (line 160) | def main() -> None:
FILE: frontend/src/api/v3/base.js
constant CONFIG (line 2) | const CONFIG = {
constant HTTP (line 6) | const HTTP = xior.create(CONFIG);
constant COOKIE_NAME (line 9) | const COOKIE_NAME = "csrftoken";
constant CSRF_HEADER (line 10) | const CSRF_HEADER = "X-CSRFToken";
constant CSRF_COOKIE_REGEX (line 11) | const CSRF_COOKIE_REGEX = RegExp("(?:^|;)\\s*" + COOKIE_NAME + "=([^;]*)");
FILE: frontend/src/api/v3/notify.js
constant WS_PATH (line 2) | const WS_PATH = `${globalThis.CODEX.API_V3_PATH}ws`;
function getSocketURL (line 3) | function getSocketURL() {
constant WS_URL (line 9) | const WS_URL = getSocketURL();
FILE: frontend/src/api/v3/vuetify-items.js
constant NULL_PKS (line 3) | const NULL_PKS = new Set(["", VUETIFY_NULL_CODE, undefined, null]);
FILE: frontend/src/components/admin/create-update-dialog/create-update-inputs-mixin.js
method data (line 26) | data() {
method handler (line 39) | handler(to) {
method handler (line 45) | handler(to) {
FILE: frontend/src/components/admin/use-now-timer.js
constant TICK_INTERVAL_MS (line 8) | const TICK_INTERVAL_MS = 1000;
function useNowTimer (line 14) | function useNowTimer() {
FILE: frontend/src/components/auth/auth-form-mixin.js
method setup (line 21) | setup() {
method data (line 28) | data() {
method handler (line 41) | handler() {
FILE: frontend/src/datetime.js
constant TWENTY_FOUR_HOUR_LOCALE (line 8) | const TWENTY_FOUR_HOUR_LOCALE = "sv-SE";
constant DATE_FORMAT (line 9) | const DATE_FORMAT = new Intl.DateTimeFormat(TWENTY_FOUR_HOUR_LOCALE);
constant NUMBER_FORMAT (line 10) | const NUMBER_FORMAT = new Intl.NumberFormat();
constant DURATION_FORMAT (line 11) | const DURATION_FORMAT = new Intl.DurationFormat("en", {
constant MINUTE_SECONDS (line 17) | const MINUTE_SECONDS = 60;
constant HOUR_SECONDS (line 18) | const HOUR_SECONDS = MINUTE_SECONDS * 60;
constant DAY_SECONDS (line 19) | const DAY_SECONDS = HOUR_SECONDS * 24;
FILE: frontend/src/platform.js
constant IS_MOBILE (line 7) | const IS_MOBILE = _IS_MOBILE_UA || globalThis.orientation !== undefined;
FILE: frontend/src/plugins/drag-scroll.js
constant DRAG_THRESHOLD (line 2) | const DRAG_THRESHOLD = 4;
method mounted (line 5) | mounted(el, binding) {
method unmounted (line 58) | unmounted(el) {
FILE: frontend/src/plugins/router.js
constant LAST_ROUTE (line 16) | const LAST_ROUTE = {
FILE: frontend/src/plugins/vuetify.js
constant WHITE (line 4) | const WHITE = "#FFFFFF";
constant DISABLED (line 5) | const DISABLED = "#808080";
FILE: frontend/src/route.js
constant REVERSE_READING_DIRECTIONS (line 1) | const REVERSE_READING_DIRECTIONS = Object.freeze(new Set("rtl", "btt"));
FILE: frontend/src/stores/admin.js
constant IRREGULAR_PLURALS (line 9) | const IRREGULAR_PLURALS = Object.freeze({
constant TABS (line 13) | const TABS = Object.freeze([
method isUserAdmin (line 48) | isUserAdmin() {
method normalLibraries (line 52) | normalLibraries() {
method customCoverLibraries (line 63) | customCoverLibraries() {
method doNormalComicLibrariesExist (line 74) | doNormalComicLibrariesExist() {
method _requireAdmin (line 80) | _requireAdmin() {
method loadTable (line 83) | async loadTable(table) {
method loadTables (line 101) | loadTables(tables) {
method loadFolders (line 107) | async loadFolders(path, showHidden) {
method clearFolders (line 116) | async clearFolders(root) {
method createRow (line 120) | async createRow(table, data) {
method updateRow (line 131) | async updateRow(table, pk, data) {
method changeUserPassword (line 142) | async changeUserPassword(pk, data) {
method deleteRow (line 152) | async deleteRow(table, pk) {
method librarianTask (line 163) | async librarianTask(task, text, libraryId) {
method nameSet (line 170) | nameSet(rows, nameKey, oldRow, dupeCheck) {
method loadStats (line 182) | async loadStats() {
method loadAllStatuses (line 191) | async loadAllStatuses() {
method updateAPIKey (line 206) | async updateAPIKey() {
FILE: frontend/src/stores/auth.js
method isAuthorized (line 26) | isAuthorized() {
method isAuthChecked (line 29) | isAuthChecked() {
method isUserAdmin (line 34) | isUserAdmin() {
method isAuthDialogOpen (line 37) | isAuthDialogOpen() {
method isBanner (line 40) | isBanner(state) {
method loadAdminFlags (line 45) | async loadAdminFlags() {
method loadProfile (line 53) | async loadProfile() {
method login (line 61) | async login(credentials, clear = true) {
method register (line 72) | async register(credentials) {
method logout (line 81) | logout() {
method changePassword (line 89) | async changePassword(credentials) {
method setTimezone (line 102) | async setTimezone() {
method getToken (line 107) | async getToken() {
method updateToken (line 112) | async updateToken() {
FILE: frontend/src/stores/browser-select-many.js
method selectedCount (line 41) | selectedCount(state) {
method isSelected (line 44) | isSelected(state) {
method hasSelection (line 47) | hasSelection(state) {
method compositeItem (line 54) | compositeItem(state) {
method deactivate (line 80) | deactivate() {
method toggleItem (line 84) | toggleItem(item) {
method selectAll (line 101) | selectAll() {
method clearSelection (line 114) | clearSelection() {
method markFinished (line 118) | async markFinished(finished) {
method download (line 137) | download() {
FILE: frontend/src/stores/browser.js
constant GROUPS (line 14) | const GROUPS = Object.freeze("rpisvc");
constant GROUPS_REVERSED (line 15) | const GROUPS_REVERSED = Object.freeze([...GROUPS].reverse().join(""));
constant HTTP_REDIRECT_CODES (line 16) | const HTTP_REDIRECT_CODES = Object.freeze(new Set([301, 302, 303, 307, 3...
constant DEFAULT_BOOKMARK_VALUES (line 17) | const DEFAULT_BOOKMARK_VALUES = Object.freeze(
constant ALWAYS_ENABLED_TOP_GROUPS (line 20) | const ALWAYS_ENABLED_TOP_GROUPS = Object.freeze(new Set(["a", "c"]));
constant NO_REDIRECT_ON_SEARCH_GROUPS (line 21) | const NO_REDIRECT_ON_SEARCH_GROUPS = Object.freeze(new Set(["a", "c", "f...
constant NON_BROWSE_GROUPS (line 22) | const NON_BROWSE_GROUPS = Object.freeze(new Set(["a", "f"]));
constant SEARCH_HIDE_TIMEOUT (line 23) | const SEARCH_HIDE_TIMEOUT = 5000;
constant COVER_KEYS (line 24) | const COVER_KEYS = Object.freeze(["customCovers", "dynamicCovers", "show...
constant DYNAMIC_COVER_KEYS (line 25) | const DYNAMIC_COVER_KEYS = Object.freeze([
constant FILTER_ONLY_KEYS (line 31) | const FILTER_ONLY_KEYS = Object.freeze(["filters", "q"]);
constant METADATA_LOAD_KEYS (line 32) | const METADATA_LOAD_KEYS = Object.freeze(["filters", "q", "mtime"]);
method groupNames (line 99) | groupNames() {
method topGroupChoices (line 107) | topGroupChoices() {
method topGroupChoicesMaxLen (line 116) | topGroupChoicesMaxLen() {
method orderByChoices (line 119) | orderByChoices(state) {
method orderByChoicesMaxLen (line 136) | orderByChoicesMaxLen() {
method filterByChoicesMaxLen (line 139) | filterByChoicesMaxLen() {
method isAuthorized (line 142) | isAuthorized() {
method isDynamicFiltersSelected (line 145) | isDynamicFiltersSelected(state) {
method isFiltersClearable (line 153) | isFiltersClearable(state) {
method lowestShownGroup (line 159) | lowestShownGroup(state) {
method isSearchMode (line 173) | isSearchMode(state) {
method lastRoute (line 176) | lastRoute(state) {
method coverSettings (line 189) | coverSettings(state) {
method filterOnlySettings (line 211) | filterOnlySettings(state) {
method metadataSettings (line 214) | metadataSettings(state) {
method routeKey (line 217) | routeKey() {
method _filterSettings (line 226) | _filterSettings(state, keys) {
method _maxLenChoices (line 247) | _maxLenChoices(choices) {
method identifierSourceTitle (line 256) | identifierSourceTitle(idSource) {
method fixUniverseTitles (line 264) | fixUniverseTitles(universes) {
method setIsSearchOpen (line 275) | setIsSearchOpen(value) {
method _isRootGroupEnabled (line 281) | _isRootGroupEnabled(topGroup) {
method _validateSearch (line 290) | _validateSearch(data) {
method _validateTopGroup (line 314) | _validateTopGroup(data, redirect) {
method getTopGroup (line 377) | getTopGroup(group) {
method _addSettings (line 397) | _addSettings(data) {
method _validateAndSaveSettings (line 415) | _validateAndSaveSettings(data) {
method setSettings (line 431) | async setSettings(data) {
method clearOneFilter (line 441) | async clearOneFilter(filterName) {
method clearFilters (line 449) | async clearFilters(clearAll = false) {
method setBookmarkFinished (line 467) | async setBookmarkFinished(params, finished) {
method clearSearchHideTimeout (line 478) | clearSearchHideTimeout() {
method startSearchHideTimeout (line 481) | startSearchHideTimeout() {
method setSearchHelpOpen (line 497) | setSearchHelpOpen(value) {
method routeToPage (line 504) | routeToPage(page) {
method handlePageError (line 509) | handlePageError(error) {
method loadSettings (line 528) | async loadSettings() {
method loadBrowserPage (line 552) | async loadBrowserPage(mtime, updateSettings = false) {
method loadAvailableFilterChoices (line 592) | async loadAvailableFilterChoices() {
method loadFilterChoices (line 604) | async loadFilterChoices(fieldName) {
method loadMtimes (line 619) | async loadMtimes() {
method routeWithSettings (line 637) | routeWithSettings(settings, route) {
method loadSavedSettingsList (line 648) | async loadSavedSettingsList() {
method saveCurrentSettings (line 661) | async saveCurrentSettings(name) {
method loadSavedSettings (line 672) | async loadSavedSettings(pk) {
method deleteSavedSettings (line 691) | async deleteSavedSettings(pk) {
method clearSavedSettingsSnackbar (line 702) | clearSavedSettingsSnackbar() {
FILE: frontend/src/stores/common.js
constant ERROR_KEYS (line 6) | const ERROR_KEYS = Object.freeze([
method loadVersions (line 52) | async loadVersions() {
method setErrors (line 61) | setErrors(xiorError) {
method setSuccess (line 68) | setSuccess(success) {
method clearErrors (line 74) | clearErrors() {
method setTimestamp (line 80) | setTimestamp() {
method setSettingsDrawerOpen (line 83) | setSettingsDrawerOpen(value) {
method loadOPDSURLs (line 86) | async loadOPDSURLs() {
FILE: frontend/src/stores/metadata.js
constant HEAD_ROLES (line 7) | const HEAD_ROLES = Object.freeze([
constant TAGS (line 55) | const TAGS = Object.freeze([
constant MAIN_TAGS (line 67) | const MAIN_TAGS = Object.freeze(new Set(["Characters", "Teams"]));
function compareByLastName (line 69) | function compareByLastName(a, b) {
method _mappedCredits (line 80) | _mappedCredits(state) {
method _sortedRoles (line 102) | _sortedRoles(state) {
method credits (line 126) | credits(state) {
method identifiers (line 129) | identifiers(state) {
method tags (line 154) | tags(state) {
method loadMetadata (line 170) | async loadMetadata({ group, pks }) {
method clearMetadata (line 183) | clearMetadata() {
method getTagName (line 186) | getTagName(key) {
method markTagMain (line 202) | markTagMain(tagName, tags) {
method mapTag (line 216) | mapTag(tagSource, keys, filter = undefined) {
method lazyImport (line 246) | lazyImport({ group, ids }) {
FILE: frontend/src/stores/reader.js
constant SETTINGS_NULL_VALUES (line 15) | const SETTINGS_NULL_VALUES = Object.freeze(new Set(["", null, undefined]));
constant DIRECTION_REVERSE_MAP (line 17) | const DIRECTION_REVERSE_MAP = Object.freeze({
constant PREFETCH_LINK (line 21) | const PREFETCH_LINK = Object.freeze({ rel: "prefetch", as: "image" });
constant VERTICAL_READING_DIRECTIONS (line 22) | const VERTICAL_READING_DIRECTIONS = Object.freeze(
constant REVERSE_READING_DIRECTIONS (line 25) | const REVERSE_READING_DIRECTIONS = Object.freeze(
constant SCALE_DEFAULT (line 28) | const SCALE_DEFAULT = 1;
constant FIT_TO_CLASSES (line 29) | const FIT_TO_CLASSES = Object.freeze({
constant BOOKS_NULL (line 35) | const BOOKS_NULL = Object.freeze({
constant ROUTES_NULL (line 40) | const ROUTES_NULL = Object.freeze({
constant DEFAULT_ARC (line 49) | const DEFAULT_ARC = Object.freeze({
method activeSettings (line 104) | activeSettings(state) {
method activeTitle (line 108) | activeTitle(state) {
method isVertical (line 123) | isVertical(state) {
method isReadInReverse (line 126) | isReadInReverse(state) {
method routeParams (line 129) | routeParams(state) {
method isPDF (line 132) | isPDF(state) {
method cacheBook (line 135) | cacheBook() {
method isPagesNotRoutes (line 141) | isPagesNotRoutes(state) {
method isBTT (line 144) | isBTT(state) {
method isFirstPage (line 147) | isFirstPage(state) {
method isLastPage (line 150) | isLastPage(state) {
method closeBookRoute (line 156) | closeBookRoute(state) {
method setReadRTLInReverse (line 180) | setReadRTLInReverse(bookSettings) {
method getBookSettings (line 187) | getBookSettings(book) {
method bookChangeLocation (line 224) | bookChangeLocation(direction) {
method bookChangeCursorClass (line 233) | bookChangeCursorClass(direction) {
method bookChangeShow (line 242) | bookChangeShow(direction) {
method bookChangeIcon (line 247) | bookChangeIcon(direction) {
method isCoverPage (line 257) | isCoverPage(book, page) {
method _getRouteParams (line 263) | _getRouteParams(book, activePage, direction) {
method fitToClass (line 286) | fitToClass(bookSettings) {
method _applyGlobalSettings (line 308) | _applyGlobalSettings(updates) {
method toggleToolbars (line 319) | toggleToolbars() {
method setShowToolbars (line 322) | setShowToolbars() {
method reset (line 325) | reset() {
method _getBookRoutePage (line 342) | _getBookRoutePage(book, isPrev) {
method _getBookRoute (line 355) | _getBookRoute(book, isPrev) {
method _getBookRoutes (line 365) | _getBookRoutes(prevBook, nextBook) {
method setRoutesAndBookmarkPage (line 371) | async setRoutesAndBookmarkPage(page) {
method setActivePage (line 382) | setActivePage(page, reactWithScroll = true) {
method loadGlobalSettings (line 403) | async loadGlobalSettings() {
method loadBooks (line 413) | async loadBooks({ params, arc, mtime }) {
method loadMtimes (line 476) | async loadMtimes() {
method _setBookmarkPage (line 498) | async _setBookmarkPage(page) {
method updateComicSettings (line 510) | async updateComicSettings(updates) {
method setSettingsClient (line 529) | setSettingsClient(updates) {
method clearComicSettings (line 535) | async clearComicSettings() {
method _getStoryArcPk (line 549) | _getStoryArcPk() {
method loadAllSettings (line 556) | async loadAllSettings(pk) {
method updateIntermediateSettings (line 596) | async updateIntermediateSettings(updates) {
method clearIntermediateSettings (line 619) | async clearIntermediateSettings() {
method clearGlobalSettings (line 633) | async clearGlobalSettings() {
method updateGlobalSettings (line 642) | async updateGlobalSettings(updates) {
method setBookChangeFlag (line 659) | setBookChangeFlag(direction) {
method linkLabel (line 663) | linkLabel(direction, suffix) {
method normalizeDirection (line 670) | normalizeDirection(direction) {
method _validateRoute (line 675) | _validateRoute(params, book) {
method _routeTo (line 692) | _routeTo(params, book) {
method routeToDirectionOne (line 701) | routeToDirectionOne(direction) {
method routeToDirection (line 715) | routeToDirection(direction) {
method routeToPage (line 731) | routeToPage(page) {
method routeToBook (line 735) | routeToBook(direction) {
method toRoute (line 738) | toRoute(params) {
method _prefetchSrc (line 742) | _prefetchSrc(params, direction, bookChange = false, secondPage = false) {
method prefetchLinks (line 764) | prefetchLinks(params, direction, bookChange = false) {
method prefetchBook (line 780) | prefetchBook(book) {
FILE: frontend/src/stores/socket.js
constant USER_GROUP_ROUTES (line 13) | const USER_GROUP_ROUTES = Object.freeze([
constant HEARTBEAT_INTERVAL_MS (line 19) | const HEARTBEAT_INTERVAL_MS = 5_000;
constant RECONNECT_RETRIES (line 20) | const RECONNECT_RETRIES = Infinity;
constant RECONNECT_DELAY_MS (line 21) | const RECONNECT_DELAY_MS = 3_000;
function currentRouteName (line 24) | function currentRouteName() {
function startHeartbeat (line 34) | function startHeartbeat(ws) {
function stopHeartbeat (line 43) | function stopHeartbeat() {
method onFailed (line 54) | onFailed() {
method onConnected (line 58) | onConnected(ws) {
method onMessage (line 62) | onMessage(_ws, event) {
method onError (line 65) | onError(ws, event
Condensed preview — 768 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (2,210K chars).
[
{
"path": ".circleci/config.yml",
"chars": 5015,
"preview": "executors:\n amd64-medium-executor:\n machine:\n image: ubuntu-2404:current\n resource_class: medium\n arm64-m"
},
{
"path": ".dockerignore",
"chars": 486,
"preview": "__pycache__\n!dist\n!docker/debian.sources\n.*cache\n.circleci\n.claude\n.coverage*\n.docker-token\n.DS_Store\n.env*\n.eslintcache"
},
{
"path": ".github/workflows/ci.yml",
"chars": 9218,
"preview": "name: CI\n\non:\n push:\n branches: [main]\n pull_request:\n types: [opened, synchronize]\n branches: [main, develop"
},
{
"path": ".gitignore",
"chars": 1146,
"preview": "__pycache__/\n__pypackages__/\n__snapshots__\n.*cache\n.claude\n.coverage\n.coverage.*\n.coverage*\n.dmypy.json\n.docker-token\n.D"
},
{
"path": ".picopt_treestamps.yaml",
"chars": 183,
"preview": "config:\n bigger: false\n convert_to: []\n formats:\n - GIF\n - JPEG\n - PNG\n - WEBP\n ignore: []\n keep_metada"
},
{
"path": ".prettierignore",
"chars": 428,
"preview": "__pycache__\n.*cache\n.*cache/\n.circleci\n.claude\n.git\n.mypy_cache\n.pytest_cache\n.ruff_cache\n.venv\n.venv*\n*Dockerfile\ncache"
},
{
"path": ".readthedocs.yaml",
"chars": 159,
"preview": "build:\n os: ubuntu-24.04\n tools:\n python: \"3\"\nmkdocs:\n configuration: mkdocs.yml\npython:\n install:\n - requirem"
},
{
"path": ".shellcheckrc",
"chars": 22,
"preview": "external-sources=true\n"
},
{
"path": "CLAUDE.md",
"chars": 4794,
"preview": "# CLAUDE.md Codex\n\n## Project Overview\n\nCodex is a comic archive web server: Django 6 backend, Vue 3 frontend, SQLite\nda"
},
{
"path": "Dockerfile",
"chars": 5460,
"preview": "###############################################################################\n# Multi-stage Dockerfile for Codex CI an"
},
{
"path": "LICENSE",
"chars": 35149,
"preview": " GNU GENERAL PUBLIC LICENSE\n Version 3, 29 June 2007\n\n Copyright (C) 2007 Free "
},
{
"path": "Makefile",
"chars": 276,
"preview": "SHELL := /usr/bin/env bash\n\ninclude cfg/codex.mk\ninclude cfg/django.mk\ninclude cfg/frontend.mk\ninclude cfg/python.mk\ninc"
},
{
"path": "NEWS.md",
"chars": 56530,
"preview": "# 📜 Codex News\n\n<img src=\"codex/img/logo.svg\" style=\"\nheight: 128px;\nwidth: 128px;\nborder-radius: 128px;\n\" />\n\n## v1.10."
},
{
"path": "README.md",
"chars": 22373,
"preview": "# Codex\n\nA comic archive browser and reader.\n\n<img src=\"/img/logo.svg\" style=\"\nheight: 128px;\nwidth: 128px;\nborder-radiu"
},
{
"path": "bin/benchmark-opds.sh",
"chars": 402,
"preview": "#!/usr/bin/env bash\n# benchmark opds url times\nset -euo pipefail\n\nBASE_URL=\"http://localhost:9810\"\nOPDS_BASE=\"/opds/v1.2"
},
{
"path": "bin/build-choices.sh",
"chars": 352,
"preview": "#!/usr/bin/env bash\n# Build json choices for frontend using special script.\nset -euo pipefail\nTHIS_DIR=\"$(dirname \"$0\")/"
},
{
"path": "bin/build-dist.sh",
"chars": 269,
"preview": "#!/usr/bin/env bash\n# Build script for producing a codex python package\nset -euxo pipefail\ncd \"$(dirname \"$0\")\"\n\nexport "
},
{
"path": "bin/ci-download-dist-if-identical.sh",
"chars": 1274,
"preview": "#!/usr/bin/env bash\n# Download last dist artifacts if current code is identical to the merge source.\nset -euo pipefail\n\n"
},
{
"path": "bin/clean-pycache.sh",
"chars": 99,
"preview": "#!/usr/bin/env bash\n# remove all pycache dirs\nfind . -name \"__pycache__\" -print0 | xargs -0 rm -rf\n"
},
{
"path": "bin/collectstatic.sh",
"chars": 296,
"preview": "#!/usr/bin/env bash\n# Run the django collectstatic command to collect static files from all\n# locations specified in set"
},
{
"path": "bin/create-output-dirs.sh",
"chars": 209,
"preview": "#!/usr/bin/env bash\n# create output directories with correct perms for ci builder docker mounts\n# circleci only\nset -euo"
},
{
"path": "bin/delete-files.sh",
"chars": 393,
"preview": "#!/usr/bin/env bash\n# Delete all files listed in the delete.txt file\nset -euo pipefail\nDEVENV=$1\nDELETE_FILE=$DEVENV/del"
},
{
"path": "bin/dev-docker.sh",
"chars": 181,
"preview": "#!/usr/bin/env bash\n# Recreate the codex-dev container and enter it with a shell\nset -euo pipefail\ndocker rm -f codex-de"
},
{
"path": "bin/dev-module.sh",
"chars": 300,
"preview": "#!/usr/bin/env bash\n# Run a main method in an arbitrary module\nset -euxo pipefail\nTHIS_DIR=\"$(dirname \"$0\")\"\ncd \"$THIS_D"
},
{
"path": "bin/dev-prod-server.sh",
"chars": 123,
"preview": "#!/usr/bin/env bash\n# run a production-like server\nexport PYTHONPATH=\"$PYTHONPATH:$THIS_DIR\"\nuv run python3 ./codex/run."
},
{
"path": "bin/dev-reverse-proxy.sh",
"chars": 175,
"preview": "#!/usr/bin/env bash\n# Run an nginx reverse proxy with a subpath for development testing\nset -euo pipefail\ncd \"$(dirname "
},
{
"path": "bin/dev-server.sh",
"chars": 542,
"preview": "#!/usr/bin/env bash\n# Run the codex server\nset -euxo pipefail\nTHIS_DIR=\"$(dirname \"$0\")/..\"\ncd \"$THIS_DIR\" || exit 1\nexp"
},
{
"path": "bin/dev-ttabs.sh",
"chars": 218,
"preview": "#!/usr/bin/env bash\n# Open development server processes in macOS terminal tabs\n# Requires npm ttab\nset -euo pipefail\n# T"
},
{
"path": "bin/docker-compose-exit.sh",
"chars": 234,
"preview": "#!/usr/bin/env bash\n# Run a docker compose service and return its exit code\nset -euo pipefail\nSERVICE=$1\n# docker compos"
},
{
"path": "bin/docker-tag-latest.sh",
"chars": 1075,
"preview": "#!/usr/bin/env bash\n# Tag old version as latest\nset -euo pipefail\n\nif [[ \"$#\" -lt 3 ]]; then\n echo \"Usage: $0 <registry"
},
{
"path": "bin/fix-docker.sh",
"chars": 309,
"preview": "#!/usr/bin/env bash\n# Fix common linting errors with docker\nset -euxo pipefail\n\n#######################\n###### Dockerfil"
},
{
"path": "bin/fix-python.sh",
"chars": 150,
"preview": "#!/usr/bin/env bash\n# Fix common linting errors\nset -euxo pipefail\n\n# Python\nuv run --group lint ruff check --fix .\nuv r"
},
{
"path": "bin/fix.sh",
"chars": 484,
"preview": "#!/usr/bin/env bash\n# Fix common linting errors\nset -euxo pipefail\n\n#####################\n###### Makefile #####\n########"
},
{
"path": "bin/icons_transform.py",
"chars": 3326,
"preview": "#!/usr/bin/env python\n\"\"\"Generate production icons from svg sources.\"\"\"\n\nimport shutil\nimport subprocess\nfrom pathlib im"
},
{
"path": "bin/kill-codex.sh",
"chars": 92,
"preview": "#!/usr/bin/env bash\n# kill all codex processes\nset -euo pipefail\npkill -9 -f 'codex/run.py'\n"
},
{
"path": "bin/kill-eslint_d.sh",
"chars": 148,
"preview": "#!/usr/bin/env bash\n# eslint_d can get into a bad state if git switches branches underneath it\nbunx eslint_d stop\npkill "
},
{
"path": "bin/lint-ci.sh",
"chars": 280,
"preview": "#!/usr/bin/env bash\n# Lint checks for ci\nset -euxo pipefail\n\nif [ \"$(uname)\" != \"Darwin\" ]; then\n exit 0\nfi\n\nif [ -f .g"
},
{
"path": "bin/lint-complexity.sh",
"chars": 214,
"preview": "#!/usr/bin/env bash\n# Lint complexity\nset -euo pipefail\nif [ \"$(uname)\" != \"Darwin\" ]; then\n exit 0\nfi\n\nuv run --group "
},
{
"path": "bin/lint-darwin.sh",
"chars": 255,
"preview": "#!/usr/bin/env bash\n# Lint checks\nset -euxo pipefail\n\nif [ \"$(uname)\" != \"Darwin\" ]; then\n exit 0\nfi\nshellharden --chec"
},
{
"path": "bin/lint-docker.sh",
"chars": 301,
"preview": "#!/usr/bin/env bash\n# Lint checks for docker\nset -euxo pipefail\n\nif [ \"$(uname)\" != \"Darwin\" ]; then\n exit 0\nfi\nmapfile"
},
{
"path": "bin/lint-python.sh",
"chars": 292,
"preview": "#!/usr/bin/env bash\n# Lint checks\nset -euxo pipefail\n\n####################\n###### Python ######\n####################\nuv "
},
{
"path": "bin/lint.sh",
"chars": 211,
"preview": "#!/usr/bin/env bash\n# Lint checks\nset -euxo pipefail\n\nuv run mbake validate Makefile cfg/*.mk\n\n# Javascript, JSON, Markd"
},
{
"path": "bin/localize-db.sh",
"chars": 426,
"preview": "#!/usr/bin/env bash\n# copy old database a localize\nset -euo pipefail\nREMOTE_DB=$1\nLOCAL_LIB_PATH=$2\nROOT_PATH=$(realpath"
},
{
"path": "bin/localize_library.sql",
"chars": 317,
"preview": "UPDATE codex_library\nSET\n\tpath = REPLACE(path, '/comics', @LOCAL_LIB_PATH)\nWHERE\n\tpath LIKE '/comics%';\n\nUPDATE codex_fa"
},
{
"path": "bin/manage.py",
"chars": 678,
"preview": "#!/usr/bin/env python\n\"\"\"Django's command-line utility for administrative tasks.\"\"\"\n\nimport os\nimport sys\n\n\ndef main():\n"
},
{
"path": "bin/pm",
"chars": 153,
"preview": "#!/usr/bin/env bash\n# Convenience script for running django manage tasks with uv\nset -euo pipefail\nexport PYTHONPATH=.\nu"
},
{
"path": "bin/prettier-nginx.sh",
"chars": 214,
"preview": "#!/usr/bin/env bash\n# Run prettier on nginx files because overrides doesn't work yet.\nset -euxo pipefail\nCONFIG_DIR=ngin"
},
{
"path": "bin/roman.py",
"chars": 5615,
"preview": "#!/usr/bin/env python3\n\"\"\"\nCheck shell scripts recursively for a descriptive comment on line 2.\n\nDetects shell scripts b"
},
{
"path": "bin/sort-ignore.sh",
"chars": 279,
"preview": "#!/usr/bin/env bash\n# Sort all ignore files in place and remove duplicates\n# Set locale to make output deterministic acr"
},
{
"path": "bin/test-python.sh",
"chars": 381,
"preview": "#!/usr/bin/env bash\n# Run all tests\nset -euxo pipefail\nmkdir -p test-results\n# LOGLEVEL=DEBUG uv run --group test rightt"
},
{
"path": "bin/uml.sh",
"chars": 172,
"preview": "#!/usr/bin/env bash\n# Create UML diagram\nset -euo pipefail\nPACKAGE=$(uv run toml get --toml-path=pyproject.toml project."
},
{
"path": "bin/update-deps-node.sh",
"chars": 96,
"preview": "#!/usr/bin/env bash\n# Update bun dependencies\nset -euo pipefail\nbun update\nbun outdated || true\n"
},
{
"path": "bin/update-deps-python.sh",
"chars": 243,
"preview": "#!/usr/bin/env bash\n# Update python dependencies\nset -euo pipefail\nuv sync --no-install-project --all-extras --all-group"
},
{
"path": "bin/vendor-diff-package.sh",
"chars": 865,
"preview": "#!/usr/bin/env bash\n# Find the diffs for two vendored packages.\n# vendor the original package into codex/_vendor_orig be"
},
{
"path": "bin/vendor-patch-imports.sh",
"chars": 248,
"preview": "#!/usr/bin/env bash\n# Replace relative imports with direct vendor imports\nset -euo pipefail\nMODULE=$1\nMODULE_DIR=\"codex/"
},
{
"path": "bin/version-node.sh",
"chars": 383,
"preview": "#!/usr/bin/env bash\n# Get version or set version in Frontend & API.\nset -euo pipefail\nVERSION=\"${1:-}\"\nif [ \"$VERSION\" ="
},
{
"path": "bin/version-python.sh",
"chars": 170,
"preview": "#!/usr/bin/env bash\n# Get version or set version for python.\nset -euo pipefail\nVERSION=\"${1:-}\"\nif [ \"$VERSION\" = \"\" ]; "
},
{
"path": "cfg/ci.mk",
"chars": 104,
"preview": "DEVENV_CI := 1\nexport DEVENV_CI\n\n.PHONY: lint\n## Lint ci errors\n## @category Lint\nlint::\n\tbin/lint-ci.sh"
},
{
"path": "cfg/codex.mk",
"chars": 1188,
"preview": ".PHONY: install\n## Configure wheel building for Darwin\n## @category Install\ninstall::\n\tBREW_PREFIX=$(brew --prefix)\n\texp"
},
{
"path": "cfg/common.mk",
"chars": 526,
"preview": "SHELL := /usr/bin/env bash\nDEVENV_SRC ?= ../devenv\n# export DEVENV_SRC\nDEVENV_COMMON := 1\nexport DEVENV_COMMON\n\n.PHONY: "
},
{
"path": "cfg/django.mk",
"chars": 955,
"preview": "DEVENV_DJANGO := 1\nexport DEVENV_DJANGO\n\n.PHONY: fix\n## Fix django lint errors in templates\n## @category Fix\nfix::\n\tuv r"
},
{
"path": "cfg/docker.mk",
"chars": 200,
"preview": "DEVENV_DOCKER := 1\nexport DEVENV_DOCKER\n\n.PHONY: fix\n## Fix docker lint errors\n## @category Fix\nfix::\n\tbin/fix-docker.sh"
},
{
"path": "cfg/docs.mk",
"chars": 307,
"preview": "DEVENV_DOCS := 1\nexport DEVENV_DOCS\n\n.PHONY: docs\n## Build doc site\n## @category Docs\ndocs:\n\tuv run --only-group docs --"
},
{
"path": "cfg/eslint.config.base.js",
"chars": 5085,
"preview": "import eslintJs from \"@eslint/js\";\nimport eslintJson from \"@eslint/json\";\nimport eslintPluginComments from \"@eslint-comm"
},
{
"path": "cfg/frontend.mk",
"chars": 1540,
"preview": "DEVENV_FRONTEND := 1\nexport DEVENV_FRONTEND\n\n.PHONY: clean-frontend\n## Clean frontend\n## @category Clean\nclean-frontend:"
},
{
"path": "cfg/help.mk",
"chars": 4831,
"preview": "# Inspired from\n# https://github.com/Mischback/django-calingen/blob/3f0e6db6/Makefile\n# and https://gist.github.com/klmr"
},
{
"path": "cfg/node.mk",
"chars": 702,
"preview": "DEVENV_NODE := 1\nexport DEVENV_NODE\n\n.PHONY: install-deps-node\n## Update and install node packages\n## @category Install\n"
},
{
"path": "cfg/node_root.mk",
"chars": 98,
"preview": "DEVENV_NODE_ROOT := 1\nexport DEVENV_NODE_ROOT\n\n# Dummy target for mbake linting\n.PHONY: all\nall: ;"
},
{
"path": "cfg/python.mk",
"chars": 2319,
"preview": "DEVENV_PYTHON := 1\nexport DEVENV_PYTHON\n\n.PHONY: clean\n## Clean python caches\n## @category Clean\nclean::\n\tfind . -name \""
},
{
"path": "ci/Dockerfile",
"chars": 2727,
"preview": "ARG CODEX_BUILDER_BASE_VERSION\nARG CODEX_BASE_VERSION\nFROM ajslater/codex-builder-base:${CODEX_BUILDER_BASE_VERSION} AS "
},
{
"path": "ci/base.Dockerfile",
"chars": 682,
"preview": "FROM ajslater/python-debian:3.14.3-slim-trixie_2\nARG CODEX_BASE_VERSION\nLABEL maintainer=\"AJ Slater <aj@slater.net>\"\nLAB"
},
{
"path": "ci/builder-base.Dockerfile",
"chars": 920,
"preview": "FROM nikolaik/python-nodejs:python3.14-nodejs24\n# nodejs25 blocked on bug https://github.com/nodejs/node/issues/60303\nAR"
},
{
"path": "ci/circleci-step-halt.sh",
"chars": 137,
"preview": "#!/bin/bash\n# If the skip job flag is step. skip this step.\nset -euo pipefail\nif [ -f ./SKIP_STEPS ]; then\n circleci-ag"
},
{
"path": "ci/cleanup-repo.py",
"chars": 5256,
"preview": "#!/usr/bin/env python3\n\"\"\"Remove old tags from a docker repo.\"\"\"\n\nimport argparse\nimport json\nimport sys\nimport time\nfro"
},
{
"path": "ci/debian.sources",
"chars": 340,
"preview": "Types: deb\nURIs: http://deb.debian.org/debian\nSuites: trixie trixie-updates\nComponents: main contrib non-free\nSigned-By:"
},
{
"path": "ci/dev.Dockerfile",
"chars": 499,
"preview": "FROM ajslater/codex-builder-base:latest-aarch64\nLABEL maintainer=\"AJ Slater <aj@slater.net>\"\nLABEL version=dev\n\n# hadoli"
},
{
"path": "ci/dist-builder.Dockerfile",
"chars": 1246,
"preview": "FROM oven/bun:latest AS bun-source\nARG CODEX_BUILDER_BASE_VERSION\nFROM ajslater/codex-builder-base:${CODEX_BUILDER_BASE_"
},
{
"path": "ci/docker-bake.hcl",
"chars": 3197,
"preview": "variable \"ARCH\" {}\nvariable \"CODEX_ARCH_VERSION\" {}\nvariable \"CODEX_BASE_VERSION\" {}\nvariable \"CODEX_BUILDER_BASE_VERSIO"
},
{
"path": "ci/docker-build-image.sh",
"chars": 877,
"preview": "#!/usr/bin/env bash\n# Generic image builder script\nset -xeuo pipefail\n. ./ci/machine-env.sh\n\n# Set env\nTARGET=$1 # the d"
},
{
"path": "ci/docker-compose-exit.sh",
"chars": 135,
"preview": "#!/bin/bash\n# Run a docker compose service and return its exit code\n. ./ci/machine-env.sh\ndocker compose up --exit-code-"
},
{
"path": "ci/docker-init.sh",
"chars": 990,
"preview": "#!/bin/bash\n# initialize docker builder with correct emulators for this arch\nset -euo pipefail\n\n# login to docker using "
},
{
"path": "ci/docker-push.sh",
"chars": 1653,
"preview": "#!/bin/bash\n# Load arch images and push all archs as one image to docker.io\nset -euxo pipefail\n. ./ci/machine-env.sh\nIMA"
},
{
"path": "ci/docker-tag-remote-version-as-latest.sh",
"chars": 175,
"preview": "#!/bin/bash\n# Tag a remote version as latest\nset -euo pipefail\nREPO=docker.io/ajslater/codex\nVERSION=$1\n\ndocker buildx i"
},
{
"path": "ci/machine-arch.sh",
"chars": 152,
"preview": "#!/bin/bash\n# get the target arch for the platform\nif [[ ${PLATFORMS-} == \"linux/armhf\" ]]; then\n ARCH=aarch32\nelse\n A"
},
{
"path": "ci/machine-env.sh",
"chars": 158,
"preview": "#!/bin/bash\n# export env variables\nexport PATH=$PATH:\"$HOME/.local/bin\"\n. ./ci/versions-env-filename.sh\nset -a\n# shellch"
},
{
"path": "ci/machine-init.sh",
"chars": 262,
"preview": "#!/bin/bash\n# Initialize environment for this machine.\nset -euo pipefail\nexport PATH=$PATH:$HOME/.local/bin\n./ci/circlec"
},
{
"path": "ci/machine-packages.sh",
"chars": 209,
"preview": "#!/bin/bash\n# install and upgrade system packages.\nset -euo pipefail\n# uv\nif which uv; then\n echo \"uv already installed"
},
{
"path": "ci/package.Dockerfile",
"chars": 672,
"preview": "FROM ajslater/codex-base:latest-aarch64\nARG CODEX_VERSION\nENV CODEX_VERSION=${CODEX_VERSION}\nLABEL maintainer=\"AJ Slater"
},
{
"path": "ci/python-publish.sh",
"chars": 86,
"preview": "#!/usr/bin/env bash\n# Publish distribution to pypi\n. ./ci/machine-env.sh\nmake publish\n"
},
{
"path": "ci/version-checksum.sh",
"chars": 507,
"preview": "#!/bin/bash\n# create an arched md5sum from a list of parts\nset -euo pipefail\n# This script must be sourced to pass these"
},
{
"path": "ci/version-codex-base.sh",
"chars": 344,
"preview": "#!/bin/bash\n# Compute the version tag for codex-base\nset -euo pipefail\nEXTRA_MD5S=(\"x x\")\n\nDEPS=(\n \"$0\"\n .dockerignore"
},
{
"path": "ci/version-codex-builder-base.sh",
"chars": 236,
"preview": "#!/bin/bash\n# Compute the version tag for ajslater/codex-builder-base\nset -euo pipefail\n. ci/machine-env.sh\nEXTRA_MD5S=("
},
{
"path": "ci/version-codex-dist-builder.sh",
"chars": 863,
"preview": "#!/usr/bin/env bash\n# Compute the version tag for ajslater/codex-dist-builder\nset -euo pipefail\n. ./ci/machine-env.sh\nEX"
},
{
"path": "ci/versions-create-env.sh",
"chars": 653,
"preview": "#!/bin/bash\n# Create the docker .env for this architecture\nset -euo pipefail\n. ./ci/versions-env-filename.sh\n\nif [[ $* ="
},
{
"path": "ci/versions-env-filename.sh",
"chars": 97,
"preview": "#!/bin/bash\n# Set the env filename var\nARCH=$(./ci/machine-arch.sh)\nexport ENV_FN=./.env-${ARCH}\n"
},
{
"path": "codex/__init__.py",
"chars": 382,
"preview": "\"\"\"Initialize Django.\"\"\"\n\nfrom os import environ\n\nfrom django import setup\n\nfrom codex.signals.django_signals import con"
},
{
"path": "codex/applications/__init__.py",
"chars": 25,
"preview": "\"\"\"ASGI Applications.\"\"\"\n"
},
{
"path": "codex/applications/lifespan.py",
"chars": 2348,
"preview": "\"\"\"Start and stop daemons.\"\"\"\n\nimport asyncio\nfrom contextlib import suppress\n\nfrom loguru import logger\n\nfrom codex.sig"
},
{
"path": "codex/applications/websocket.py",
"chars": 656,
"preview": "\"\"\"Channels Websocket Application.\"\"\"\n\nfrom channels.auth import AuthMiddlewareStack\nfrom channels.routing import URLRou"
},
{
"path": "codex/asgi.py",
"chars": 695,
"preview": "\"\"\"\nASGI config for codex project.\n\nIt exposes the ASGI callable as a module-level variable named ``DJANGO_APPLICATION``"
},
{
"path": "codex/authentication.py",
"chars": 492,
"preview": "\"\"\"Custom Authentication classes.\"\"\"\n\nfrom django.contrib.auth.middleware import RemoteUserMiddleware\nfrom rest_framewor"
},
{
"path": "codex/choices/__init__.py",
"chars": 51,
"preview": "\"\"\"Enums and Choices for models and Seralizers.\"\"\"\n"
},
{
"path": "codex/choices/admin.py",
"chars": 953,
"preview": "\"\"\"Admin Choices.\"\"\"\n\nfrom types import MappingProxyType\n\nfrom django.db.models.enums import TextChoices\n\n\nclass AdminFl"
},
{
"path": "codex/choices/browser.py",
"chars": 2507,
"preview": "\"\"\"Browser Choices.\"\"\"\n\nfrom types import MappingProxyType\n\nfrom comicbox.enums.maps.identifiers import ID_SOURCE_NAME_M"
},
{
"path": "codex/choices/choices_to_json.py",
"chars": 4126,
"preview": "#!/usr/bin/env python3\n\"\"\"Dump choices to JSON.\"\"\"\n\nimport json\nfrom collections.abc import Mapping\nfrom pathlib import "
},
{
"path": "codex/choices/jobs.py",
"chars": 16153,
"preview": "\"\"\"Admin Jobs: task-to-status mapping for the combined Jobs tab.\"\"\"\n\nfrom types import MappingProxyType\n\n# All importer "
},
{
"path": "codex/choices/notifications.py",
"chars": 391,
"preview": "\"\"\"Notification messages.\"\"\"\n\nfrom enum import Enum\n\n\nclass Notifications(Enum):\n \"\"\"Websocket Notifications.\"\"\"\n\n "
},
{
"path": "codex/choices/reader.py",
"chars": 878,
"preview": "\"\"\"Frontend Choices, Defaults and Messages.\"\"\"\n\nfrom types import MappingProxyType\n\nREADER_CHOICES = MappingProxyType(\n "
},
{
"path": "codex/choices/search.py",
"chars": 3821,
"preview": "\"\"\"Create the search field alias help map.\"\"\"\n\nfrom types import MappingProxyType\n\n_REVERSE_TYPE_MAP = MappingProxyType("
},
{
"path": "codex/choices/statii.py",
"chars": 1467,
"preview": "\"\"\"Status code to title map.\"\"\"\n\nfrom itertools import chain\n\nfrom bidict import frozenbidict\n\nfrom codex.librarian.cove"
},
{
"path": "codex/librarian/README.md",
"chars": 112,
"preview": "# librarian\n\nMost non-ui tasks are run by the background librariand process. librariand\nspawns threads as well.\n"
},
{
"path": "codex/librarian/__init__.py",
"chars": 74,
"preview": "\"\"\"The librarian is a collection of daemons that run background tasks.\"\"\"\n"
},
{
"path": "codex/librarian/bookmark/__init__.py",
"chars": 23,
"preview": "\"\"\"Bookmark Thread.\"\"\"\n"
},
{
"path": "codex/librarian/bookmark/bookmarkd.py",
"chars": 3840,
"preview": "\"\"\"Sends notifications to connections, reading from a queue.\"\"\"\n\nfrom collections.abc import Mapping\nfrom dataclasses im"
},
{
"path": "codex/librarian/bookmark/latest_version.py",
"chars": 2455,
"preview": "\"\"\"Fetch the current codex version.\"\"\"\n\nimport json\nfrom datetime import timedelta\nfrom urllib.request import urlopen\n\nf"
},
{
"path": "codex/librarian/bookmark/tasks.py",
"chars": 733,
"preview": "\"\"\"Bookmark Tasks.\"\"\"\n\nfrom collections.abc import Mapping\nfrom dataclasses import dataclass\nfrom typing import Any\n\nfro"
},
{
"path": "codex/librarian/bookmark/update.py",
"chars": 4996,
"preview": "\"\"\"Sends notifications to connections, reading from a queue.\"\"\"\n\nfrom django.db.models.expressions import F\nfrom django."
},
{
"path": "codex/librarian/bookmark/user_active.py",
"chars": 1295,
"preview": "\"\"\"Mixin for recording user active entry.\"\"\"\n\nfrom datetime import timedelta\n\nfrom django.contrib.auth.models import Use"
},
{
"path": "codex/librarian/covers/__init__.py",
"chars": 30,
"preview": "\"\"\"Comic cover operations.\"\"\"\n"
},
{
"path": "codex/librarian/covers/coverd.py",
"chars": 1129,
"preview": "\"\"\"Functions for dealing with comic cover thumbnails.\"\"\"\n\nfrom typing import override\n\nfrom codex.librarian.covers.purge"
},
{
"path": "codex/librarian/covers/create.py",
"chars": 5679,
"preview": "\"\"\"Create comic cover paths.\"\"\"\n\nfrom abc import ABC\nfrom io import BytesIO\nfrom multiprocessing.queues import Queue\nfro"
},
{
"path": "codex/librarian/covers/path.py",
"chars": 1250,
"preview": "\"\"\"Cover Path functions.\"\"\"\n\nfrom pathlib import Path\n\nfrom codex.settings import ROOT_CACHE_PATH\n\n\nclass CoverPathMixin"
},
{
"path": "codex/librarian/covers/purge.py",
"chars": 4038,
"preview": "\"\"\"Purge comic covers.\"\"\"\n\nimport shutil\nfrom abc import ABC\nfrom pathlib import Path\n\nfrom codex.librarian.covers.creat"
},
{
"path": "codex/librarian/covers/status.py",
"chars": 693,
"preview": "\"\"\"Cover status types.\"\"\"\n\nfrom abc import ABC\n\nfrom codex.librarian.status import Status\n\n\nclass CoversStatus(Status, A"
},
{
"path": "codex/librarian/covers/tasks.py",
"chars": 700,
"preview": "\"\"\"Covers Tasks.\"\"\"\n\nfrom dataclasses import dataclass\n\nfrom codex.librarian.tasks import LibrarianTask\n\n\n@dataclass\ncla"
},
{
"path": "codex/librarian/cron/__init__.py",
"chars": 20,
"preview": "\"\"\"Crond thread.\"\"\"\n"
},
{
"path": "codex/librarian/cron/crond.py",
"chars": 3079,
"preview": "\"\"\"Perform maintenance tasks.\"\"\"\n\nfrom threading import Condition, Event\nfrom time import sleep\nfrom types import Mappin"
},
{
"path": "codex/librarian/fs/README.md",
"chars": 780,
"preview": "# File System Watcher & Poller\n\nFilesystem watching for Codex libraries using\n[watchfiles](https://github.com/samuelcolv"
},
{
"path": "codex/librarian/fs/__init__.py",
"chars": 65,
"preview": "\"\"\"File system watching for Codex libraries using watchfiles.\"\"\"\n"
},
{
"path": "codex/librarian/fs/event_batcherd.py",
"chars": 6326,
"preview": "\"\"\"\nBatch filesystem events into bulk database import tasks.\n\nEvents from both the watchfiles watcher and the database p"
},
{
"path": "codex/librarian/fs/events.py",
"chars": 818,
"preview": "\"\"\"Codex filesystem event dataclasses.\"\"\"\n\nfrom dataclasses import dataclass\nfrom enum import IntEnum\n\nfrom watchfiles i"
},
{
"path": "codex/librarian/fs/filters.py",
"chars": 1862,
"preview": "\"\"\"Filter files with regexes.\"\"\"\n\nimport re\nfrom pathlib import Path\n\nfrom comicbox.box import Comicbox\nfrom loguru impo"
},
{
"path": "codex/librarian/fs/poller/__init__.py",
"chars": 31,
"preview": "\"\"\"Codex Filesystem Poller.\"\"\"\n"
},
{
"path": "codex/librarian/fs/poller/events.py",
"chars": 400,
"preview": "\"\"\"Poller handlers (snapshot diff: has moved events, dirs, full classification).\"\"\"\n\nfrom dataclasses import dataclass\nf"
},
{
"path": "codex/librarian/fs/poller/poller.py",
"chars": 10140,
"preview": "\"\"\"Database polling for library changes.\"\"\"\n\nfrom pathlib import Path\nfrom threading import Condition, Event\nfrom typing"
},
{
"path": "codex/librarian/fs/poller/snapshot.py",
"chars": 6441,
"preview": "\"\"\"Filesystem and database snapshot classes for change detection.\"\"\"\n\nimport os\nfrom collections.abc import Iterator\nfro"
},
{
"path": "codex/librarian/fs/poller/snapshot_diff.py",
"chars": 7314,
"preview": "\"\"\"\nCompute the diff between two snapshots.\n\nSupports inode-based move detection and optional device-ignoring for\nDocker"
},
{
"path": "codex/librarian/fs/poller/status.py",
"chars": 261,
"preview": "\"\"\"Watcher Statii.\"\"\"\n\nfrom codex.librarian.fs.status import FSStatus\n\n\nclass FSPollStatus(FSStatus):\n \"\"\"FS Poll Sta"
},
{
"path": "codex/librarian/fs/poller/tasks.py",
"chars": 243,
"preview": "\"\"\"Poller Tasks.\"\"\"\n\nfrom dataclasses import dataclass\n\nfrom codex.librarian.fs.tasks import FSTask\n\n\n@dataclass\nclass F"
},
{
"path": "codex/librarian/fs/status.py",
"chars": 147,
"preview": "\"\"\"Watcher Statii.\"\"\"\n\nfrom abc import ABC\n\nfrom codex.librarian.status import Status\n\n\nclass FSStatus(Status, ABC):\n "
},
{
"path": "codex/librarian/fs/tasks.py",
"chars": 440,
"preview": "\"\"\"Filesystem Tasks.\"\"\"\n\nfrom dataclasses import dataclass\n\nfrom codex.librarian.fs.events import FSEvent\nfrom codex.lib"
},
{
"path": "codex/librarian/fs/watcher/__init__.py",
"chars": 27,
"preview": "\"\"\"File System Watcher.\"\"\"\n"
},
{
"path": "codex/librarian/fs/watcher/data.py",
"chars": 652,
"preview": "\"\"\"Dataclass for events post processing changes.\"\"\"\n\nfrom dataclasses import dataclass, field\n\nfrom codex.librarian.fs.e"
},
{
"path": "codex/librarian/fs/watcher/dirs.py",
"chars": 3249,
"preview": "\"\"\"Add missing events for directories.\"\"\"\n\nimport os\nfrom pathlib import Path\n\nfrom loguru import logger\n\nfrom codex.lib"
},
{
"path": "codex/librarian/fs/watcher/events.py",
"chars": 3716,
"preview": "\"\"\"\nProcess raw watchfiles changes into rich FSEvents.\n\nHandles three capabilities that raw watchfiles doesn't provide:\n"
},
{
"path": "codex/librarian/fs/watcher/move.py",
"chars": 3732,
"preview": "\"\"\"Watchfiles Move detection.\"\"\"\n\nfrom pathlib import Path\n\nfrom loguru import logger\n\nfrom codex.librarian.fs.events im"
},
{
"path": "codex/librarian/fs/watcher/status.py",
"chars": 331,
"preview": "\"\"\"Watcher Statii.\"\"\"\n\nfrom codex.librarian.fs.status import FSStatus\n\n\nclass FSWatcherRestartStatus(FSStatus):\n \"\"\"F"
},
{
"path": "codex/librarian/fs/watcher/tasks.py",
"chars": 205,
"preview": "\"\"\"Restart Watcher to sync with database.\"\"\"\n\nfrom dataclasses import dataclass\n\nfrom codex.librarian.fs.tasks import FS"
},
{
"path": "codex/librarian/fs/watcher/watcher.py",
"chars": 6828,
"preview": "\"\"\"Filesystem watcher using watchfiles.\"\"\"\n\nfrom pathlib import Path\nfrom threading import Event\nfrom time import sleep\n"
},
{
"path": "codex/librarian/librariand.py",
"chars": 7984,
"preview": "\"\"\"Library process worker for background tasks.\"\"\"\n\nfrom copy import copy\nfrom multiprocessing import Process, Queue\nfro"
},
{
"path": "codex/librarian/memory.py",
"chars": 1536,
"preview": "\"\"\"Detect how much memory we're working with.\"\"\"\n\nimport resource\nfrom contextlib import suppress\nfrom pathlib import Pa"
},
{
"path": "codex/librarian/mp_queue.py",
"chars": 156,
"preview": "\"\"\"Library Queue.\"\"\"\n\n# This file cannot be named queue or it causes weird type checker errors\nfrom multiprocessing impo"
},
{
"path": "codex/librarian/notifier/__init__.py",
"chars": 23,
"preview": "\"\"\"Notifier Thread.\"\"\"\n"
},
{
"path": "codex/librarian/notifier/notifierd.py",
"chars": 1811,
"preview": "\"\"\"Sends notifications to connections, reading from a queue.\"\"\"\n\nfrom typing import override\n\nfrom codex.librarian.threa"
},
{
"path": "codex/librarian/notifier/tasks.py",
"chars": 1097,
"preview": "\"\"\"Notifier Tasks.\"\"\"\n\nfrom dataclasses import dataclass\n\nfrom codex.choices.notifications import Notifications\nfrom cod"
},
{
"path": "codex/librarian/restarter/__init__.py",
"chars": 23,
"preview": "\"\"\"Codex restarter.\"\"\"\n"
},
{
"path": "codex/librarian/restarter/restarter.py",
"chars": 1681,
"preview": "\"\"\"Update the codex python package.\"\"\"\n\nimport os\nimport signal\n\nfrom codex.librarian.restarter.status import (\n Code"
},
{
"path": "codex/librarian/restarter/status.py",
"chars": 664,
"preview": "\"\"\"Restarter Statii.\"\"\"\n\nfrom abc import ABC\n\nfrom codex.librarian.status import Status\n\n\nclass CodexRestarterStatus(Sta"
},
{
"path": "codex/librarian/restarter/tasks.py",
"chars": 298,
"preview": "\"\"\"Codex Restarter Taskss.\"\"\"\n\nfrom codex.librarian.tasks import LibrarianTask\n\n\nclass CodexRestarterTask(LibrarianTask)"
},
{
"path": "codex/librarian/scribe/__init__.py",
"chars": 47,
"preview": "\"\"\"Scribe module for bulk writes to sqlite.\"\"\"\n"
},
{
"path": "codex/librarian/scribe/importer/__init__.py",
"chars": 22,
"preview": "\"\"\"Comic Importer.\"\"\"\n"
},
{
"path": "codex/librarian/scribe/importer/const.py",
"chars": 10710,
"preview": "\"\"\"BULK_CREATE_COMIC_FIELDSConsts and maps for import.\"\"\"\n\nfrom types import MappingProxyType, SimpleNamespace\n\nfrom bid"
},
{
"path": "codex/librarian/scribe/importer/create/__init__.py",
"chars": 1107,
"preview": "\"\"\"\nCreate all missing comic foreign keys for an import.\n\nSo we may safely create the comics next.\n\"\"\"\n\nfrom codex.libra"
},
{
"path": "codex/librarian/scribe/importer/create/comics.py",
"chars": 6805,
"preview": "\"\"\"Bulk update and create comic objects and bulk update m2m fields.\"\"\"\n\nfrom django.db.models import NOT_PROVIDED\nfrom d"
},
{
"path": "codex/librarian/scribe/importer/create/const.py",
"chars": 3797,
"preview": "\"\"\"Create fks consts.\"\"\"\n\nfrom collections.abc import Mapping\nfrom types import MappingProxyType\n\nfrom codex.librarian.s"
},
{
"path": "codex/librarian/scribe/importer/create/covers.py",
"chars": 4702,
"preview": "\"\"\"Create Custom Covers.\"\"\"\n\nfrom django.core.exceptions import ObjectDoesNotExist\nfrom django.db.models.functions.datet"
},
{
"path": "codex/librarian/scribe/importer/create/folders.py",
"chars": 3597,
"preview": "\"\"\"Create missing folder foreign keys for an import.\"\"\"\n\nfrom pathlib import Path\n\nfrom codex.librarian.scribe.importer."
},
{
"path": "codex/librarian/scribe/importer/create/foreign_keys.py",
"chars": 9427,
"preview": "\"\"\"\nCreate all missing comic many to many objects for an import.\n\nSo we may safely create the comics next.\n\"\"\"\n\nfrom col"
},
{
"path": "codex/librarian/scribe/importer/create/link_fks.py",
"chars": 2782,
"preview": "\"\"\"Bulk update m2m fields foreign keys.\"\"\"\n\nfrom contextlib import suppress\nfrom pathlib import Path\nfrom typing import "
},
{
"path": "codex/librarian/scribe/importer/delete/__init__.py",
"chars": 1054,
"preview": "\"\"\"Clean up the database after moves or imports.\"\"\"\n\nfrom codex.librarian.scribe.importer.delete.folders import DeletedF"
},
{
"path": "codex/librarian/scribe/importer/delete/comics.py",
"chars": 3759,
"preview": "\"\"\"Delete comics methods.\"\"\"\n\nfrom codex.librarian.scribe.importer.const import ALL_COMIC_GROUP_FIELD_NAMES\nfrom codex.l"
},
{
"path": "codex/librarian/scribe/importer/delete/covers.py",
"chars": 1409,
"preview": "\"\"\"Clean up covers from the db.\"\"\"\n\nfrom codex.librarian.covers.tasks import CoverRemoveTask\nfrom codex.librarian.scribe"
},
{
"path": "codex/librarian/scribe/importer/delete/folders.py",
"chars": 1287,
"preview": "\"\"\"Delete database folders methods.\"\"\"\n\nfrom codex.librarian.scribe.importer.delete.comics import DeletedComicsImporter\n"
},
{
"path": "codex/librarian/scribe/importer/failed/__init__.py",
"chars": 22,
"preview": "\"\"\"Failed imports.\"\"\"\n"
},
{
"path": "codex/librarian/scribe/importer/failed/create.py",
"chars": 3796,
"preview": "\"\"\"Update and create failed imports.\"\"\"\n\nfrom django.db.models.functions import Now\n\nfrom codex.librarian.scribe.importe"
},
{
"path": "codex/librarian/scribe/importer/failed/failed.py",
"chars": 2123,
"preview": "\"\"\"Update and create failed imports.\"\"\"\n\nfrom codex.librarian.scribe.importer.const import DELETE_FI_PATHS\nfrom codex.li"
},
{
"path": "codex/librarian/scribe/importer/failed/query.py",
"chars": 4334,
"preview": "\"\"\"Update and create failed imports.\"\"\"\n\nfrom pathlib import Path\n\nfrom codex.librarian.scribe.importer.const import (\n "
},
{
"path": "codex/librarian/scribe/importer/finish.py",
"chars": 2795,
"preview": "\"\"\"The main importer class.\"\"\"\n\nfrom time import time\nfrom types import MappingProxyType\n\nfrom django.core.cache import "
},
{
"path": "codex/librarian/scribe/importer/importer.py",
"chars": 702,
"preview": "\"\"\"The main importer class.\"\"\"\n\nfrom codex.librarian.scribe.importer.moved import MovedImporter\n\n_METHODS = (\n \"init_"
},
{
"path": "codex/librarian/scribe/importer/init.py",
"chars": 11767,
"preview": "\"\"\"Initialize Importer.\"\"\"\n\nfrom dataclasses import asdict, dataclass\nfrom multiprocessing.queues import Queue\nfrom path"
},
{
"path": "codex/librarian/scribe/importer/link/__init__.py",
"chars": 470,
"preview": "\"\"\"Bulk update m2m fields.\"\"\"\n\nfrom codex.librarian.scribe.importer.link.many_to_many import LinkManyToManyImporter\n\n\ncl"
},
{
"path": "codex/librarian/scribe/importer/link/const.py",
"chars": 814,
"preview": "\"\"\"Link constants.\"\"\"\n\nfrom types import MappingProxyType\n\nfrom codex.librarian.scribe.importer.const import (\n CREDI"
},
{
"path": "codex/librarian/scribe/importer/link/covers.py",
"chars": 2608,
"preview": "\"\"\"Link Covers.\"\"\"\n\nfrom pathlib import Path\n\nfrom codex.librarian.scribe.importer.const import (\n CLASS_CUSTOM_COVER"
},
{
"path": "codex/librarian/scribe/importer/link/delete.py",
"chars": 4009,
"preview": "\"\"\"Delete stale m2ms.\"\"\"\n\nfrom typing import TYPE_CHECKING\n\nfrom django.db.models import ManyToManyField, Q\n\nfrom codex."
},
{
"path": "codex/librarian/scribe/importer/link/many_to_many.py",
"chars": 3518,
"preview": "\"\"\"Link Comics M2M fields.\"\"\"\n\nfrom typing import TYPE_CHECKING\n\nfrom codex.librarian.scribe.importer.const import (\n "
},
{
"path": "codex/librarian/scribe/importer/link/prepare.py",
"chars": 5060,
"preview": "\"\"\"Prepare links with database objects.\"\"\"\n\nfrom collections.abc import Callable, Mapping\nfrom typing import TYPE_CHECKI"
},
{
"path": "codex/librarian/scribe/importer/link/sum.py",
"chars": 609,
"preview": "\"\"\"Total methods for updating statii.\"\"\"\n\nfrom codex.librarian.scribe.importer.link.delete import LinkImporterDelete\n\n\nc"
},
{
"path": "codex/librarian/scribe/importer/moved/__init__.py",
"chars": 2134,
"preview": "\"\"\"Bulk import and move comics and folders.\"\"\"\n\nfrom pathlib import Path\n\nfrom django.db.models.functions import Now\n\nfr"
},
{
"path": "codex/librarian/scribe/importer/moved/comics.py",
"chars": 4963,
"preview": "\"\"\"Bulk import and move comics.\"\"\"\n\nfrom pathlib import Path\n\nfrom django.db.models.functions import Now\n\nfrom codex.lib"
},
{
"path": "codex/librarian/scribe/importer/moved/covers.py",
"chars": 3278,
"preview": "\"\"\"Bulk import and move covers.\"\"\"\n\nfrom pathlib import Path\n\nfrom django.db.models.functions import Now\n\nfrom codex.lib"
},
{
"path": "codex/librarian/scribe/importer/moved/folders.py",
"chars": 8304,
"preview": "\"\"\"Bulk import and move comics and folders.\"\"\"\n\nfrom pathlib import Path\n\nfrom bidict import bidict, frozenbidict\nfrom d"
},
{
"path": "codex/librarian/scribe/importer/query/__init__.py",
"chars": 1461,
"preview": "\"\"\"Query missing foreign keys.\"\"\"\n\nfrom codex.librarian.scribe.importer.const import (\n CREATE_FKS,\n DELETE_M2MS,\n"
},
{
"path": "codex/librarian/scribe/importer/query/covers.py",
"chars": 2146,
"preview": "\"\"\"Query Missing Custom Covers.\"\"\"\n\nfrom codex.librarian.scribe.importer.const import (\n CREATE_COVERS,\n UPDATE_CO"
},
{
"path": "codex/librarian/scribe/importer/query/filters.py",
"chars": 2093,
"preview": "\"\"\"Query the missing foreign keys methods.\"\"\"\n\nfrom django.db.models.query_utils import Q\n\nfrom codex.librarian.scribe.i"
},
{
"path": "codex/librarian/scribe/importer/query/foreign_keys.py",
"chars": 7263,
"preview": "\"\"\"Query the missing foreign keys methods.\"\"\"\n\nfrom codex.librarian.scribe.importer.const import (\n CREATE_FKS,\n F"
},
{
"path": "codex/librarian/scribe/importer/query/links.py",
"chars": 1022,
"preview": "\"\"\"Prune link actions.\"\"\"\n\nfrom codex.librarian.scribe.importer.const import (\n LINK_FKS,\n LINK_M2MS,\n)\nfrom codex"
},
{
"path": "codex/librarian/scribe/importer/query/links_fk.py",
"chars": 4022,
"preview": "\"\"\"Prune M2O links that don't need updating.\"\"\"\n\nfrom codex.librarian.scribe.importer.const import (\n COMIC_FK_FIELD_"
},
{
"path": "codex/librarian/scribe/importer/query/links_m2m.py",
"chars": 4819,
"preview": "\"\"\"Prune M2M links that don't need updating.\"\"\"\n\nfrom codex.librarian.scribe.importer.const import (\n COMIC_M2M_FIELD"
},
{
"path": "codex/librarian/scribe/importer/query/update_comics.py",
"chars": 2619,
"preview": "\"\"\"Move comics that need only updating into correct structure.\"\"\"\n\nfrom codex.librarian.scribe.importer.const import (\n "
},
{
"path": "codex/librarian/scribe/importer/query/update_fks.py",
"chars": 5078,
"preview": "\"\"\"Query the missing foreign keys methods.\"\"\"\n\nfrom comicbox.enums.comicbox import compare_identifier_source\n\nfrom codex"
}
]
// ... and 568 more files (download for full content)
About this extraction
This page contains the full source code of the ajslater/codex GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 768 files (2.0 MB), approximately 549.9k tokens, and a symbol index with 2299 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.