├── .dockerignore ├── .gitattributes ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ ├── discord-release-bot.yml │ ├── dockerhub-build-push-on-push.yml │ ├── dockerhub-build-push-on-release.yml │ └── dockerhub-description.yml ├── .gitignore ├── Dockerfile ├── LICENSE ├── README.md ├── README_images ├── CWA-Homepage.png ├── CWA-banner.png ├── CWA-new-process-ui.gif ├── V3-settings-infographic.png ├── cwa-bulk-editting-diagram.png ├── cwa-db-diagram.png ├── cwa-enforcer-diagram.png ├── cwa-logo-round-dark.png ├── cwa-logo-round-light.png ├── cwa-rebrand-ui.png ├── cwa-server-stats-page.png ├── new-book-list.png └── old-book-list.png ├── build.sh ├── dirs.json ├── docker-compose.yml ├── empty_library ├── app.db └── metadata.db ├── requirements.txt ├── root ├── app │ └── calibre-web │ │ └── cps │ │ ├── admin.py │ │ ├── config_sql.py │ │ ├── constants.py │ │ ├── cwa_functions.py │ │ ├── db.py │ │ ├── editbooks.py │ │ ├── kobo.py │ │ ├── main.py │ │ ├── metadata_provider │ │ ├── hardcover.py │ │ └── ibdb.py │ │ ├── render_template.py │ │ ├── search_metadata.py │ │ ├── services │ │ ├── Metadata.py │ │ ├── __init__.py │ │ └── hardcover.py │ │ ├── shelf.py │ │ ├── static │ │ ├── css │ │ │ ├── caliBlur.css │ │ │ ├── caliBlur_cwa.css │ │ │ └── style.css │ │ ├── js │ │ │ ├── caliBlur.js │ │ │ ├── get_meta.js │ │ │ └── table.js │ │ └── user-profile-data │ │ │ └── CWA-profile-updater.js │ │ ├── templates │ │ ├── admin.html │ │ ├── book_edit.html │ │ ├── book_table.html │ │ ├── config_db.html │ │ ├── config_edit.html │ │ ├── cwa_convert_library.html │ │ ├── cwa_epub_fixer.html │ │ ├── cwa_list_logs.html │ │ ├── cwa_read_log.html │ │ ├── cwa_settings.html │ │ ├── cwa_stats.html │ │ ├── cwa_stats_full.html │ │ ├── detail.html │ │ ├── image.html │ │ ├── layout.html │ │ ├── profile_pictures.html │ │ └── user_edit.html │ │ ├── ub.py │ │ └── web.py └── etc │ └── s6-overlay │ └── s6-rc.d │ ├── cwa-auto-library │ ├── dependencies.d │ │ └── cwa-init │ ├── run │ ├── type │ └── up │ ├── cwa-auto-zipper │ ├── dependencies.d │ │ └── cwa-init │ ├── run │ ├── type │ └── up │ ├── cwa-ingest-service │ ├── dependencies.d │ │ └── cwa-auto-library │ ├── run │ ├── type │ └── up │ ├── cwa-init │ ├── dependencies.d │ │ └── init-custom-files │ ├── run │ ├── type │ └── up │ ├── init-adduser │ └── branding │ ├── metadata-change-detector │ ├── dependencies.d │ │ └── cwa-auto-library │ ├── run │ ├── type │ └── up │ ├── universal-calibre-setup │ ├── dependencies.d │ │ └── init-custom-files │ ├── run │ ├── type │ └── up │ └── user │ └── contents.d │ ├── cwa-auto-library │ ├── cwa-auto-zipper │ ├── cwa-ingest-service │ ├── cwa-init │ ├── metadata-change-detector │ └── universal-calibre-setup └── scripts ├── auto_library.py ├── auto_zip.py ├── check-cwa-services.sh ├── convert_library.py ├── cover_enforcer.py ├── cwa_db.py ├── cwa_schema.sql ├── ingest_processor.py ├── kindle_epub_fixer.py └── setup-cwa.sh /.dockerignore: -------------------------------------------------------------------------------- 1 | README_images 2 | docker-compose.yml 3 | .git 4 | .github 5 | .gitignore 6 | .github/workflows 7 | build.sh 8 | README.md 9 | LICENSE 10 | Dockerfile 11 | .dockerignore 12 | build 13 | .history 14 | Dockerfile_calibre_not_included -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Enforce LF for all shell scripts and related files 2 | *.sh text eol=lf 3 | *.bash text eol=lf 4 | *.env text eol=lf 5 | *.conf text eol=lf 6 | Dockerfile text eol=lf 7 | *.yml text eol=lf 8 | **/run text eol=lf 9 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | ko_fi: crocodilestick -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve! 4 | title: "[bug]" 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Configuration(please complete the following information):** 27 | - OS: [e.g. Ubuntu Server, Debian, UnRaid ect.] 28 | - Hardware: [x86 server, Synology NAS, ARM VPS ect.] 29 | - Version [e.g. 22] 30 | 31 | **Additional context** 32 | Add any other context about the problem here. 33 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: "[Feature Request]" 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/workflows/discord-release-bot.yml: -------------------------------------------------------------------------------- 1 | name: Post New Release Notes to Discord 2 | # Automatically posts new Release Notes to the project's discord 3 | 4 | on: 5 | release: 6 | types: 7 | - published 8 | 9 | jobs: 10 | github-releases-to-discord: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout 14 | uses: actions/checkout@v3 15 | - name: Releases To Discord 16 | uses: sillyangel/releases-to-discord@v1 17 | with: 18 | webhook_url: ${{ secrets.DISCORD_RELEASE_WEBHOOK_URL }} 19 | username: "CWA Release Changelog Bot" 20 | avatar_url: "https://github.com/crocodilestick/Calibre-Web-Automated/blob/main/README_images/cwa-logo-round-light.png" 21 | -------------------------------------------------------------------------------- /.github/workflows/dockerhub-build-push-on-push.yml: -------------------------------------------------------------------------------- 1 | name: Build & Push - Dev 2 | # Automatically builds and pushes a multi-platform dev image based on commits involving key files in any branch 3 | 4 | on: 5 | push: 6 | branches: 7 | - '**' # Runs on pushes to any branch 8 | paths: 9 | - 'empty_library/**' 10 | - 'root/**' 11 | - 'scripts/**' 12 | - '**/Dockerfile' 13 | create: 14 | branches: 15 | - '**' # Runs when a new branch is created 16 | 17 | jobs: 18 | build: 19 | runs-on: ubuntu-latest 20 | if: github.event.ref_type == 'branch' || github.event_name == 'push' # Ensures it runs for branch creation & push events 21 | 22 | steps: 23 | - name: Checkout correct branch 24 | uses: actions/checkout@v4 25 | with: 26 | ref: ${{ github.ref }} 27 | 28 | - name: Determine Docker Image Tag 29 | id: tag 30 | run: | 31 | if [[ "${{ github.ref_name }}" == "main" ]]; then 32 | echo "IMAGE_TAG=dev" >> $GITHUB_ENV 33 | else 34 | echo "IMAGE_TAG=dev-${{ github.ref_name }}" >> $GITHUB_ENV 35 | fi 36 | 37 | - name: DockerHub Login 38 | uses: docker/login-action@v3 39 | with: 40 | username: ${{ secrets.DOCKERHUB_USERNAME }} 41 | password: ${{ secrets.DOCKERHUB_PA_TOKEN }} 42 | 43 | - name: Install QEMU 44 | run: docker run --rm --privileged multiarch/qemu-user-static --reset -p yes 45 | 46 | - name: Set up Docker Buildx 47 | uses: docker/setup-buildx-action@v3 48 | 49 | - name: Build and push Docker image 50 | uses: docker/build-push-action@v6 51 | with: 52 | provenance: false # Disable provenance metadata to fix BuildKit issues 53 | context: . 54 | file: ./Dockerfile 55 | push: true 56 | build-args: | 57 | BUILD_DATE=${{ github.event.repository.updated_at }} 58 | VERSION=${{ vars.CURRENT_DEV_VERSION }}-DEV_BUILD-${{ env.IMAGE_TAG }}-${{ vars.CURRENT_DEV_BUILD_NUM }} 59 | tags: | 60 | ${{ secrets.DOCKERHUB_USERNAME }}/calibre-web-automated:dev 61 | 62 | platforms: linux/amd64,linux/arm64 63 | 64 | - name: Increment dev build number 65 | uses: action-pack/increment@v2 66 | id: increment 67 | with: 68 | name: 'CURRENT_DEV_BUILD_NUM' 69 | token: ${{ secrets.DEV_BUILD_NUM_INCREMENTOR_TOKEN }} 70 | -------------------------------------------------------------------------------- /.github/workflows/dockerhub-build-push-on-release.yml: -------------------------------------------------------------------------------- 1 | name: Build & Push - Release 2 | # Automatically builds and pushes a multi-platform image based on the repo's newest release and tags it both as latest and with the version number of the release 3 | 4 | on: 5 | release: 6 | types: 7 | - published 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - uses: actions/checkout@v4 15 | 16 | - name: DockerHub Login 17 | uses: docker/login-action@v3 18 | with: 19 | username: ${{ secrets.DOCKERHUB_USERNAME }} 20 | password: ${{ secrets.DOCKERHUB_PA_TOKEN }} 21 | 22 | - name: Set up Docker Buildx 23 | uses: docker/setup-buildx-action@v3 24 | 25 | - name: Build and push Docker image 26 | uses: docker/build-push-action@v6 27 | with: 28 | context: . 29 | file: ./Dockerfile 30 | push: true 31 | build-args: | 32 | BUILD_DATE=${{ github.event.repository.updated_at }} 33 | VERSION=${{ github.event.release.tag_name }} 34 | tags: | 35 | ${{ secrets.DOCKERHUB_USERNAME }}/calibre-web-automated:${{ github.event.release.tag_name }} 36 | ${{ secrets.DOCKERHUB_USERNAME }}/calibre-web-automated:latest 37 | 38 | platforms: linux/amd64,linux/arm64 39 | -------------------------------------------------------------------------------- /.github/workflows/dockerhub-description.yml: -------------------------------------------------------------------------------- 1 | name: Update Docker Hub Description 2 | # Automatically mirrors the README to the project's DockerHub page 3 | 4 | on: 5 | push: 6 | branches: 7 | - main 8 | paths: 9 | - README.md 10 | - .github/workflows/dockerhub-description.yml 11 | jobs: 12 | dockerHubDescription: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v4 16 | 17 | - name: Docker Hub Description 18 | uses: peter-evans/dockerhub-description@v4 19 | with: 20 | username: ${{ secrets.DOCKERHUB_USERNAME }} 21 | password: ${{ secrets.DOCKERHUB_PASSWORD }} 22 | repository: crocodilestick/calibre-web-automated 23 | short-description: ${{ github.event.repository.description }} 24 | enable-url-completion: true 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Enviroment Guff 10 | 11 | poetry.lock 12 | replit.nix 13 | pyproject.toml 14 | .replit 15 | .breakpoints 16 | .venv 17 | .vscode 18 | .history 19 | 20 | # Test Scripts 21 | test* 22 | TEST 23 | DEV 24 | */REFERENCE 25 | cwa.db 26 | *.epub 27 | 28 | # Dev files 29 | changelogs/ 30 | *.txz 31 | 32 | # Distribution / packaging 33 | .Python 34 | build/ 35 | develop-eggs/ 36 | dist/ 37 | downloads/ 38 | eggs/ 39 | .eggs/ 40 | lib/ 41 | lib64/ 42 | parts/ 43 | sdist/ 44 | var/ 45 | wheels/ 46 | share/python-wheels/ 47 | *.egg-info/ 48 | .installed.cfg 49 | *.egg 50 | MANIFEST 51 | 52 | 53 | # PyInstaller 54 | # Usually these files are written by a python script from a template 55 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 56 | *.manifest 57 | *.spec 58 | 59 | # Installer logs 60 | pip-log.txt 61 | pip-delete-this-directory.txt 62 | 63 | # Unit test / coverage reports 64 | htmlcov/ 65 | .tox/ 66 | .nox/ 67 | .coverage 68 | .coverage.* 69 | .cache 70 | nosetests.xml 71 | coverage.xml 72 | *.cover 73 | *.py,cover 74 | .hypothesis/ 75 | .pytest_cache/ 76 | cover/ 77 | 78 | # Translations 79 | *.mo 80 | *.pot 81 | 82 | # Django stuff: 83 | *.log 84 | local_settings.py 85 | db.sqlite3 86 | db.sqlite3-journal 87 | 88 | # Flask stuff: 89 | instance/ 90 | .webassets-cache 91 | 92 | # Scrapy stuff: 93 | .scrapy 94 | 95 | # Sphinx documentation 96 | docs/_build/ 97 | 98 | # PyBuilder 99 | .pybuilder/ 100 | target/ 101 | 102 | # Jupyter Notebook 103 | .ipynb_checkpoints 104 | 105 | # IPython 106 | profile_default/ 107 | ipython_config.py 108 | 109 | # pyenv 110 | # For a library or package, you might want to ignore these files since the code is 111 | # intended to run in multiple environments; otherwise, check them in: 112 | # .python-version 113 | 114 | # pipenv 115 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 116 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 117 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 118 | # install all needed dependencies. 119 | #Pipfile.lock 120 | 121 | # poetry 122 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 123 | # This is especially recommended for binary packages to ensure reproducibility, and is more 124 | # commonly ignored for libraries. 125 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 126 | #poetry.lock 127 | 128 | # pdm 129 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 130 | #pdm.lock 131 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 132 | # in version control. 133 | # https://pdm.fming.dev/#use-with-ide 134 | .pdm.toml 135 | 136 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 137 | __pypackages__/ 138 | 139 | # Celery stuff 140 | celerybeat-schedule 141 | celerybeat.pid 142 | 143 | # SageMath parsed files 144 | *.sage.py 145 | 146 | # Environments 147 | .env 148 | .venv 149 | env/ 150 | venv/ 151 | ENV/ 152 | env.bak/ 153 | venv.bak/ 154 | 155 | # Spyder project settings 156 | .spyderproject 157 | .spyproject 158 | 159 | # Rope project settings 160 | .ropeproject 161 | 162 | # mkdocs documentation 163 | /site 164 | 165 | # mypy 166 | .mypy_cache/ 167 | .dmypy.json 168 | dmypy.json 169 | 170 | # Pyre type checker 171 | .pyre/ 172 | 173 | # pytype static type analyzer 174 | .pytype/ 175 | 176 | # Cython debug symbols 177 | cython_debug/ 178 | 179 | # PyCharm 180 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 181 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 182 | # and can be added to the global gitignore or merged into this file. For a more nuclear 183 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 184 | #.idea/ -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # syntax=docker/dockerfile:1 2 | 3 | FROM ghcr.io/linuxserver/unrar:latest AS unrar 4 | FROM ghcr.io/linuxserver/baseimage-ubuntu:jammy 5 | 6 | # Set the default shell for the following RUN instructions to bash instead of sh 7 | SHELL ["/bin/bash", "-c"] 8 | 9 | # Simple Example Build Command: 10 | # docker build \ 11 | # --tag crocodilestick/calibre-web-automated:dev \ 12 | # --build-arg="BUILD_DATE=27-09-2024 12:06" \ 13 | # --build-arg="VERSION=2.1.0-test-5" . 14 | 15 | # Good guide on how to set up a buildx builder here: 16 | # https://a-berahman.medium.com/simplifying-docker-multiplatform-builds-with-buildx-3d7efd670f58 17 | 18 | # Multi-Platform Example Build & Push Command: 19 | # docker buildx build \ 20 | # --push \ 21 | # --platform linux/amd64,linux/arm64, \ 22 | # --build-arg="BUILD_DATE=02-08-2024 20:52" \ 23 | # --build-arg="VERSION=2.1.0" \ 24 | # --tag crocodilestick/calibre-web-automated:latest . 25 | 26 | ARG BUILD_DATE 27 | ARG VERSION 28 | ARG CALIBREWEB_RELEASE=0.6.24 29 | ARG LSCW_RELEASE=0.6.24-ls304 30 | ARG CALIBRE_RELEASE=8.4.0 31 | ARG KEPUBIFY_RELEASE=v4.0.4 32 | LABEL build_version="Version:- ${VERSION}" 33 | LABEL build_date="${BUILD_DATE}" 34 | LABEL CW-Stock-version="${CALIBREWEB_RELEASE}" 35 | LABEL LSCW_Image_Release="${LSCW_RELEASE}" 36 | LABEL maintainer="CrocodileStick" 37 | 38 | # Copy local files into the container 39 | COPY --chown=abc:abc . /app/calibre-web-automated/ 40 | 41 | # STEP 1 - Install stock Calibre-Web 42 | RUN \ 43 | # STEP 1.1 - Installs required build & runtime packages 44 | echo "**** install build packages ****" && \ 45 | apt-get update && \ 46 | apt-get install -y --no-install-recommends \ 47 | build-essential \ 48 | libldap2-dev \ 49 | libsasl2-dev \ 50 | python3-dev && \ 51 | echo "**** install runtime packages ****" && \ 52 | apt-get install -y --no-install-recommends \ 53 | imagemagick \ 54 | ghostscript \ 55 | libldap-2.5-0 \ 56 | libmagic1 \ 57 | libsasl2-2 \ 58 | libxi6 \ 59 | libxslt1.1 \ 60 | python3-venv && \ 61 | echo "**** install calibre-web ****" && \ 62 | # STEP 1.2 - Check that $CALIBREWEB_RELEASE ARG is not none and if it is, sets the variables value to the most recent tag name 63 | if [ -z ${CALIBREWEB_RELEASE+x} ]; then \ 64 | CALIBREWEB_RELEASE=$(curl -sX GET "https://api.github.com/repos/janeczku/calibre-web/releases/latest" \ 65 | | awk '/tag_name/{print $4;exit}' FS='[""]'); \ 66 | fi && \ 67 | # STEP 1.3 - Downloads the tarball of the release stored in $CALIBREWEB_RELEASE from CW's GitHub Repo, saving it into /tmp 68 | curl -o \ 69 | /tmp/calibre-web.tar.gz -L \ 70 | https://github.com/janeczku/calibre-web/archive/${CALIBREWEB_RELEASE}.tar.gz && \ 71 | # STEP 1.4 - Makes /app/calibre-web to extract the downloaded files from the repo to, -p to ignore potential errors that could arise if the folder already existed 72 | mkdir -p \ 73 | /app/calibre-web && \ 74 | # STEP 1.5 - Extracts the contents of the tar.gz file downloaded from the repo to the /app/calibre-web dir previously created 75 | tar xf \ 76 | /tmp/calibre-web.tar.gz -C \ 77 | /app/calibre-web --strip-components=1 && \ 78 | # STEP 1.6 - Sets up a python virtual environment and installs pip and wheel packages 79 | cd /app/calibre-web && \ 80 | python3 -m venv /lsiopy && \ 81 | pip install -U --no-cache-dir \ 82 | pip \ 83 | wheel && \ 84 | # STEP 1.7 - Installing the required python packages listed in 'requirements.txt' and 'optional-requirements.txt' 85 | # HOWEVER, they are not pulled from PyPi directly, they are pulled from linuxserver's Ubuntu Wheel Index 86 | # This is essentially a repository of precompiled some of the most popular packages with C/C++ source code 87 | # This provides the install maximum compatibility with multiple different architectures including: x86_64, armv71 and aarch64 88 | # You can read more about python wheels here: https://realpython.com/python-wheels/ 89 | pip install -U --no-cache-dir --find-links https://wheel-index.linuxserver.io/ubuntu/ -r \ 90 | requirements.txt -r \ 91 | optional-requirements.txt && \ 92 | # STEP 1.8 - Installs kepubify 93 | echo "**** install kepubify ****" && \ 94 | if [[ $KEPUBIFY_RELEASE == 'newest' ]]; then \ 95 | KEPUBIFY_RELEASE=$(curl -sX GET "https://api.github.com/repos/pgaskin/kepubify/releases/latest" \ 96 | | awk '/tag_name/{print $4;exit}' FS='[""]'); \ 97 | fi && \ 98 | if [ "$(uname -m)" == "x86_64" ]; then \ 99 | curl -o \ 100 | /usr/bin/kepubify -L \ 101 | https://github.com/pgaskin/kepubify/releases/download/${KEPUBIFY_RELEASE}/kepubify-linux-64bit; \ 102 | elif [ "$(uname -m)" == "aarch64" ]; then \ 103 | curl -o \ 104 | /usr/bin/kepubify -L \ 105 | https://github.com/pgaskin/kepubify/releases/download/${KEPUBIFY_RELEASE}/kepubify-linux-arm64; \ 106 | fi && \ 107 | # STEP 2 - Install Calibre-Web Automated 108 | echo "~~~~ CWA Install - installing additional required packages ~~~~" && \ 109 | # STEP 2.1 - Install additional required packages 110 | apt-get update && \ 111 | apt-get install -y --no-install-recommends \ 112 | xdg-utils \ 113 | inotify-tools \ 114 | python3 \ 115 | python3-pip \ 116 | nano \ 117 | sqlite3 && \ 118 | # STEP 2.2 - Install additional required python packages 119 | pip install -r /app/calibre-web-automated/requirements.txt && \ 120 | # STEP 2.3 - Get required 'root' dir from the linuxserver/docker-calibre-web repo 121 | echo "~~~~ Getting required files from linuxserver/docker-calibre-web... ~~~~" && \ 122 | # STEP 2.4.1 - Check the most recent release of linuxserver/docker-calibre-web and store it's tag in LSCW_RELEASE if one was not specified as an ARG 123 | if [[ $LSCW_RELEASE == 'newest' ]]; then \ 124 | LSCW_RELEASE=$(curl -sX GET "https://api.github.com/repos/linuxserver/docker-calibre-web/releases/latest" \ 125 | | awk '/tag_name/{print $4;exit}' FS='[""]'); \ 126 | fi && \ 127 | # STEP 2.4.2 - Download the most recent LSCW release to /tmp 128 | curl -o \ 129 | /tmp/lscw.tar.gz -L \ 130 | https://github.com/linuxserver/docker-calibre-web/archive/refs/tags/${LSCW_RELEASE}.tar.gz && \ 131 | # STEP 2.4.3 - Makes /app/calibre-web to extract the downloaded files from the repo to, -p to ignore potential errors that could arise if the folder already existed 132 | mkdir -p \ 133 | /tmp/lscw && \ 134 | # STEP 2.4.4 - Extract contents of lscw.tat.gz to /tmp/lscw 135 | tar xf \ 136 | /tmp/lscw.tar.gz -C \ 137 | /tmp/lscw --strip-components=1 && \ 138 | # STEP 2.4.5 - Move contents of 'root' dirs to root dir 139 | cp -R /tmp/lscw/root/* / && \ 140 | cp -R /app/calibre-web-automated/root/* / && \ 141 | # STEP 2.4.6 - Remove the temp files 142 | rm -R /app/calibre-web-automated/root/ && \ 143 | rm -R /tmp/lscw/root/ && \ 144 | # STEP 2.5 - ADD files referencing the versions of the installed main packages 145 | # CALIBRE_RELEASE is placed in root by universal calibre below and containers the calibre version being used 146 | echo "$VERSION" >| /app/CWA_RELEASE && \ 147 | echo "$LSCW_RELEASE" >| /app/LSCW_RELEASE && \ 148 | echo "$KEPUBIFY_RELEASE" >| /app/KEPUBIFY_RELEASE && \ 149 | # STEP 2.6 - Run CWA install script to make required dirs, set script permissions and add aliases for CLI commands ect. 150 | chmod +x /app/calibre-web-automated/scripts/setup-cwa.sh && \ 151 | /app/calibre-web-automated/scripts/setup-cwa.sh && \ 152 | # STEP 3 - Install Universal Calibre 153 | # STEP 3.1 - Install additional required packages 154 | apt-get update && \ 155 | apt-get install -y --no-install-recommends \ 156 | libxtst6 \ 157 | libxrandr2 \ 158 | libxkbfile1 \ 159 | libxcomposite1 \ 160 | libopengl0 \ 161 | libnss3 \ 162 | libxkbcommon0 \ 163 | libegl1 \ 164 | libxdamage1 \ 165 | libgl1 \ 166 | libglx-mesa0 \ 167 | xz-utils \ 168 | binutils && \ 169 | # STEP 3.2 - Make the /app/calibre directory for the installed files 170 | mkdir -p \ 171 | /app/calibre && \ 172 | # STEP 3.3 - Download the desired version of Calibre, determined by the CALIBRE_RELEASE variable and the architecture of the build environment 173 | if [ "$(uname -m)" == "x86_64" ]; then \ 174 | curl -o \ 175 | /calibre.txz -L \ 176 | "https://download.calibre-ebook.com/${CALIBRE_RELEASE}/calibre-${CALIBRE_RELEASE}-x86_64.txz"; \ 177 | elif [ "$(uname -m)" == "aarch64" ]; then \ 178 | curl -o \ 179 | /calibre.txz -L \ 180 | "https://download.calibre-ebook.com/${CALIBRE_RELEASE}/calibre-${CALIBRE_RELEASE}-arm64.txz"; \ 181 | fi && \ 182 | # STEP 3.4 - Extract the downloaded file to /app/calibre 183 | tar xf \ 184 | /calibre.txz -C \ 185 | /app/calibre && \ 186 | # STEP 3.4.1 - Remove the ABI tag from the extracted libQt6* files to allow them to be used on older kernels 187 | strip --remove-section=.note.ABI-tag /app/calibre/lib/libQt6* && \ 188 | # STEP 3.5 - Delete the extracted calibre.txz to save space in final image 189 | rm /calibre.txz && \ 190 | # STEP 3.6 - Store the CALIBRE_RELEASE in the root of the image in CALIBRE_RELEASE 191 | echo $CALIBRE_RELEASE > /CALIBRE_RELEASE 192 | 193 | # Removes packages that are no longer required, also emptying dirs used to build the image that are no longer needed 194 | RUN \ 195 | echo "**** cleanup ****" && \ 196 | apt-get -y purge \ 197 | build-essential \ 198 | libldap2-dev \ 199 | libsasl2-dev \ 200 | python3-dev && \ 201 | apt-get -y autoremove && \ 202 | rm -rf \ 203 | /tmp/* \ 204 | /var/lib/apt/lists/* \ 205 | /var/tmp/* \ 206 | /root/.cache 207 | 208 | # add unrar 209 | COPY --from=unrar /usr/bin/unrar-ubuntu /usr/bin/unrar 210 | 211 | # ports and volumes 212 | EXPOSE 8083 213 | VOLUME /config 214 | VOLUME /cwa-book-ingest 215 | VOLUME /calibre-library 216 | -------------------------------------------------------------------------------- /README_images/CWA-Homepage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crocodilestick/Calibre-Web-Automated/3746332a6d04e518f194551caa6363851e0e0e5d/README_images/CWA-Homepage.png -------------------------------------------------------------------------------- /README_images/CWA-banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crocodilestick/Calibre-Web-Automated/3746332a6d04e518f194551caa6363851e0e0e5d/README_images/CWA-banner.png -------------------------------------------------------------------------------- /README_images/CWA-new-process-ui.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crocodilestick/Calibre-Web-Automated/3746332a6d04e518f194551caa6363851e0e0e5d/README_images/CWA-new-process-ui.gif -------------------------------------------------------------------------------- /README_images/V3-settings-infographic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crocodilestick/Calibre-Web-Automated/3746332a6d04e518f194551caa6363851e0e0e5d/README_images/V3-settings-infographic.png -------------------------------------------------------------------------------- /README_images/cwa-bulk-editting-diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crocodilestick/Calibre-Web-Automated/3746332a6d04e518f194551caa6363851e0e0e5d/README_images/cwa-bulk-editting-diagram.png -------------------------------------------------------------------------------- /README_images/cwa-db-diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crocodilestick/Calibre-Web-Automated/3746332a6d04e518f194551caa6363851e0e0e5d/README_images/cwa-db-diagram.png -------------------------------------------------------------------------------- /README_images/cwa-enforcer-diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crocodilestick/Calibre-Web-Automated/3746332a6d04e518f194551caa6363851e0e0e5d/README_images/cwa-enforcer-diagram.png -------------------------------------------------------------------------------- /README_images/cwa-logo-round-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crocodilestick/Calibre-Web-Automated/3746332a6d04e518f194551caa6363851e0e0e5d/README_images/cwa-logo-round-dark.png -------------------------------------------------------------------------------- /README_images/cwa-logo-round-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crocodilestick/Calibre-Web-Automated/3746332a6d04e518f194551caa6363851e0e0e5d/README_images/cwa-logo-round-light.png -------------------------------------------------------------------------------- /README_images/cwa-rebrand-ui.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crocodilestick/Calibre-Web-Automated/3746332a6d04e518f194551caa6363851e0e0e5d/README_images/cwa-rebrand-ui.png -------------------------------------------------------------------------------- /README_images/cwa-server-stats-page.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crocodilestick/Calibre-Web-Automated/3746332a6d04e518f194551caa6363851e0e0e5d/README_images/cwa-server-stats-page.png -------------------------------------------------------------------------------- /README_images/new-book-list.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crocodilestick/Calibre-Web-Automated/3746332a6d04e518f194551caa6363851e0e0e5d/README_images/new-book-list.png -------------------------------------------------------------------------------- /README_images/old-book-list.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crocodilestick/Calibre-Web-Automated/3746332a6d04e518f194551caa6363851e0e0e5d/README_images/old-book-list.png -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Enter location for cwa repo files below 4 | REPO_DIR="/home/cwa-repo-download" 5 | # Enter your DockerHub username here 6 | DH_USER="crocodilestick" 7 | 8 | # Use the following guide to set up buildx on your own system to build multi-platform images 9 | # https://a-berahman.medium.com/simplifying-docker-multiplatform-builds-with-buildx-3d7efd670f58 10 | 11 | rm -r -f $REPO_DIR 12 | git clone http://github.com/crocodilestick/calibre-web-automated.git $REPO_DIR 13 | cd $REPO_DIR 14 | 15 | echo 16 | echo Enter Version Number\: \(Convention is e.g. V2.0.1\) 17 | read version 18 | 19 | echo Dev or Production Image? Enter \'dev\' or \'prod\' to choose: 20 | while true ; do 21 | read type 22 | case $type in 23 | dev | Dev | DEV) 24 | type="dev" 25 | echo Enter test version number\: 26 | read testnum 27 | break 28 | ;; 29 | 30 | prod | Prod | PROD) 31 | type="prod" 32 | break 33 | ;; 34 | 35 | *) 36 | echo Invalid entry. Please try again. 37 | ;; 38 | esac 39 | done 40 | 41 | NOW="$(date +"%Y-%m-%d %H:%M:%S")" 42 | 43 | if [ type == "dev" ]; then 44 | docker build --tag $DH_USER/calibre-web-automated:dev --build-arg="BUILD_DATE=$NOW" --build-arg="VERSION=$version-TEST-$testnum" . 45 | echo 46 | echo "Dev image Version $version - Test $testnum created! Exiting now... " 47 | else 48 | docker build --tag $DH_USER/calibre-web-automated:$version --build-arg="BUILD_DATE=$NOW" --build-arg="VERSION=$version" . 49 | echo 50 | echo "Prod image Version $version created! Exiting now..." 51 | fi 52 | 53 | cd -------------------------------------------------------------------------------- /dirs.json: -------------------------------------------------------------------------------- 1 | { 2 | "ingest_folder":"/cwa-book-ingest", 3 | "calibre_library_dir":"/calibre-library", 4 | "tmp_conversion_dir":"/config/.cwa_conversion_tmp" 5 | } -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | --- 2 | services: 3 | calibre-web-automated: 4 | image: crocodilestick/calibre-web-automated:latest 5 | container_name: calibre-web-automated 6 | environment: 7 | # Only change these if you know what you're doing 8 | - PUID=1000 9 | - PGID=1000 10 | # Edit to match your current timezone https://en.wikipedia.org/wiki/List_of_tz_database_time_zones 11 | - TZ=UTC 12 | volumes: 13 | # CW users migrating should stop their existing CW instance, make a copy of the config folder, and bind that here to carry over all of their user settings ect. 14 | - /path/to/config/folder:/config 15 | # This is an ingest dir, NOT a library one. Anything added here will be automatically added to your library according to the settings you have configured in CWA Settings page. All files placed here are REMOVED AFTER PROCESSING 16 | - /path/to/the/folder/you/want/to/use/for/book/ingest:/cwa-book-ingest 17 | # If you don't have an existing library, CWA will automatically create one at the bind provided here 18 | - /path/to/your/calibre/library:/calibre-library 19 | ports: 20 | # Change the first number to change the port you want to access the Web UI, not the second 21 | - 8083:8083 22 | restart: unless-stopped -------------------------------------------------------------------------------- /empty_library/app.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crocodilestick/Calibre-Web-Automated/3746332a6d04e518f194551caa6363851e0e0e5d/empty_library/app.db -------------------------------------------------------------------------------- /empty_library/metadata.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crocodilestick/Calibre-Web-Automated/3746332a6d04e518f194551caa6363851e0e0e5d/empty_library/metadata.db -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | certifi==2024.7.4 2 | charset-normalizer==3.3.2 3 | idna==3.7 4 | requests==2.32.3 5 | tabulate==0.9.0 6 | urllib3==2.2.2 -------------------------------------------------------------------------------- /root/app/calibre-web/cps/constants.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web) 4 | # Copyright (C) 2019 OzzieIsaacs, pwr 5 | # 6 | # This program is free software: you can redistribute it and/or modify 7 | # it under the terms of the GNU General Public License as published by 8 | # the Free Software Foundation, either version 3 of the License, or 9 | # (at your option) any later version. 10 | # 11 | # This program is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU General Public License 17 | # along with this program. If not, see . 18 | 19 | import sys 20 | import os 21 | from collections import namedtuple 22 | 23 | # APP_MODE - production, development, or test 24 | APP_MODE = os.environ.get('APP_MODE', 'production') 25 | 26 | # if installed via pip this variable is set to true (empty file with name .HOMEDIR present) 27 | HOME_CONFIG = os.path.isfile(os.path.join(os.path.dirname(os.path.abspath(__file__)), '.HOMEDIR')) 28 | 29 | # In executables updater is not available, so variable is set to False there 30 | UPDATER_AVAILABLE = False 31 | 32 | # Base dir is parent of current file, necessary if called from different folder 33 | BASE_DIR = os.path.abspath(os.path.join(os.path.dirname(os.path.abspath(__file__)), os.pardir)) 34 | # if executable file the files should be placed in the parent dir (parallel to the exe file) 35 | 36 | STATIC_DIR = os.path.join(BASE_DIR, 'cps', 'static') 37 | TEMPLATES_DIR = os.path.join(BASE_DIR, 'cps', 'templates') 38 | TRANSLATIONS_DIR = os.path.join(BASE_DIR, 'cps', 'translations') 39 | 40 | # Cache dir - use CACHE_DIR environment variable, otherwise use the default directory: cps/cache 41 | DEFAULT_CACHE_DIR = os.path.join(BASE_DIR, 'cps', 'cache') 42 | CACHE_DIR = os.environ.get('CACHE_DIR', DEFAULT_CACHE_DIR) 43 | 44 | if HOME_CONFIG: 45 | home_dir = os.path.join(os.path.expanduser("~"), ".calibre-web") 46 | if not os.path.exists(home_dir): 47 | os.makedirs(home_dir) 48 | CONFIG_DIR = os.environ.get('CALIBRE_DBPATH', home_dir) 49 | else: 50 | CONFIG_DIR = os.environ.get('CALIBRE_DBPATH', BASE_DIR) 51 | if getattr(sys, 'frozen', False): 52 | CONFIG_DIR = os.path.abspath(os.path.join(CONFIG_DIR, os.pardir)) 53 | 54 | 55 | DEFAULT_SETTINGS_FILE = "app.db" 56 | DEFAULT_GDRIVE_FILE = "gdrive.db" 57 | 58 | ROLE_USER = 0 << 0 59 | ROLE_ADMIN = 1 << 0 60 | ROLE_DOWNLOAD = 1 << 1 61 | ROLE_UPLOAD = 1 << 2 62 | ROLE_EDIT = 1 << 3 63 | ROLE_PASSWD = 1 << 4 64 | ROLE_ANONYMOUS = 1 << 5 65 | ROLE_EDIT_SHELFS = 1 << 6 66 | ROLE_DELETE_BOOKS = 1 << 7 67 | ROLE_VIEWER = 1 << 8 68 | 69 | ALL_ROLES = { 70 | "admin_role": ROLE_ADMIN, 71 | "download_role": ROLE_DOWNLOAD, 72 | "upload_role": ROLE_UPLOAD, 73 | "edit_role": ROLE_EDIT, 74 | "passwd_role": ROLE_PASSWD, 75 | "edit_shelf_role": ROLE_EDIT_SHELFS, 76 | "delete_role": ROLE_DELETE_BOOKS, 77 | "viewer_role": ROLE_VIEWER, 78 | } 79 | 80 | DETAIL_RANDOM = 1 << 0 81 | SIDEBAR_LANGUAGE = 1 << 1 82 | SIDEBAR_SERIES = 1 << 2 83 | SIDEBAR_CATEGORY = 1 << 3 84 | SIDEBAR_HOT = 1 << 4 85 | SIDEBAR_RANDOM = 1 << 5 86 | SIDEBAR_AUTHOR = 1 << 6 87 | SIDEBAR_BEST_RATED = 1 << 7 88 | SIDEBAR_READ_AND_UNREAD = 1 << 8 89 | SIDEBAR_RECENT = 1 << 9 90 | SIDEBAR_SORTED = 1 << 10 91 | MATURE_CONTENT = 1 << 11 92 | SIDEBAR_PUBLISHER = 1 << 12 93 | SIDEBAR_RATING = 1 << 13 94 | SIDEBAR_FORMAT = 1 << 14 95 | SIDEBAR_ARCHIVED = 1 << 15 96 | SIDEBAR_DOWNLOAD = 1 << 16 97 | SIDEBAR_LIST = 1 << 17 98 | 99 | sidebar_settings = { 100 | "detail_random": DETAIL_RANDOM, 101 | "sidebar_language": SIDEBAR_LANGUAGE, 102 | "sidebar_series": SIDEBAR_SERIES, 103 | "sidebar_category": SIDEBAR_CATEGORY, 104 | "sidebar_random": SIDEBAR_RANDOM, 105 | "sidebar_author": SIDEBAR_AUTHOR, 106 | "sidebar_best_rated": SIDEBAR_BEST_RATED, 107 | "sidebar_read_and_unread": SIDEBAR_READ_AND_UNREAD, 108 | "sidebar_recent": SIDEBAR_RECENT, 109 | "sidebar_sorted": SIDEBAR_SORTED, 110 | "sidebar_publisher": SIDEBAR_PUBLISHER, 111 | "sidebar_rating": SIDEBAR_RATING, 112 | "sidebar_format": SIDEBAR_FORMAT, 113 | "sidebar_archived": SIDEBAR_ARCHIVED, 114 | "sidebar_download": SIDEBAR_DOWNLOAD, 115 | "sidebar_list": SIDEBAR_LIST, 116 | } 117 | 118 | 119 | ADMIN_USER_ROLES = sum(r for r in ALL_ROLES.values()) & ~ROLE_ANONYMOUS 120 | ADMIN_USER_SIDEBAR = (SIDEBAR_LIST << 1) - 1 121 | 122 | UPDATE_STABLE = 0 << 0 123 | AUTO_UPDATE_STABLE = 1 << 0 124 | UPDATE_NIGHTLY = 1 << 1 125 | AUTO_UPDATE_NIGHTLY = 1 << 2 126 | 127 | LOGIN_STANDARD = 0 128 | LOGIN_LDAP = 1 129 | LOGIN_OAUTH = 2 130 | 131 | LDAP_AUTH_ANONYMOUS = 0 132 | LDAP_AUTH_UNAUTHENTICATE = 1 133 | LDAP_AUTH_SIMPLE = 0 134 | 135 | DEFAULT_MAIL_SERVER = "mail.example.org" 136 | 137 | DEFAULT_PASSWORD = "admin123" # nosec 138 | DEFAULT_PORT = 8083 139 | env_CALIBRE_PORT = os.environ.get("CALIBRE_PORT", DEFAULT_PORT) 140 | try: 141 | DEFAULT_PORT = int(env_CALIBRE_PORT) 142 | except ValueError: 143 | print('Environment variable CALIBRE_PORT has invalid value (%s), faling back to default (8083)' % env_CALIBRE_PORT) 144 | del env_CALIBRE_PORT 145 | 146 | 147 | EXTENSIONS_AUDIO = {'mp3', 'mp4', 'ogg', 'opus', 'wav', 'flac', 'm4a', 'm4b'} 148 | EXTENSIONS_CONVERT_FROM = ['pdf', 'epub', 'mobi', 'azw3', 'docx', 'rtf', 'fb2', 'lit', 'lrf', 149 | 'txt', 'htmlz', 'rtf', 'odt', 'cbz', 'cbr', 'prc'] 150 | EXTENSIONS_CONVERT_TO = ['pdf', 'epub', 'mobi', 'azw3', 'docx', 'rtf', 'fb2', 151 | 'lit', 'lrf', 'txt', 'htmlz', 'rtf', 'odt'] 152 | EXTENSIONS_UPLOAD = {'txt', 'pdf', 'epub', 'kepub', 'mobi', 'azw', 'azw3', 'cbr', 'cbz', 'cbt', 'cb7', 'djvu', 'djv', 153 | 'prc', 'doc', 'docx', 'fb2', 'html', 'rtf', 'lit', 'odt', 'mp3', 'mp4', 'ogg', 154 | 'opus', 'wav', 'flac', 'm4a', 'm4b'} 155 | 156 | _extension = "" 157 | if sys.platform == "win32": 158 | _extension = ".exe" 159 | SUPPORTED_CALIBRE_BINARIES = {binary: binary + _extension for binary in ["ebook-convert", "calibredb"]} 160 | 161 | 162 | def has_flag(value, bit_flag): 163 | return bit_flag == (bit_flag & (value or 0)) 164 | 165 | 166 | def selected_roles(dictionary): 167 | return sum(v for k, v in ALL_ROLES.items() if k in dictionary) 168 | 169 | 170 | # :rtype: BookMeta 171 | BookMeta = namedtuple('BookMeta', 'file_path, extension, title, author, cover, description, tags, series, ' 172 | 'series_id, languages, publisher, pubdate, identifiers') 173 | 174 | # python build process likes to have x.y.zbw -> b for beta and w a counting number 175 | STABLE_VERSION = '0.6.24' 176 | 177 | NIGHTLY_VERSION = dict() 178 | NIGHTLY_VERSION[0] = '0af52f205358b0147ee3430f9e6c8fe007c0ea77' 179 | NIGHTLY_VERSION[1] = '2024-11-16T07:21:28+01:00' 180 | 181 | # CACHE 182 | CACHE_TYPE_THUMBNAILS = 'thumbnails' 183 | 184 | # Thumbnail Types 185 | THUMBNAIL_TYPE_COVER = 1 186 | THUMBNAIL_TYPE_SERIES = 2 187 | THUMBNAIL_TYPE_AUTHOR = 3 188 | 189 | # Thumbnails Sizes 190 | COVER_THUMBNAIL_ORIGINAL = 0 191 | COVER_THUMBNAIL_SMALL = 1 192 | COVER_THUMBNAIL_MEDIUM = 2 193 | COVER_THUMBNAIL_LARGE = 4 194 | 195 | # clean-up the module namespace 196 | del sys, os, namedtuple 197 | -------------------------------------------------------------------------------- /root/app/calibre-web/cps/main.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web) 4 | # Copyright (C) 2012-2022 OzzieIsaacs 5 | # 6 | # This program is free software: you can redistribute it and/or modify 7 | # it under the terms of the GNU General Public License as published by 8 | # the Free Software Foundation, either version 3 of the License, or 9 | # (at your option) any later version. 10 | # 11 | # This program is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU General Public License 17 | # along with this program. If not, see . 18 | 19 | import sys 20 | 21 | from . import create_app, limiter 22 | from .jinjia import jinjia 23 | from flask import request 24 | 25 | 26 | def request_username(): 27 | return request.authorization.username 28 | 29 | 30 | def main(): 31 | app = create_app() 32 | 33 | from .cwa_functions import switch_theme, library_refresh, convert_library, epub_fixer, cwa_stats, cwa_check_status, cwa_settings, cwa_logs 34 | from .web import web 35 | from .opds import opds 36 | from .admin import admi 37 | from .gdrive import gdrive 38 | from .editbooks import editbook 39 | from .about import about 40 | from .search import search 41 | from .search_metadata import meta 42 | from .shelf import shelf 43 | from .tasks_status import tasks 44 | from .error_handler import init_errorhandler 45 | from .remotelogin import remotelogin 46 | try: 47 | from .kobo import kobo, get_kobo_activated 48 | from .kobo_auth import kobo_auth 49 | from flask_limiter.util import get_remote_address 50 | kobo_available = get_kobo_activated() 51 | except (ImportError, AttributeError): # Catch also error for not installed flask-WTF (missing csrf decorator) 52 | kobo_available = False 53 | kobo = kobo_auth = get_remote_address = None 54 | 55 | try: 56 | from .oauth_bb import oauth 57 | oauth_available = True 58 | except ImportError: 59 | oauth_available = False 60 | oauth = None 61 | 62 | from . import web_server 63 | init_errorhandler() 64 | 65 | # CWA Blueprints 66 | app.register_blueprint(switch_theme) 67 | app.register_blueprint(library_refresh) 68 | app.register_blueprint(convert_library) 69 | app.register_blueprint(epub_fixer) 70 | app.register_blueprint(cwa_stats) 71 | app.register_blueprint(cwa_check_status) 72 | app.register_blueprint(cwa_settings) 73 | app.register_blueprint(cwa_logs) 74 | 75 | # Stock CW 76 | app.register_blueprint(search) 77 | app.register_blueprint(tasks) 78 | app.register_blueprint(web) 79 | app.register_blueprint(opds) 80 | limiter.limit("3/minute", key_func=request_username)(opds) 81 | app.register_blueprint(jinjia) 82 | app.register_blueprint(about) 83 | app.register_blueprint(shelf) 84 | app.register_blueprint(admi) 85 | app.register_blueprint(remotelogin) 86 | app.register_blueprint(meta) 87 | app.register_blueprint(gdrive) 88 | app.register_blueprint(editbook) 89 | if kobo_available: 90 | app.register_blueprint(kobo) 91 | app.register_blueprint(kobo_auth) 92 | limiter.limit("3/minute", key_func=get_remote_address)(kobo) 93 | if oauth_available: 94 | app.register_blueprint(oauth) 95 | success = web_server.start() 96 | sys.exit(0 if success else 1) 97 | -------------------------------------------------------------------------------- /root/app/calibre-web/cps/metadata_provider/hardcover.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web) 4 | # Copyright (C) 2021 OzzieIsaacs 5 | # 6 | # This program is free software: you can redistribute it and/or modify 7 | # it under the terms of the GNU General Public License as published by 8 | # the Free Software Foundation, either version 3 of the License, or 9 | # (at your option) any later version. 10 | # 11 | # This program is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU General Public License 17 | # along with this program. If not, see . 18 | 19 | # Hardcover api document: https://Hardcover.gamespot.com/api/documentation 20 | from typing import Dict, List, Optional 21 | 22 | import requests 23 | from cps import logger, config 24 | from cps.services.Metadata import MetaRecord, MetaSourceInfo, Metadata 25 | from cps.isoLanguages import get_language_name 26 | from ..cw_login import current_user 27 | from os import getenv 28 | 29 | log = logger.create() 30 | 31 | class Hardcover(Metadata): 32 | __name__ = "Hardcover" 33 | __id__ = "hardcover" 34 | DESCRIPTION = "Hardcover Books" 35 | META_URL = "https://hardcover.app" 36 | BASE_URL = "https://api.hardcover.app/v1/graphql" 37 | SEARCH_QUERY = """query Search($query: String!) { 38 | search(query: $query, query_type: "Book", per_page: 50) { 39 | results 40 | } 41 | } 42 | """ 43 | EDITION_QUERY = """query getEditions($query: Int!) { 44 | books( 45 | where: { id: { _eq: $query } } 46 | order_by: { users_read_count: desc_nulls_last } 47 | ) { 48 | title 49 | slug 50 | id 51 | 52 | book_series { 53 | series { 54 | name 55 | } 56 | position 57 | } 58 | rating 59 | editions( 60 | where: { 61 | _or: [{ reading_format_id: { _neq: 2 } }, { edition_format: { _is_null: true } }] 62 | } 63 | order_by: [{ reading_format_id: desc_nulls_last },{users_count: desc_nulls_last }] 64 | ) { 65 | id 66 | isbn_13 67 | isbn_10 68 | title 69 | reading_format_id 70 | contributions { 71 | author { 72 | name 73 | } 74 | } 75 | image { 76 | url 77 | } 78 | language { 79 | code3 80 | } 81 | publisher { 82 | name 83 | } 84 | release_date 85 | 86 | } 87 | description 88 | cached_tags(path: "Genre") 89 | } 90 | } 91 | """ 92 | HEADERS = { 93 | "Content-Type": "application/json", 94 | } 95 | FORMATS = ["","Physical Book","","","E-Book"] # Map reading_format_id to text equivelant. 96 | 97 | def search( 98 | self, query: str, generic_cover: str = "", locale: str = "en" 99 | ) -> Optional[List[MetaRecord]]: 100 | val = list() 101 | if self.active: 102 | try: 103 | token = current_user.hardcover_token or config.config_hardcover_token or getenv("HARDCOVER_TOKEN") 104 | if not token: 105 | self.set_status(False) 106 | raise Exception("Hardcover token not set for user, and no global token provided.") 107 | edition_seach = query.split(":")[0] == "hardcover-id" 108 | Hardcover.HEADERS["Authorization"] = "Bearer %s" % token.replace("Bearer ","") 109 | result = requests.post( 110 | Hardcover.BASE_URL, 111 | json={ 112 | "query":Hardcover.SEARCH_QUERY if not edition_seach else Hardcover.EDITION_QUERY, 113 | "variables":{"query":query if not edition_seach else query.split(":")[1]} 114 | }, 115 | headers=Hardcover.HEADERS, 116 | ) 117 | result.raise_for_status() 118 | except Exception as e: 119 | log.warning(e) 120 | return None 121 | if edition_seach: 122 | result = result.json()["data"]["books"][0] 123 | val = self._parse_edition_results(result=result, generic_cover=generic_cover, locale=locale) 124 | else: 125 | for result in result.json()["data"]["search"]["results"]["hits"]: 126 | match = self._parse_title_result( 127 | result=result, generic_cover=generic_cover, locale=locale 128 | ) 129 | val.append(match) 130 | return val 131 | 132 | def _parse_title_result( 133 | self, result: Dict, generic_cover: str, locale: str 134 | ) -> MetaRecord: 135 | series = result["document"].get("featured_series",{}).get("series_name", "") 136 | series_index = result["document"].get("featured_series",{}).get("position", "") 137 | match = MetaRecord( 138 | id=result["document"].get("id",""), 139 | title=result["document"].get("title",""), 140 | authors=result["document"].get("author_names", []), 141 | url=self._parse_title_url(result, ""), 142 | source=MetaSourceInfo( 143 | id=self.__id__, 144 | description=Hardcover.DESCRIPTION, 145 | link=Hardcover.META_URL, 146 | ), 147 | series=series, 148 | ) 149 | match.cover = result["document"]["image"].get("url", generic_cover) 150 | 151 | match.description = result["document"].get("description","") 152 | match.publishedDate = result["document"].get( 153 | "release_date", "") 154 | match.series_index = series_index 155 | match.tags = result["document"].get("genres",[]) 156 | match.identifiers = { 157 | "hardcover-id": match.id, 158 | "hardcover-slug": result["document"].get("slug", "") 159 | } 160 | return match 161 | 162 | def _parse_edition_results( 163 | self, result: Dict, generic_cover: str, locale: str 164 | ) -> MetaRecord: 165 | editions = list() 166 | id = result.get("id","") 167 | for edition in result["editions"]: 168 | match = MetaRecord( 169 | id=id, 170 | title=edition.get("title",""), 171 | authors=self._parse_edition_authors(edition,[]), 172 | url=self._parse_edition_url(result, edition, ""), 173 | source=MetaSourceInfo( 174 | id=self.__id__, 175 | description=Hardcover.DESCRIPTION, 176 | link=Hardcover.META_URL, 177 | ), 178 | series=(result.get("book_series") or [{}])[0].get("series",{}).get("name", ""), 179 | ) 180 | match.cover = (edition.get("image") or {}).get("url", generic_cover) 181 | match.description = result.get("description","") 182 | match.publisher = (edition.get("publisher") or {}).get("name","") 183 | match.publishedDate = edition.get("release_date", "") 184 | match.series_index = (result.get("book_series") or [{}])[0].get("position", "") 185 | match.tags = self._parse_tags(result,[]) 186 | match.languages = self._parse_languages(edition,locale) 187 | match.identifiers = { 188 | "hardcover-id": id, 189 | "hardcover-slug": result.get("slug", ""), 190 | "hardcover-edition": edition.get("id",""), 191 | "isbn": (edition.get("isbn_13",edition.get("isbn_10")) or "") 192 | } 193 | isbn = edition.get("isbn_13",edition.get("isbn_10")) 194 | if isbn: 195 | match.identifiers["isbn"] = isbn 196 | match.format = Hardcover.FORMATS[edition.get("reading_format_id",0)] 197 | editions.append(match) 198 | return editions 199 | 200 | @staticmethod 201 | def _parse_title_url(result: Dict, url: str) -> str: 202 | hardcover_slug = result["document"].get("slug", "") 203 | if hardcover_slug: 204 | return f"https://hardcover.app/books/{hardcover_slug}" 205 | return url 206 | 207 | @staticmethod 208 | def _parse_edition_url(result: Dict, edition: Dict, url: str) -> str: 209 | edition = edition.get("id", "") 210 | slug = result.get("slug","") 211 | if edition: 212 | return f"https://hardcover.app/books/{slug}/editions/{edition}" 213 | return url 214 | 215 | @staticmethod 216 | def _parse_edition_authors(edition: Dict, authors: List[str]) -> List[str]: 217 | try: 218 | return [author["author"]["name"] for author in edition.get("contributions",[]) if "author" in author and "name" in author["author"]] 219 | except Exception as e: 220 | log.warning(e) 221 | return authors 222 | 223 | @staticmethod 224 | def _parse_tags(result: Dict, tags: List[str]) -> List[str]: 225 | try: 226 | return [item["tag"] for item in result["cached_tags"] if "tag" in item] 227 | except Exception as e: 228 | log.warning(e) 229 | return tags 230 | 231 | @staticmethod 232 | def _parse_languages(edition: Dict, locale: str) -> List[str]: 233 | language_iso = (edition.get("language") or {}).get("code3","") 234 | languages = ( 235 | [get_language_name(locale, language_iso)] 236 | if language_iso 237 | else [] 238 | ) 239 | return languages -------------------------------------------------------------------------------- /root/app/calibre-web/cps/metadata_provider/ibdb.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web) 4 | # Copyright (C) 2021 OzzieIsaacs 5 | # 6 | # This program is free software: you can redistribute it and/or modify 7 | # it under the terms of the GNU General Public License as published by 8 | # the Free Software Foundation, either version 3 of the License, or 9 | # (at your option) any later version. 10 | # 11 | # This program is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU General Public License 17 | # along with this program. If not, see . 18 | 19 | # Google Books api document: https://developers.google.com/books/docs/v1/using 20 | from typing import Dict, List, Optional 21 | from urllib.parse import quote 22 | from datetime import datetime 23 | 24 | import requests 25 | 26 | from cps import logger 27 | from cps.isoLanguages import get_lang3, get_language_name 28 | from cps.services.Metadata import MetaRecord, MetaSourceInfo, Metadata 29 | 30 | log = logger.create() 31 | 32 | 33 | class IBDb(Metadata): 34 | __name__ = "IBDb" 35 | __id__ = "ibdb" 36 | DESCRIPTION = "Internet Book Database" 37 | META_URL = "https://ibdb.dev/" 38 | BOOK_URL = "https://ibdb.dev/book/" 39 | SEARCH_URL = "https://ibdb.dev/search?q=" 40 | 41 | def search( 42 | self, query: str, generic_cover: str = "", locale: str = "en" 43 | ) -> Optional[List[MetaRecord]]: 44 | val = list() 45 | if self.active: 46 | 47 | title_tokens = list(self.get_title_tokens(query, strip_joiners=False)) 48 | if title_tokens: 49 | tokens = [quote(t.encode("utf-8")) for t in title_tokens] 50 | query = "+".join(tokens) 51 | try: 52 | results = requests.get(IBDb.SEARCH_URL + query) 53 | results.raise_for_status() 54 | except Exception as e: 55 | log.warning(e) 56 | return [] 57 | for result in results.json().get("books", []): 58 | val.append( 59 | self._parse_search_result( 60 | result=result, generic_cover=generic_cover, locale=locale 61 | ) 62 | ) 63 | return val 64 | 65 | def _parse_search_result( 66 | self, result: Dict, generic_cover: str, locale: str 67 | ) -> MetaRecord: 68 | match = MetaRecord( 69 | id=result["id"], 70 | title=result["title"], 71 | authors= self._parse_authors(result=result), 72 | url=IBDb.BOOK_URL + result["id"], 73 | source=MetaSourceInfo( 74 | id=self.__id__, 75 | description=IBDb.DESCRIPTION, 76 | link=IBDb.META_URL, 77 | ), 78 | ) 79 | 80 | match.cover = self._parse_cover(result=result, generic_cover=generic_cover) 81 | match.description = result.get("synopsis", "") 82 | match.languages = self._parse_languages(result=result, locale=locale) 83 | match.publisher = result.get("publisher", "") 84 | try: 85 | datetime.strptime(result.get("publishedDate", ""), "%Y-%m-%d") 86 | match.publishedDate = result.get("publishedDate", "") 87 | except ValueError: 88 | match.publishedDate = "" 89 | match.rating = 0 90 | match.series, match.series_index = "", 1 91 | match.tags = [] 92 | 93 | match.identifiers = {"ibdb": match.id, "isbn": result.get("isbn13", "")} 94 | return match 95 | 96 | @staticmethod 97 | def _parse_authors(result: Dict) -> List[str]: 98 | if (result.get("authors")): 99 | return [author.get("name", "-no-name-") for author in result.get("authors", [])] 100 | return [] 101 | 102 | @staticmethod 103 | def _parse_cover(result: Dict, generic_cover: str) -> str: 104 | if result.get("image"): 105 | return result["image"]["url"] 106 | 107 | return generic_cover 108 | 109 | @staticmethod 110 | def _parse_languages(result: Dict, locale: str) -> List[str]: 111 | language_iso2 = result.get("language", "") 112 | languages = ( 113 | [get_language_name(locale, get_lang3(language_iso2))] 114 | if language_iso2 115 | else [] 116 | ) 117 | return languages 118 | -------------------------------------------------------------------------------- /root/app/calibre-web/cps/render_template.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web) 4 | # Copyright (C) 2018-2020 OzzieIsaacs 5 | # 6 | # This program is free software: you can redistribute it and/or modify 7 | # it under the terms of the GNU General Public License as published by 8 | # the Free Software Foundation, either version 3 of the License, or 9 | # (at your option) any later version. 10 | # 11 | # This program is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU General Public License 17 | # along with this program. If not, see . 18 | 19 | from flask import render_template, g, abort, request, flash 20 | from flask_babel import gettext as _ 21 | from werkzeug.local import LocalProxy 22 | from .cw_login import current_user 23 | from sqlalchemy.sql.expression import or_ 24 | 25 | from . import config, constants, logger, ub 26 | from .ub import User 27 | 28 | # CWA specific imports 29 | import requests 30 | from datetime import datetime 31 | import os.path 32 | 33 | import sys 34 | sys.path.insert(1, '/app/calibre-web-automated/scripts/') 35 | from cwa_db import CWA_DB 36 | 37 | 38 | log = logger.create() 39 | 40 | def get_sidebar_config(kwargs=None): 41 | kwargs = kwargs or [] 42 | simple = bool([e for e in ['kindle', 'tolino', "kobo", "bookeen"] 43 | if (e in request.headers.get('User-Agent', "").lower())]) 44 | if 'content' in kwargs: 45 | content = kwargs['content'] 46 | content = isinstance(content, (User, LocalProxy)) and not content.role_anonymous() 47 | else: 48 | content = 'conf' in kwargs 49 | sidebar = list() 50 | sidebar.append({"glyph": "glyphicon-book", "text": _('Books'), "link": 'web.index', "id": "new", 51 | "visibility": constants.SIDEBAR_RECENT, 'public': True, "page": "root", 52 | "show_text": _('Show recent books'), "config_show":False}) 53 | sidebar.append({"glyph": "glyphicon-fire", "text": _('Hot Books'), "link": 'web.books_list', "id": "hot", 54 | "visibility": constants.SIDEBAR_HOT, 'public': True, "page": "hot", 55 | "show_text": _('Show Hot Books'), "config_show": True}) 56 | if current_user.role_admin(): 57 | sidebar.append({"glyph": "glyphicon-download", "text": _('Downloaded Books'), "link": 'web.download_list', 58 | "id": "download", "visibility": constants.SIDEBAR_DOWNLOAD, 'public': (not current_user.is_anonymous), 59 | "page": "download", "show_text": _('Show Downloaded Books'), 60 | "config_show": content}) 61 | else: 62 | sidebar.append({"glyph": "glyphicon-download", "text": _('Downloaded Books'), "link": 'web.books_list', 63 | "id": "download", "visibility": constants.SIDEBAR_DOWNLOAD, 'public': (not current_user.is_anonymous), 64 | "page": "download", "show_text": _('Show Downloaded Books'), 65 | "config_show": content}) 66 | sidebar.append( 67 | {"glyph": "glyphicon-star", "text": _('Top Rated Books'), "link": 'web.books_list', "id": "rated", 68 | "visibility": constants.SIDEBAR_BEST_RATED, 'public': True, "page": "rated", 69 | "show_text": _('Show Top Rated Books'), "config_show": True}) 70 | sidebar.append({"glyph": "glyphicon-eye-open", "text": _('Read Books'), "link": 'web.books_list', "id": "read", 71 | "visibility": constants.SIDEBAR_READ_AND_UNREAD, 'public': (not current_user.is_anonymous), 72 | "page": "read", "show_text": _('Show Read and Unread'), "config_show": content}) 73 | sidebar.append( 74 | {"glyph": "glyphicon-eye-close", "text": _('Unread Books'), "link": 'web.books_list', "id": "unread", 75 | "visibility": constants.SIDEBAR_READ_AND_UNREAD, 'public': (not current_user.is_anonymous), "page": "unread", 76 | "show_text": _('Show unread'), "config_show": False}) 77 | sidebar.append({"glyph": "glyphicon-random", "text": _('Discover'), "link": 'web.books_list', "id": "rand", 78 | "visibility": constants.SIDEBAR_RANDOM, 'public': True, "page": "discover", 79 | "show_text": _('Show Random Books'), "config_show": True}) 80 | sidebar.append({"glyph": "glyphicon-inbox", "text": _('Categories'), "link": 'web.category_list', "id": "cat", 81 | "visibility": constants.SIDEBAR_CATEGORY, 'public': True, "page": "category", 82 | "show_text": _('Show Category Section'), "config_show": True}) 83 | sidebar.append({"glyph": "glyphicon-bookmark", "text": _('Series'), "link": 'web.series_list', "id": "serie", 84 | "visibility": constants.SIDEBAR_SERIES, 'public': True, "page": "series", 85 | "show_text": _('Show Series Section'), "config_show": True}) 86 | sidebar.append({"glyph": "glyphicon-user", "text": _('Authors'), "link": 'web.author_list', "id": "author", 87 | "visibility": constants.SIDEBAR_AUTHOR, 'public': True, "page": "author", 88 | "show_text": _('Show Author Section'), "config_show": True}) 89 | sidebar.append( 90 | {"glyph": "glyphicon-text-size", "text": _('Publishers'), "link": 'web.publisher_list', "id": "publisher", 91 | "visibility": constants.SIDEBAR_PUBLISHER, 'public': True, "page": "publisher", 92 | "show_text": _('Show Publisher Section'), "config_show":True}) 93 | sidebar.append({"glyph": "glyphicon-flag", "text": _('Languages'), "link": 'web.language_overview', "id": "lang", 94 | "visibility": constants.SIDEBAR_LANGUAGE, 'public': (current_user.filter_language() == 'all'), 95 | "page": "language", 96 | "show_text": _('Show Language Section'), "config_show": True}) 97 | sidebar.append({"glyph": "glyphicon-star-empty", "text": _('Ratings'), "link": 'web.ratings_list', "id": "rate", 98 | "visibility": constants.SIDEBAR_RATING, 'public': True, 99 | "page": "rating", "show_text": _('Show Ratings Section'), "config_show": True}) 100 | sidebar.append({"glyph": "glyphicon-file", "text": _('File formats'), "link": 'web.formats_list', "id": "format", 101 | "visibility": constants.SIDEBAR_FORMAT, 'public': True, 102 | "page": "format", "show_text": _('Show File Formats Section'), "config_show": True}) 103 | sidebar.append( 104 | {"glyph": "glyphicon-trash", "text": _('Archived Books'), "link": 'web.books_list', "id": "archived", 105 | "visibility": constants.SIDEBAR_ARCHIVED, 'public': (not current_user.is_anonymous), "page": "archived", 106 | "show_text": _('Show Archived Books'), "config_show": content}) 107 | if not simple: 108 | sidebar.append( 109 | {"glyph": "glyphicon-th-list", "text": _('Books List'), "link": 'web.books_table', "id": "list", 110 | "visibility": constants.SIDEBAR_LIST, 'public': (not current_user.is_anonymous), "page": "list", 111 | "show_text": _('Show Books List'), "config_show": content}) 112 | g.shelves_access = ub.session.query(ub.Shelf).filter( 113 | or_(ub.Shelf.is_public == 1, ub.Shelf.user_id == current_user.id)).order_by(ub.Shelf.name).all() 114 | 115 | return sidebar, simple 116 | 117 | # Checks if an update for CWA is available, returning True if yes 118 | def cwa_update_available() -> tuple[bool, str, str]: 119 | with open("/app/CWA_RELEASE", 'r') as f: 120 | current_version = f.read().strip() 121 | response = requests.get("https://api.github.com/repos/crocodilestick/calibre-web-automated/releases/latest") 122 | tag_name = response.json().get('tag_name', current_version) 123 | return (tag_name != current_version), current_version, tag_name 124 | 125 | # Gets the date the last cwa update notification was displayed 126 | def get_cwa_last_notification() -> str: 127 | current_date = datetime.now().strftime("%Y-%m-%d") 128 | if not os.path.isfile('/app/cwa_update_notice'): 129 | with open('/app/cwa_update_notice', 'w') as f: 130 | f.write(current_date) 131 | return "0001-01-01" 132 | else: 133 | with open('/app/cwa_update_notice', 'r') as f: 134 | last_notification = f.read() 135 | return last_notification 136 | 137 | # Displays a notification to the user that an update for CWA is available, no matter which page they're on 138 | # Currently set to only display once per calender day 139 | def cwa_update_notification() -> None: 140 | db = CWA_DB() 141 | if db.cwa_settings['cwa_update_notifications']: 142 | current_date = datetime.now().strftime("%Y-%m-%d") 143 | cwa_last_notification = get_cwa_last_notification() 144 | 145 | if cwa_last_notification == current_date: 146 | return 147 | 148 | update_available, current_version, tag_name = cwa_update_available() 149 | if update_available: 150 | message = f"⚡🚨 CWA UPDATE AVAILABLE! 🚨⚡ Current - {current_version} | Newest - {tag_name} | To update, just re-pull the image! This message will only display once per day |" 151 | flash(_(message), category="cwa_update") 152 | print(f"[cwa-update-notification-service] {message}", flush=True) 153 | 154 | with open('/app/cwa_update_notice', 'w') as f: 155 | f.write(current_date) 156 | return 157 | else: 158 | return 159 | 160 | 161 | # Returns the template for rendering and includes the instance name 162 | def render_title_template(*args, **kwargs): 163 | sidebar, simple = get_sidebar_config(kwargs) 164 | if current_user.role_admin(): 165 | try: 166 | cwa_update_notification() 167 | except Exception as e: 168 | print(f"[cwa-update-notification-service] The following error occurred when checking for available updates:\n{e}", flush=True) 169 | try: 170 | return render_template(instance=config.config_calibre_web_title, sidebar=sidebar, simple=simple, 171 | accept=config.config_upload_formats.split(','), 172 | *args, **kwargs) 173 | except PermissionError: 174 | log.error("No permission to access {} file.".format(args[0])) 175 | abort(403) 176 | -------------------------------------------------------------------------------- /root/app/calibre-web/cps/search_metadata.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web) 4 | # Copyright (C) 2021 OzzieIsaacs 5 | # 6 | # This program is free software: you can redistribute it and/or modify 7 | # it under the terms of the GNU General Public License as published by 8 | # the Free Software Foundation, either version 3 of the License, or 9 | # (at your option) any later version. 10 | # 11 | # This program is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU General Public License 17 | # along with this program. If not, see . 18 | 19 | import concurrent.futures 20 | import importlib 21 | import inspect 22 | import json 23 | import os 24 | import sys 25 | 26 | from flask import Blueprint, request, url_for, make_response, jsonify, copy_current_request_context 27 | from .cw_login import current_user 28 | from flask_babel import get_locale 29 | from sqlalchemy.exc import InvalidRequestError, OperationalError 30 | from sqlalchemy.orm.attributes import flag_modified 31 | 32 | from cps.services.Metadata import Metadata 33 | from . import constants, logger, ub, web_server 34 | from .usermanagement import user_login_required 35 | 36 | 37 | meta = Blueprint("metadata", __name__) 38 | 39 | log = logger.create() 40 | 41 | try: 42 | from dataclasses import asdict 43 | except ImportError: 44 | log.info('*** "dataclasses" is needed for calibre-web to run. Please install it using pip: "pip install dataclasses" ***') 45 | print('*** "dataclasses" is needed for calibre-web to run. Please install it using pip: "pip install dataclasses" ***') 46 | web_server.stop(True) 47 | sys.exit(6) 48 | 49 | new_list = list() 50 | meta_dir = os.path.join(constants.BASE_DIR, "cps", "metadata_provider") 51 | modules = os.listdir(os.path.join(constants.BASE_DIR, "cps", "metadata_provider")) 52 | for f in modules: 53 | if os.path.isfile(os.path.join(meta_dir, f)) and not f.endswith("__init__.py"): 54 | a = os.path.basename(f)[:-3] 55 | try: 56 | importlib.import_module("cps.metadata_provider." + a) 57 | new_list.append(a) 58 | except (IndentationError, SyntaxError) as e: 59 | log.error("Syntax error for metadata source: {} - {}".format(a, e)) 60 | except ImportError as e: 61 | log.debug("Import error for metadata source: {} - {}".format(a, e)) 62 | 63 | 64 | def list_classes(provider_list): 65 | classes = list() 66 | for element in provider_list: 67 | for name, obj in inspect.getmembers( 68 | sys.modules["cps.metadata_provider." + element] 69 | ): 70 | if ( 71 | inspect.isclass(obj) 72 | and name != "Metadata" 73 | and issubclass(obj, Metadata) 74 | ): 75 | classes.append(obj()) 76 | return classes 77 | 78 | 79 | cl = list_classes(new_list) 80 | 81 | 82 | @meta.route("/metadata/provider") 83 | @user_login_required 84 | def metadata_provider(): 85 | active = current_user.view_settings.get("metadata", {}) 86 | provider = list() 87 | for c in cl: 88 | ac = active.get(c.__id__, True) 89 | provider.append( 90 | {"name": c.__name__, "active": ac, "initial": ac, "id": c.__id__} 91 | ) 92 | return make_response(jsonify(provider)) 93 | 94 | 95 | @meta.route("/metadata/provider", methods=["POST"]) 96 | @meta.route("/metadata/provider/", methods=["POST"]) 97 | @user_login_required 98 | def metadata_change_active_provider(prov_name): 99 | new_state = request.get_json() 100 | active = current_user.view_settings.get("metadata", {}) 101 | active[new_state["id"]] = new_state["value"] 102 | current_user.view_settings["metadata"] = active 103 | try: 104 | try: 105 | flag_modified(current_user, "view_settings") 106 | except AttributeError: 107 | pass 108 | ub.session.commit() 109 | except (InvalidRequestError, OperationalError): 110 | log.error("Invalid request received: {}".format(request)) 111 | return "Invalid request", 400 112 | if "initial" in new_state and prov_name: 113 | data = [] 114 | provider = next((c for c in cl if c.__id__ == prov_name), None) 115 | if provider is not None: 116 | data = provider.search(new_state.get("query", "")) 117 | return make_response(jsonify([asdict(x) for x in data])) 118 | return "" 119 | 120 | 121 | @meta.route("/metadata/search", methods=["POST"]) 122 | @user_login_required 123 | def metadata_search(): 124 | query = request.form.to_dict().get("query") 125 | data = list() 126 | active = current_user.view_settings.get("metadata", {}) 127 | locale = get_locale() 128 | if query: 129 | static_cover = url_for("static", filename="generic_cover.jpg") 130 | # ret = cl[0].search(query, static_cover, locale) 131 | with concurrent.futures.ThreadPoolExecutor(max_workers=5) as executor: 132 | meta = { 133 | executor.submit(copy_current_request_context(c.search), query, static_cover, locale): c 134 | for c in cl 135 | if active.get(c.__id__, True) 136 | } 137 | for future in concurrent.futures.as_completed(meta): 138 | data.extend([asdict(x) for x in future.result() if x]) 139 | return make_response(jsonify(data)) 140 | -------------------------------------------------------------------------------- /root/app/calibre-web/cps/services/Metadata.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web) 4 | # Copyright (C) 2021 OzzieIsaacs 5 | # 6 | # This program is free software: you can redistribute it and/or modify 7 | # it under the terms of the GNU General Public License as published by 8 | # the Free Software Foundation, either version 3 of the License, or 9 | # (at your option) any later version. 10 | # 11 | # This program is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU General Public License 17 | # along with this program. If not, see . 18 | import abc 19 | import dataclasses 20 | import os 21 | import re 22 | from typing import Dict, Generator, List, Optional, Union 23 | 24 | from cps import constants 25 | 26 | 27 | @dataclasses.dataclass 28 | class MetaSourceInfo: 29 | id: str 30 | description: str 31 | link: str 32 | 33 | 34 | @dataclasses.dataclass 35 | class MetaRecord: 36 | id: Union[str, int] 37 | title: str 38 | authors: List[str] 39 | url: str 40 | source: MetaSourceInfo 41 | cover: str = os.path.join(constants.STATIC_DIR, 'generic_cover.jpg') 42 | description: Optional[str] = "" 43 | series: Optional[str] = None 44 | series_index: Optional[Union[int, float]] = 0 45 | identifiers: Dict[str, Union[str, int]] = dataclasses.field(default_factory=dict) 46 | publisher: Optional[str] = None 47 | publishedDate: Optional[str] = None 48 | rating: Optional[int] = 0 49 | languages: Optional[List[str]] = dataclasses.field(default_factory=list) 50 | tags: Optional[List[str]] = dataclasses.field(default_factory=list) 51 | format: Optional[str] = None 52 | 53 | 54 | class Metadata: 55 | __name__ = "Generic" 56 | __id__ = "generic" 57 | 58 | def __init__(self): 59 | self.active = True 60 | 61 | def set_status(self, state): 62 | self.active = state 63 | 64 | @abc.abstractmethod 65 | def search( 66 | self, query: str, generic_cover: str = "", locale: str = "en" 67 | ) -> Optional[List[MetaRecord]]: 68 | pass 69 | 70 | @staticmethod 71 | def get_title_tokens( 72 | title: str, strip_joiners: bool = True 73 | ) -> Generator[str, None, None]: 74 | """ 75 | Taken from calibre source code 76 | It's a simplified (cut out what is unnecessary) version of 77 | https://github.com/kovidgoyal/calibre/blob/99d85b97918625d172227c8ffb7e0c71794966c0/ 78 | src/calibre/ebooks/metadata/sources/base.py#L363-L367 79 | (src/calibre/ebooks/metadata/sources/base.py - lines 363-398) 80 | """ 81 | title_patterns = [ 82 | (re.compile(pat, re.IGNORECASE), repl) 83 | for pat, repl in [ 84 | # Remove things like: (2010) (Omnibus) etc. 85 | ( 86 | r"(?i)[({\[](\d{4}|omnibus|anthology|hardcover|" 87 | r"audiobook|audio\scd|paperback|turtleback|" 88 | r"mass\s*market|edition|ed\.)[\])}]", 89 | "", 90 | ), 91 | # Remove any strings that contain the substring edition inside 92 | # parentheses 93 | (r"(?i)[({\[].*?(edition|ed.).*?[\]})]", ""), 94 | # Remove commas used a separators in numbers 95 | (r"(\d+),(\d+)", r"\1\2"), 96 | # Remove hyphens only if they have whitespace before them 97 | (r"(\s-)", " "), 98 | # Replace other special chars with a space 99 | (r"""[:,;!@$%^&*(){}.`~"\s\[\]/]《》「」“”""", " "), 100 | ] 101 | ] 102 | 103 | for pat, repl in title_patterns: 104 | title = pat.sub(repl, title) 105 | 106 | tokens = title.split() 107 | for token in tokens: 108 | token = token.strip().strip('"').strip("'") 109 | if token and ( 110 | not strip_joiners or token.lower() not in ("a", "and", "the", "&") 111 | ): 112 | yield token 113 | -------------------------------------------------------------------------------- /root/app/calibre-web/cps/services/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web) 4 | # Copyright (C) 2019 pwr 5 | # 6 | # This program is free software: you can redistribute it and/or modify 7 | # it under the terms of the GNU General Public License as published by 8 | # the Free Software Foundation, either version 3 of the License, or 9 | # (at your option) any later version. 10 | # 11 | # This program is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU General Public License 17 | # along with this program. If not, see . 18 | 19 | from .. import logger 20 | 21 | log = logger.create() 22 | 23 | try: 24 | from . import goodreads_support 25 | except ImportError as err: 26 | log.debug("Cannot import goodreads, showing authors-metadata will not work: %s", err) 27 | goodreads_support = None 28 | 29 | 30 | try: 31 | from . import simpleldap as ldap 32 | from .simpleldap import ldapVersion 33 | except ImportError as err: 34 | log.debug("Cannot import simpleldap, logging in with ldap will not work: %s", err) 35 | ldap = None 36 | ldapVersion = None 37 | 38 | try: 39 | from . import SyncToken as SyncToken 40 | kobo = True 41 | except ImportError as err: 42 | log.debug("Cannot import SyncToken, syncing books with Kobo Devices will not work: %s", err) 43 | kobo = None 44 | SyncToken = None 45 | 46 | try: 47 | from . import gmail 48 | except ImportError as err: 49 | log.debug("Cannot import gmail, sending books via Gmail Oauth2 Verification will not work: %s", err) 50 | gmail = None 51 | 52 | try: 53 | from . import hardcover 54 | except ImportError as err: 55 | log.debug("Cannot import hardcover, syncing Kobo read progress to Hardcover will not work: %s", err) 56 | hardcover = None -------------------------------------------------------------------------------- /root/app/calibre-web/cps/services/hardcover.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web) 4 | # Copyright (C) 2018-2019 OzzieIsaacs, pwr 5 | # 6 | # This program is free software: you can redistribute it and/or modify 7 | # it under the terms of the GNU General Public License as published by 8 | # the Free Software Foundation, either version 3 of the License, or 9 | # (at your option) any later version. 10 | # 11 | # This program is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU General Public License 17 | # along with this program. If not, see . 18 | 19 | from datetime import datetime 20 | import requests 21 | 22 | from .. import logger 23 | 24 | log = logger.create() 25 | 26 | GRAPHQL_ENDPOINT = "https://api.hardcover.app/v1/graphql" 27 | 28 | USER_BOOK_FRAGMENT = """ 29 | fragment userBookFragment on user_books { 30 | id 31 | status_id 32 | book_id 33 | book { 34 | slug 35 | title 36 | } 37 | edition { 38 | id 39 | pages 40 | } 41 | user_book_reads(order_by: {started_at: desc}, where: {finished_at: {_is_null: true}}) { 42 | id 43 | started_at 44 | finished_at 45 | edition_id 46 | progress_pages 47 | } 48 | }""" 49 | 50 | class HardcoverClient: 51 | def __init__(self, token): 52 | self.endpoint = GRAPHQL_ENDPOINT 53 | self.headers = { 54 | "Content-Type": "application/json", 55 | "Authorization" : f"Bearer {token}" 56 | } 57 | self.privacy = self.get_privacy() 58 | 59 | def get_privacy(self): 60 | query = """ 61 | { 62 | me { 63 | account_privacy_setting_id 64 | } 65 | }""" 66 | response = self.execute(query) 67 | return (response.get("me")[0] or [{}]).get("account_privacy_setting_id",1) 68 | 69 | def get_user_book(self, ids): 70 | query = "" 71 | variables={} 72 | if "hardcover-edition" in ids: 73 | query = """ 74 | query ($query: Int) { 75 | me { 76 | user_books(where: {edition_id: {_eq: $query}}) { 77 | ...userBookFragment 78 | } 79 | } 80 | }""" 81 | variables["query"] = ids["hardcover-edition"] 82 | elif "hardcover-id" in ids: 83 | query = """ 84 | query ($query: Int) { 85 | me { 86 | user_books(where: {book: {id: {_eq: $query}}}) { 87 | ...userBookFragment 88 | } 89 | } 90 | }""" 91 | variables["query"] = ids["hardcover-id"] 92 | elif "hardcover-slug" in ids: 93 | query = """ 94 | query ($slug: String!) { 95 | me { 96 | user_books(where: {book: {slug: {_eq: $query}}}) { 97 | ...userBookFragment 98 | } 99 | } 100 | }""" 101 | variables["query"] = ids["hardcover-slug"] 102 | query += USER_BOOK_FRAGMENT 103 | response = self.execute(query,variables) 104 | return next(iter(response.get("me")[0].get("user_books")),None) 105 | 106 | 107 | # TODO Add option for autocreate if missing books instead of forcing it. 108 | def update_reading_progress(self, identifiers, progress_percent): 109 | ids = self.parse_identifiers(identifiers) 110 | book = self.get_user_book(ids) 111 | # Book doesn't exist, add it in Reading status 112 | if not book: 113 | book = self.add_book(ids, status=2) 114 | # Book is either WTR or Read, and we aren't finished reading 115 | if book.get("status_id") != 2 and progress_percent != 100: 116 | book = self.change_book_status(book, 2) 117 | # Book is already marked as read, and we are also done 118 | if book.get("status_id") == 3 and progress_percent == 100: 119 | return 120 | pages = book.get("edition",{}).get("pages",0) 121 | if pages: 122 | pages_read = round(pages * (progress_percent / 100)) 123 | read = next(iter(book.get("user_book_reads")),None) 124 | if not read: 125 | # read = self.add_read(book, pages_read) 126 | # No read exists for some reason, return since we can't update anything. 127 | return 128 | else: 129 | mutation = """ 130 | mutation ($readId: Int!, $pages: Int, $editionId: Int, $startedAt: date, $finishedAt: date) { 131 | update_user_book_read(id: $readId, object: { 132 | progress_pages: $pages, 133 | edition_id: $editionId, 134 | started_at: $startedAt, 135 | finished_at: $finishedAt 136 | }) { 137 | id 138 | } 139 | }""" 140 | variables = { 141 | "readId": int(read.get("id")), 142 | "pages": pages_read, 143 | "editionId": int(book.get("edition").get("id")), 144 | "startedAt":read.get("started_at",datetime.now().strftime("%Y-%m-%d")), 145 | "finishedAt": datetime.now().strftime("%Y-%m-%d") if progress_percent == 100 else None 146 | } 147 | if progress_percent == 100: 148 | self.change_book_status(book, 3) 149 | self.execute(query=mutation, variables=variables) 150 | return 151 | 152 | def change_book_status(self, book, status): 153 | mutation = """ 154 | mutation ($id:Int!, $status_id: Int!) { 155 | update_user_book(id: $id, object: {status_id: $status_id}) { 156 | error 157 | user_book { 158 | ...userBookFragment 159 | } 160 | } 161 | }""" + USER_BOOK_FRAGMENT 162 | variables = { 163 | "id":book.get("id"), 164 | "status_id":status 165 | } 166 | response = self.execute(query=mutation, variables=variables) 167 | return response.get("update_user_book",{}).get("user_book",{}) 168 | 169 | def add_book(self, identifiers, status=1): 170 | ids = self.parse_identifiers(identifiers) 171 | mutation = """ 172 | mutation ($object: UserBookCreateInput!) { 173 | insert_user_book(object: $object) { 174 | error 175 | user_book { 176 | ...userBookFragment 177 | } 178 | } 179 | }""" + USER_BOOK_FRAGMENT 180 | variables = { 181 | "object": { 182 | "book_id":int(ids.get("hardcover-id")), 183 | "edition_id":int(ids.get("hardcover-edition")) if ids.get("hardcover-edition") else None, 184 | "status_id": status, 185 | "privacy_setting_id": self.privacy 186 | } 187 | } 188 | response = self.execute(query=mutation, variables=variables) 189 | return response.get("insert_user_book",{}).get("user_book",{}) 190 | 191 | def add_read(self, book, pages=0): 192 | mutation = """ 193 | mutation ($id: Int!, $pages: Int, $editionId: Int, $startedAt: date) { 194 | insert_user_book_read(user_book_id: $id, user_book_read: { 195 | progress_pages: $pages, 196 | edition_id: $editionId, 197 | started_at: $startedAt, 198 | }) { 199 | error 200 | user_book_read { 201 | id 202 | started_at 203 | finished_at 204 | edition_id 205 | progress_pages 206 | } 207 | } 208 | }""" 209 | variables = { 210 | "id":int(book.get("id")), 211 | "editionId":int(book.get("edition").get("id")) if book.get("edition").get("id") else None, 212 | "pages": pages, 213 | "startedAt": datetime.now().strftime("%Y-%m-%d") 214 | } 215 | response = self.execute(query=mutation, variables=variables) 216 | return response.get("insert_user_book_read").get("user_book_read") 217 | 218 | def parse_identifiers(self, identifiers): 219 | if type(identifiers) != dict: 220 | return {id.type:id.val for id in identifiers if "hardcover" in id.type} 221 | return identifiers 222 | 223 | def execute(self, query, variables=None): 224 | payload = { 225 | "query": query, 226 | "variables": variables or {} 227 | } 228 | response = requests.post(self.endpoint, json=payload, headers=self.headers) 229 | try: 230 | response.raise_for_status() 231 | except requests.exceptions.HTTPError as e: 232 | raise Exception(f"HTTP error occurred: {e}") 233 | result = response.json() 234 | if "errors" in result: 235 | raise Exception(f"GraphQL error: {result['errors']}") 236 | return result.get("data", {}) 237 | -------------------------------------------------------------------------------- /root/app/calibre-web/cps/static/css/caliBlur_cwa.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crocodilestick/Calibre-Web-Automated/3746332a6d04e518f194551caa6363851e0e0e5d/root/app/calibre-web/cps/static/css/caliBlur_cwa.css -------------------------------------------------------------------------------- /root/app/calibre-web/cps/static/css/style.css: -------------------------------------------------------------------------------- 1 | .tooltip.bottom .tooltip-inner { 2 | font-size: 13px; 3 | font-family: Open Sans Semibold, Helvetica Neue, Helvetica, Arial, sans-serif; 4 | -webkit-font-smoothing: antialiased; 5 | -moz-osx-font-smoothing: grayscale; 6 | padding: 3px 10px; 7 | border-radius: 4px; 8 | background-color: #fff; 9 | -webkit-box-shadow: 0 4px 10px 0 rgba(0, 0, 0, 0.35); 10 | box-shadow: 0 4px 10px 0 rgba(0, 0, 0, 0.35); 11 | opacity: 1; 12 | white-space: nowrap; 13 | margin-top: -16px !important; 14 | line-height: 1.71428571; 15 | color: #ddd; 16 | } 17 | 18 | @font-face { 19 | font-family: "Grand Hotel"; 20 | font-style: normal; 21 | font-weight: 400; 22 | src: local("Grand Hotel"), local("GrandHotel-Regular"), 23 | url("fonts/GrandHotel-Regular.ttf") format("truetype"); 24 | } 25 | 26 | html.http-error { 27 | margin: 0; 28 | height: 100%; 29 | } 30 | 31 | body { 32 | background: #f2f2f2; 33 | margin-bottom: 40px; 34 | } 35 | 36 | .http-error body { 37 | margin: 0; 38 | height: 100%; 39 | display: table; 40 | width: 100%; 41 | } 42 | 43 | .http-error body > div { 44 | display: table-cell; 45 | vertical-align: middle; 46 | text-align: center; 47 | } 48 | 49 | body h2 { 50 | font-weight: normal; 51 | color: #444; 52 | } 53 | 54 | a, 55 | .danger, 56 | .book-remove, 57 | .editable-empty, 58 | .editable-empty:hover { 59 | color: #45b29d; 60 | } 61 | .book-remove:hover { 62 | color: #23527c; 63 | } 64 | .user-remove:hover { 65 | color: #23527c; 66 | } 67 | .btn-default a { 68 | color: #444; 69 | } 70 | .panel-title > a { 71 | text-decoration: none; 72 | } 73 | 74 | .navigation li a { 75 | color: #444; 76 | text-decoration: none; 77 | display: block; 78 | padding: 10px; 79 | } 80 | 81 | .btn-default a:hover { 82 | color: #45b29d; 83 | text-decoration: None; 84 | } 85 | 86 | .btn-default:hover { 87 | color: #45b29d; 88 | } 89 | 90 | .editable-click, 91 | a.editable-click, 92 | a.editable-click:hover { 93 | border-bottom: None; 94 | } 95 | 96 | .navigation .nav-head { 97 | text-transform: uppercase; 98 | color: #999; 99 | margin: 20px 0; 100 | } 101 | 102 | .navigation .nav-head:nth-child(1n + 2) { 103 | border-top: 1px solid #ccc; 104 | padding-top: 20px; 105 | } 106 | 107 | .book-meta .tags a { 108 | display: inline; 109 | } 110 | table .bg-primary a { 111 | color: #fff; 112 | } 113 | table .bg-dark-danger a { 114 | color: #fff; 115 | } 116 | .book-meta .identifiers a { 117 | display: inline; 118 | } 119 | 120 | .navigation .create-shelf a { 121 | color: #fff; 122 | background: #45b29d; 123 | padding: 10px 20px; 124 | border-radius: 5px; 125 | text-decoration: none; 126 | } 127 | 128 | .navigation li a:hover { 129 | background: rgba(153, 153, 153, 0.4); 130 | border-radius: 5px; 131 | } 132 | 133 | .navigation li a span { 134 | margin-right: 10px; 135 | } 136 | 137 | .navigation .create-shelf { 138 | margin: 30px 0; 139 | font-size: 12px; 140 | text-align: center; 141 | } 142 | 143 | .row.display-flex { 144 | display: flex; 145 | flex-wrap: wrap; 146 | } 147 | 148 | .row-fluid.text-center { 149 | margin-top: -20px; 150 | } 151 | 152 | .container-fluid img { 153 | display: block; 154 | max-width: 100%; 155 | height: auto; 156 | max-height: 100%; 157 | } 158 | 159 | .container-fluid .discover { 160 | margin-bottom: 50px; 161 | } 162 | .container-fluid .new-books { 163 | border-top: 1px solid #ccc; 164 | } 165 | .container-fluid .new-books h2 { 166 | margin: 50px 0 0 0; 167 | } 168 | 169 | .container-fluid .book { 170 | margin-top: 20px; 171 | max-width: 180px; 172 | display: flex; 173 | flex-direction: column; 174 | } 175 | .cover { 176 | margin-bottom: 10px; 177 | } 178 | 179 | .container-fluid .book .cover { 180 | height: 225px; 181 | position: relative; 182 | } 183 | 184 | .author-link img { 185 | display: block; 186 | height: 100%; 187 | } 188 | 189 | .author-bio img { 190 | margin: 0 1em 1em 0; 191 | } 192 | 193 | .container-fluid .single .cover img { 194 | border: 1px solid #fff; 195 | box-sizing: border-box; 196 | -webkit-box-shadow: 0 5px 8px -6px #777; 197 | -moz-box-shadow: 0 5px 8px -6px #777; 198 | box-shadow: 0 5px 8px -6px #777; 199 | } 200 | 201 | .datepicker.form-control { 202 | position: static; 203 | } 204 | 205 | .container-fluid .book .cover span .img { 206 | bottom: 0; 207 | height: 100%; 208 | position: absolute; 209 | } 210 | 211 | .container-fluid .book .cover span img { 212 | border: 1px solid #fff; 213 | position: relative; 214 | height: 100%; 215 | 216 | box-sizing: border-box; 217 | -webkit-box-shadow: 0 5px 8px -6px #777; 218 | -moz-box-shadow: 0 5px 8px -6px #777; 219 | box-shadow: 0 5px 8px -6px #777; 220 | } 221 | 222 | .container-fluid .book .meta { 223 | margin-top: 10px; 224 | } 225 | .media-body p { 226 | text-align: justify; 227 | } 228 | .container-fluid .book .meta p { 229 | margin: 0; 230 | } 231 | 232 | .container-fluid .book .meta .title { 233 | font-weight: bold; 234 | font-size: 15px; 235 | color: #444; 236 | } 237 | 238 | .container-fluid .book .meta .series { 239 | font-weight: 400; 240 | font-size: 12px; 241 | color: #444; 242 | } 243 | 244 | .container-fluid .book .meta .author { 245 | font-size: 12px; 246 | color: #999; 247 | } 248 | 249 | .container-fluid .book .meta .rating { 250 | margin-top: 5px; 251 | } 252 | .rating .glyphicon-star-empty { 253 | color: #444; 254 | } 255 | .rating .glyphicon-star.good { 256 | color: #444; 257 | } 258 | .rating-clear .glyphicon-remove { 259 | color: #333; 260 | } 261 | 262 | .container-fluid .author .author-hidden, 263 | .container-fluid .author .author-hidden-divider { 264 | display: none; 265 | } 266 | 267 | .navbar-brand { 268 | font-family: "Grand Hotel", cursive; 269 | font-size: 35px; 270 | color: #45b29d !important; 271 | } 272 | 273 | .more-stuff { 274 | margin-top: 20px; 275 | padding-top: 20px; 276 | border-top: 1px solid #ccc; 277 | } 278 | 279 | .more-stuff > li { 280 | margin-bottom: 10px; 281 | } 282 | .navbar-collapse.in .navbar-nav { 283 | margin: 0; 284 | } 285 | 286 | span.glyphicon.glyphicon-tags { 287 | padding-right: 5px; 288 | color: #999; 289 | vertical-align: text-top; 290 | } 291 | 292 | .book-meta { 293 | padding-bottom: 20px; 294 | } 295 | 296 | .navbar-default .navbar-toggle .icon-bar { 297 | background-color: #000; 298 | } 299 | .navbar-default .navbar-toggle { 300 | border-color: #000; 301 | } 302 | 303 | .cover .badge { 304 | position: absolute; 305 | top: 2px; 306 | left: 2px; 307 | color: #000; 308 | border-radius: 10px; 309 | background-color: #fff; 310 | } 311 | 312 | .cover .read { 313 | position: relative; 314 | top: -20px; 315 | /*left: auto; 316 | right: 2px;*/ 317 | width: 17px; 318 | height: 17px; 319 | display: inline-block; 320 | padding: 2px; 321 | } 322 | .cover-height { 323 | max-height: 100px; 324 | } 325 | 326 | .col-sm-2 a .cover-small { 327 | margin: 5px; 328 | max-height: 200px; 329 | } 330 | 331 | .btn-file { 332 | position: relative; 333 | overflow: hidden; 334 | } 335 | 336 | .btn-file input[type="file"] { 337 | position: absolute; 338 | top: 0; 339 | right: 0; 340 | min-width: 100%; 341 | min-height: 100%; 342 | font-size: 100px; 343 | text-align: right; 344 | filter: alpha(opacity=0); 345 | opacity: 0; 346 | outline: none; 347 | background: white; 348 | cursor: inherit; 349 | display: block; 350 | } 351 | 352 | .btn-toolbar .btn, 353 | .discover .btn { 354 | margin-bottom: 5px; 355 | } 356 | .button-link { 357 | color: #fff; 358 | } 359 | 360 | .btn-primary:hover, 361 | .btn-primary:focus, 362 | .btn-primary:active, 363 | .btn-primary.active, 364 | .open .dropdown-toggle.btn-primary { 365 | background-color: #1c5484; 366 | } 367 | 368 | .btn-primary.disabled, 369 | .btn-primary[disabled], 370 | fieldset[disabled] .btn-primary, 371 | .btn-primary.disabled:hover, 372 | .btn-primary[disabled]:hover, 373 | fieldset[disabled] .btn-primary:hover, 374 | .btn-primary.disabled:focus, 375 | .btn-primary[disabled]:focus, 376 | fieldset[disabled] .btn-primary:focus, 377 | .btn-primary.disabled:active, 378 | .btn-primary[disabled]:active, 379 | fieldset[disabled] .btn-primary:active, 380 | .btn-primary.disabled.active, 381 | .btn-primary[disabled].active, 382 | fieldset[disabled] .btn-primary.active { 383 | background-color: #89b9e2; 384 | } 385 | 386 | .btn-toolbar > .btn + .btn, 387 | .btn-toolbar > .btn-group + .btn, 388 | .btn-toolbar > .btn + .btn-group, 389 | .btn-toolbar > .btn-group + .btn-group { 390 | margin-left: 0; 391 | } 392 | 393 | .panel-body { 394 | background-color: #f5f5f5; 395 | } 396 | .spinner { 397 | margin: 0 41%; 398 | } 399 | .spinner2 { 400 | margin: 0 41%; 401 | } 402 | .intend-form { 403 | margin-left: 20px; 404 | } 405 | 406 | table .bg-dark-danger { 407 | background-color: #d9534f; 408 | color: #fff; 409 | } 410 | table .bg-dark-danger:hover { 411 | background-color: #c9302c; 412 | } 413 | table .bg-primary:hover { 414 | background-color: #1c5484; 415 | } 416 | .block-label { 417 | display: block; 418 | } 419 | 420 | .form-control.fake-input { 421 | position: absolute; 422 | pointer-events: none; 423 | top: 0; 424 | } 425 | 426 | input.pill { 427 | position: absolute; 428 | opacity: 0; 429 | } 430 | 431 | input.pill + label { 432 | border: 2px solid #45b29d; 433 | border-radius: 15px; 434 | color: #45b29d; 435 | cursor: pointer; 436 | display: inline-block; 437 | padding: 3px 15px; 438 | user-select: none; 439 | -webkit-font-smoothing: antialiased; 440 | -moz-osx-font-smoothing: grayscale; 441 | } 442 | 443 | input.pill:checked + label { 444 | background-color: #45b29d; 445 | border-color: #fff; 446 | color: #fff; 447 | } 448 | 449 | input.pill:not(:checked) + label .glyphicon { 450 | display: none; 451 | } 452 | 453 | .author-link { 454 | display: inline-block; 455 | margin-top: 10px; 456 | width: 100px; 457 | } 458 | 459 | #remove-from-shelves .btn, 460 | #shelf-action-errors { 461 | margin-left: 5px; 462 | } 463 | 464 | .tags_click, 465 | .serie_click, 466 | .language_click { 467 | margin-right: 5px; 468 | } 469 | 470 | #meta-info { 471 | height: 600px; 472 | overflow-y: scroll; 473 | } 474 | 475 | #meta-info #book-list { 476 | list-style-type: none; 477 | padding: 0px 15px 0px 0px; 478 | } 479 | 480 | #meta-info #book-list .media { 481 | display: flex; 482 | background-color: #f9f9f9; 483 | margin-bottom: 20px; 484 | border-radius: 8px; 485 | overflow: hidden; 486 | box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1); 487 | } 488 | 489 | #meta-info #book-list .media .media-image { 490 | width: 200px; 491 | padding: 20px; 492 | display: flex; 493 | flex-direction: column; 494 | align-items: center; 495 | position: relative; 496 | } 497 | 498 | #meta-info #book-list .media .media-image-wrapper { 499 | position: relative; 500 | width: 100%; 501 | } 502 | 503 | #meta-info #book-list .media .media-image img { 504 | max-width: 100%; 505 | height: auto; 506 | border-radius: 4px; 507 | display: block; 508 | } 509 | #meta-info #book-list .media .media-image .image-dimensions { 510 | position: absolute; 511 | bottom: 5px; 512 | right: 5px; 513 | background-color: rgba(0, 0, 0, 0.7); 514 | color: white; 515 | padding: 4px 8px; 516 | border-radius: 4px; 517 | font-size: 12px; 518 | } 519 | #meta-info #book-list .media .media-image .image-dimensions.larger { 520 | background-color: rgba(0, 160, 0, 0.7); 521 | } 522 | #meta-info #book-list .media .media-image .image-dimensions.smaller { 523 | background-color: rgba(160, 0, 0, 0.7); 524 | } 525 | #meta-info #book-list .media .media-image input { 526 | position: absolute; 527 | top: 5px; 528 | right: 5px; 529 | width: 20px; 530 | height: 20px; 531 | } 532 | 533 | #meta-info #book-list .media .media-image button { 534 | margin-top: 10px; 535 | /* padding: 8px 16px; */ 536 | cursor: pointer; 537 | } 538 | 539 | #meta-info #book-list .media .media-image div { 540 | margin-top: 10px; 541 | font-size: 0.9em; 542 | color: #666; 543 | text-align: center; 544 | } 545 | 546 | #meta-info #book-list .media .media-body { 547 | flex: 1; 548 | padding: 20px; 549 | display: grid; 550 | grid-template-columns: auto 1fr; 551 | gap: 10px; 552 | align-content: start; 553 | } 554 | 555 | #meta-info #book-list .media .media-body dt { 556 | font-weight: bold; 557 | display: flex; 558 | align-items: center; 559 | } 560 | 561 | #meta-info #book-list .media .media-body dt input { 562 | margin-right: 5px; 563 | margin-top: 0px; 564 | } 565 | 566 | #meta-info #book-list .media .media-body dd { 567 | margin: 0; 568 | color: #666; 569 | } 570 | 571 | .padded-bottom { 572 | margin-bottom: 15px; 573 | } 574 | .upload-format-input-text { 575 | display: initial; 576 | } 577 | #btn-upload-format { 578 | display: none; 579 | } 580 | .upload-cover-input-text { 581 | display: initial; 582 | } 583 | #btn-upload-cover { 584 | display: none; 585 | } 586 | 587 | .editable-buttons { 588 | display: inline-block; 589 | margin-left: 7px; 590 | } 591 | 592 | .editable-input { 593 | display: inline-block; 594 | } 595 | 596 | .editable-cancel { 597 | margin-bottom: 0 !important; 598 | margin-left: 7px !important; 599 | } 600 | 601 | .editable-submit { 602 | margin-bottom: 0 !important; 603 | } 604 | .filterheader { 605 | margin-bottom: 20px; 606 | } 607 | .errorlink { 608 | margin-top: 20px; 609 | } 610 | .emailconfig { 611 | margin-top: 10px; 612 | } 613 | 614 | .modal-body .comments { 615 | max-height: 300px; 616 | overflow-y: auto; 617 | } 618 | 619 | div.log { 620 | font-family: Courier New, serif; 621 | font-size: 12px; 622 | box-sizing: border-box; 623 | height: 700px; 624 | overflow-y: scroll; 625 | border: 1px solid #ddd; 626 | white-space: nowrap; 627 | padding: 0.5em; 628 | } 629 | 630 | #detailcover { 631 | cursor: zoom-in; 632 | } 633 | #detailcover:-webkit-full-screen { 634 | cursor: zoom-out; 635 | border: 0; 636 | } 637 | #detailcover:-moz-full-screen { 638 | cursor: zoom-out; 639 | border: 0; 640 | } 641 | #detailcover:-ms-fullscreen { 642 | cursor: zoom-out; 643 | border: 0; 644 | } 645 | #detailcover:fullscreen { 646 | cursor: zoom-out; 647 | border: 0; 648 | } 649 | 650 | .error-list { 651 | margin-top: 5px; 652 | } 653 | 654 | :root { 655 | --color-secondary: #45b29d; 656 | } 657 | 658 | p.cwa-settings-tooltip { 659 | margin: 1px 4px 16px 32px; 660 | padding: 5px 10px; 661 | line-height: normal; 662 | color: #444444; 663 | max-width: 100rem; 664 | } 665 | 666 | p.cwa-settings-explanation { 667 | color: #444444; 668 | font-style: italic; 669 | font-size: inherit; 670 | line-height: normal; 671 | padding-left: 10px; 672 | max-width: 90rem; 673 | } 674 | p.cwa-settings-disclaimer { 675 | font-size: small; 676 | font-style: italic; 677 | color: #444444; 678 | padding-left: 10px; 679 | line-height: normal; 680 | } 681 | 682 | select.cwa-settings-select { 683 | text-align: center; 684 | border-radius: 6px; 685 | width: 8rem; 686 | background-color: #f8f8f8 !important; 687 | } 688 | 689 | .settings-section-header { 690 | color: #45b29d; 691 | padding-bottom: 14px; 692 | } 693 | 694 | .settings-container { 695 | background: #f8f8f8; 696 | padding-left: 4rem; 697 | padding-right: 6rem; 698 | margin-right: 2rem; 699 | padding-top: 2rem; 700 | padding-bottom: 2rem; 701 | margin-bottom: 2rem; 702 | } 703 | 704 | .cwa_stats_container { 705 | display: grid; 706 | grid-template-columns: 1fr 1fr 1fr 1fr; /* Default: 4 equal columns */ 707 | gap: 10px; /* Space between sections */ 708 | padding: 20px; 709 | width: 100%; /* Ensure it spans the full width */ 710 | height: auto; /* Allow height to adjust based on content */ 711 | } 712 | 713 | .cwa_stats_section { 714 | display: flex; 715 | flex-direction: column; 716 | justify-content: center; 717 | align-items: center; 718 | border: none; 719 | border-radius: 8px; 720 | padding: 20px; 721 | text-align: center; 722 | background-color: #ffffff; 723 | } 724 | 725 | .cwa_stats_header { 726 | font-size: 1.5em; 727 | font-weight: bold; 728 | margin-bottom: 10px; 729 | color: #333333; 730 | } 731 | 732 | .cwa_stats_value { 733 | font-size: 2.5em; 734 | font-weight: bold; 735 | color: #45b29d; 736 | } 737 | 738 | /* Media Query for Medium Screens */ 739 | @media (max-width: 1024px) { 740 | .cwa_stats_container { 741 | grid-template-columns: repeat( 742 | 2, 743 | 1fr 744 | ); /* 2 equal columns on medium screens */ 745 | } 746 | } 747 | 748 | /* Media Query for Mobile Screens or Thin Windows */ 749 | @media (max-width: 480px) { 750 | .cwa_stats_container { 751 | grid-template-columns: 1fr; /* 1 column, 4 sections stacked vertically */ 752 | } 753 | } 754 | 755 | .stats_see_more_btn { 756 | border-radius: 6px !important; 757 | margin-bottom: 0px !important; 758 | } 759 | 760 | .refresh-cwa { 761 | width: -webkit-fill-available; 762 | justify-content: space-evenly; 763 | } 764 | -------------------------------------------------------------------------------- /root/app/calibre-web/cps/static/js/get_meta.js: -------------------------------------------------------------------------------- 1 | /* This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web) 2 | * Copyright (C) 2018 idalin 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU General Public License as published by 6 | * the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU General Public License 15 | * along with this program. If not, see . 16 | */ 17 | /* global _, i18nMsg, tinymce, getPath */ 18 | 19 | $(function () { 20 | var msg = i18nMsg; 21 | var keyword = ""; 22 | 23 | var templates = { 24 | bookResult: _.template($("#template-book-result").html()), 25 | }; 26 | 27 | function getUniqueValues(attribute_name, book) { 28 | var presentArray = $.map( 29 | $("#" + attribute_name) 30 | .val() 31 | .split(","), 32 | $.trim 33 | ); 34 | if (presentArray.length === 1 && presentArray[0] === "") { 35 | presentArray = []; 36 | } 37 | $.each(book[attribute_name], function (i, el) { 38 | if ($.inArray(el, presentArray) === -1) presentArray.push(el); 39 | }); 40 | return presentArray; 41 | } 42 | 43 | function populateForm(book, idx) { 44 | var updateItems = Object.fromEntries( 45 | Array.from(document.querySelectorAll(`[data-meta-index="${idx}"]`)).map( 46 | (value) => [value.dataset.metaValue, value.checked] 47 | ) 48 | ); 49 | if (updateItems.description) { 50 | tinymce.get("comments").setContent(book.description); 51 | } 52 | if (updateItems.tags) { 53 | var uniqueTags = getUniqueValues("tags", book); 54 | $("#tags").val(uniqueTags.join(", ")); 55 | } 56 | var uniqueLanguages = getUniqueValues("languages", book); 57 | if (updateItems.authors) { 58 | var ampSeparatedAuthors = (book.authors || []).join(" & "); 59 | $("#authors").val(ampSeparatedAuthors); 60 | } 61 | if (updateItems.title) { 62 | $("#title").val(book.title); 63 | } 64 | $("#languages").val(uniqueLanguages.join(", ")); 65 | $("#rating").data("rating").setValue(Math.round(book.rating)); 66 | 67 | if (updateItems.cover && book.cover && $("#cover_url").length) { 68 | $(".cover img").attr("src", book.cover); 69 | $("#cover_url").val(book.cover); 70 | } 71 | if (updateItems.pubDate) { 72 | $("#pubdate").val(book.publishedDate); 73 | } 74 | if (updateItems.publisher) { 75 | $("#publisher").val(book.publisher); 76 | } 77 | if (updateItems.series && typeof book.series !== "undefined") { 78 | $("#series").val(book.series); 79 | } 80 | if (updateItems.seriesIndex && typeof book.series_index !== "undefined") { 81 | $("#series_index").val(book.series_index); 82 | } 83 | if (typeof book.identifiers !== "undefined") { 84 | selectedIdentifiers = Object.keys(book.identifiers) 85 | .filter((key) => updateItems[key]) 86 | .reduce((result, key) => { 87 | result[key] = book.identifiers[key]; 88 | return result; 89 | }, {}); 90 | populateIdentifiers(selectedIdentifiers); 91 | } 92 | } 93 | 94 | function populateIdentifiers(identifiers) { 95 | for (const property in identifiers) { 96 | console.log(`${property}: ${identifiers[property]}`); 97 | if ($('input[name="identifier-type-' + property + '"]').length) { 98 | $('input[name="identifier-val-' + property + '"]').val( 99 | identifiers[property] 100 | ); 101 | } else { 102 | addIdentifier(property, identifiers[property]); 103 | } 104 | } 105 | } 106 | 107 | function addIdentifier(name, value) { 108 | var line = ""; 109 | line += 110 | ''; 117 | line += 118 | ''; 125 | line += 126 | '' + 127 | _("Remove") + 128 | ""; 129 | line += ""; 130 | $("#identifier-table").append(line); 131 | } 132 | 133 | function doSearch(keyword) { 134 | if (keyword) { 135 | $("#meta-info").text(msg.loading); 136 | $.ajax({ 137 | url: getPath() + "/metadata/search", 138 | type: "POST", 139 | data: { query: keyword }, 140 | dataType: "json", 141 | success: function success(data) { 142 | if (data.length) { 143 | $("#meta-info").html('
    '); 144 | data.forEach(function (book, idx) { 145 | var $book = $(templates.bookResult({ book: book, index: idx })); 146 | $book.find("button").on("click", function () { 147 | populateForm(book, idx); 148 | }); 149 | $("#book-list").append($book); 150 | }); 151 | } else { 152 | $("#meta-info").html( 153 | '

    ' + 154 | msg.no_result + 155 | "!

    " + 156 | $("#meta-info")[0].innerHTML 157 | ); 158 | } 159 | }, 160 | error: function error() { 161 | $("#meta-info").html( 162 | '

    ' + 163 | msg.search_error + 164 | "!

    " + 165 | $("#meta-info")[0].innerHTML 166 | ); 167 | }, 168 | }); 169 | } 170 | } 171 | 172 | function populate_provider() { 173 | $("#metadata_provider").empty(); 174 | $.ajax({ 175 | url: getPath() + "/metadata/provider", 176 | type: "get", 177 | dataType: "json", 178 | success: function success(data) { 179 | data.forEach(function (provider) { 180 | var checked = ""; 181 | if (provider.active) { 182 | checked = "checked"; 183 | } 184 | var $provider_button = 185 | ''; 198 | $("#metadata_provider").append($provider_button); 199 | }); 200 | }, 201 | }); 202 | } 203 | 204 | $(document).on("change", ".pill", function () { 205 | var element = $(this); 206 | var id = element.data("control"); 207 | var initial = element.data("initial"); 208 | var val = element.prop("checked"); 209 | var params = { id: id, value: val }; 210 | if (!initial) { 211 | params["initial"] = initial; 212 | params["query"] = keyword; 213 | } 214 | $.ajax({ 215 | method: "post", 216 | contentType: "application/json; charset=utf-8", 217 | dataType: "json", 218 | url: getPath() + "/metadata/provider/" + id, 219 | data: JSON.stringify(params), 220 | success: function success(data) { 221 | element.data("initial", "true"); 222 | data.forEach(function (book, idx) { 223 | var $book = $(templates.bookResult({ book: book, index: idx })); 224 | $book.find("button").on("click", function () { 225 | populateForm(book, idx); 226 | }); 227 | $("#book-list").append($book); 228 | }); 229 | }, 230 | }); 231 | }); 232 | 233 | $("#meta-search").on("submit", function (e) { 234 | e.preventDefault(); 235 | keyword = $("#keyword").val(); 236 | $(".pill").each(function () { 237 | $(this).data("initial", $(this).prop("checked")); 238 | }); 239 | doSearch(keyword); 240 | }); 241 | 242 | $("#get_meta").click(function () { 243 | populate_provider(); 244 | var bookTitle = $("#title").val(); 245 | $("#keyword").val(bookTitle); 246 | keyword = bookTitle; 247 | doSearch(bookTitle); 248 | }); 249 | $("#metaModal").on("show.bs.modal", function (e) { 250 | $(e.relatedTarget).one("focus", function (e) { 251 | $(this).blur(); 252 | }); 253 | }); 254 | }); 255 | -------------------------------------------------------------------------------- /root/app/calibre-web/cps/static/user-profile-data/CWA-profile-updater.js: -------------------------------------------------------------------------------- 1 | fetch('/user_profiles.json') 2 | .then(response => response.json()) 3 | .then(usernameToImage => { 4 | var usernameElement = document.querySelector('#top_user .hidden-sm'); 5 | if (usernameElement) { 6 | var username = usernameElement.textContent.trim(); 7 | 8 | if (usernameToImage[username]) { 9 | var style = document.createElement('style'); 10 | style.innerHTML = ` 11 | .profileDrop > span:before { 12 | background-image: url(${usernameToImage[username]}) !important; 13 | } 14 | body.me > div.container-fluid > div.row-fluid > div.col-sm-10:before { 15 | background-image: url(${usernameToImage[username]}) !important; 16 | } 17 | `; 18 | document.head.appendChild(style); 19 | } 20 | } 21 | }); 22 | -------------------------------------------------------------------------------- /root/app/calibre-web/cps/templates/config_db.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | {% block flash %} 3 | 6 | {% endblock %} 7 | {% block body %} 8 |
    9 |

    {{title}}

    10 |
    11 | 12 | 13 |
    14 | 15 | 16 | 17 | 18 |
    19 |
    20 | 21 | 22 |
    23 |
    24 |
    25 | 26 | 27 | 28 | 29 |
    30 |
    31 | {% if feature_support['gdrive'] %} 32 |
    33 | 34 | 35 |
    36 | {% if not gdriveError and config.config_use_google_drive %} 37 | {% if show_authenticate_google_drive and config.config_use_google_drive %} 38 | 41 | {% else %} 42 | {% if not show_authenticate_google_drive %} 43 |
    44 | 45 | 50 |
    51 | {% if config.config_google_drive_watch_changes_response %} 52 | 53 |
    54 | 55 | {{_('Revoke')}} 56 |
    57 | {% else %} 58 | Enable watch of metadata.db 59 | {% endif %} 60 | {% endif %} 61 | {% endif %} 62 | {% endif %} 63 | {% endif %} 64 |
    65 |
    {{_('Save')}}
    66 | {{_('Cancel')}} 67 |
    68 |
    69 |
    70 | {% endblock %} 71 | {% block modal %} 72 | {{ filechooser_modal() }} 73 | {{ change_confirm_modal() }} 74 | 87 | {% endblock %} 88 | -------------------------------------------------------------------------------- /root/app/calibre-web/cps/templates/cwa_convert_library.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | 3 | {% block body %} 4 |
    5 |

    {{title}}

    6 |
    7 |
    8 |

    CWA Library Convertor - Current Target Format - {{target_format}}

    9 | 13 |
    14 |
    17 |

    18 | Upon loading this page, if you have previously started a run of the CWA Convert Library service either here in the Web UI or through the CLI, you will see the output of the most recent previous run below. 19 | Once you start a run, you are free to leave the page and return whenever you want to check on the run's progress. If you wish to cancel a run that is still in progress, simply press the Cancel button above 20 | and the run will terminate ASAP. If you wish to change the service's target format, please change your target format in the CWA Settings panel as desired. 21 |

    22 | 26 |
    27 |
    28 |
    29 |
    30 |
    31 |
    38 |

    No current or previous run to display. Press the Start button above to initiate a run.

    39 |
    40 |
    41 |
    42 |
    43 | {% endblock %} 44 | 45 | {% block js %} 46 | 104 | {% endblock %} -------------------------------------------------------------------------------- /root/app/calibre-web/cps/templates/cwa_epub_fixer.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | 3 | {% block body %} 4 |
    5 |

    {{title}}

    6 |
    7 |
    8 |

    CWA Send-to-Kindle EPUB Fixer

    9 | 13 |
    14 |
    17 |

    18 | Upon loading this page, if you have previously started a run of the CWA Send-to_kindle EPUB Fixer service either here in the Web UI or through the CLI, you will see the output of the most recent previous run below. 19 | Once you start a run, you are free to leave the page and return whenever you want to check on the run's progress. If you wish to cancel a run that is still in progress, simply press the Cancel button above 20 | and the run will terminate ASAP. This tool is based on the Amazon Kindle EPUB Fix tool by innocenat 21 |

    22 | 26 |
    27 |
    28 |
    29 |
    30 |
    31 |
    38 |

    No current or previous run to display. Press the Start button above to initiate a run.

    39 |
    40 |
    41 |
    42 |
    43 | {% endblock %} 44 | 45 | {% block js %} 46 | 104 | {% endblock %} -------------------------------------------------------------------------------- /root/app/calibre-web/cps/templates/cwa_list_logs.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | 3 | {% block header %} 4 | 6 | {% endblock %} 7 | 8 | {% block body %} 9 |
    10 |

    {{title}}

    11 | 12 |
    13 |
    14 |

    {{title}} - Logs

    15 |
    16 |
    17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | {% for log, log_path in logs.items() %} 25 | 26 | 27 | 28 | 29 | 30 | 31 | {% endfor %} 32 |
    Log DateLog FilenameOpen LogDownload Log
    {{ log_dates[log]["date"] }} {{ log_dates[log]["time"] }}{{ log }}Open in Browser{{ log_path }}
    33 |
    34 | 35 |
    36 | {% endblock %} -------------------------------------------------------------------------------- /root/app/calibre-web/cps/templates/cwa_read_log.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | 3 | {% block body %} 4 |
    5 |

    {{title}}

    6 |
    7 |
    8 |

    {{ title }}


    9 | {{_('Download Log')}} 10 |
    11 | 12 |
    13 |
    20 |

    {{ log | replace("\n", "
    ") | safe }}

    21 |
    22 |
    23 |
    24 |
    25 | {% endblock %} 26 | 27 | {% block js %} 28 | 30 | {% endblock %} -------------------------------------------------------------------------------- /root/app/calibre-web/cps/templates/cwa_settings.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | {% block flash %} 3 | 6 | {% endblock %} 7 | 8 | {% block header %} 9 | 10 | {% endblock %} 11 | 12 | {% block body %} 13 |
    14 |

    {{title}}

    15 |
    16 |
    17 |

    Enable/Disable Calibre-Web Automated Services

    18 | 19 | {% if cwa_settings['auto_convert'] %} 20 | 21 | {% else %} 22 | 23 | {% endif %} 24 |
    25 |

    26 | On by default, when active all ingested books will automatically be converted to the target format specified below (epub by default) 27 | EXCEPT those you have specifically told CWA to ignore below. 28 |

    29 | 30 | {% if cwa_settings['auto_metadata_enforcement'] %} 31 | 32 | {% else %} 33 | 34 | {% endif %} 35 |
    36 |

    37 | On by default, when active, whenever the Metadata and/or Cover Image is edited in the Web UI, the CWA Metadata Enforcement service 38 | will then apply those changes the ebook files themselves. Normally in Stock CW or when this setting is disabled, the changes made 39 | are only applied to what you see in the Web UI, not the ebook files themselves. This feature currently only supports files in EPUB 40 | or AZW3 format. 41 |

    42 | 43 | {% if cwa_settings['kindle_epub_fixer'] %} 44 | 45 | {% else %} 46 | 47 | {% endif %} 48 |
    49 |

    50 | When active, the encoding among other attributes of all EPUB files processed by CWA will be checked and fixed to ensure maximum 51 | compatibility with Amazon's Send-to-Kindle Service. 52 |

    53 | (TLDR: if you've ever had EPUB files that Amazon just constantly rejects for seemingly no reason, this should prevent that 54 | from happening again). This tool was adapted from the kindle-epub-fix.netlify.app 55 | tool made by innocenat. 56 |

    57 |
    58 | 59 |
    60 |

    Web UI Settings

    61 | 62 | {% if cwa_settings['cwa_update_notifications'] %} 63 | 64 | {% else %} 65 | 66 | {% endif %} 67 |
    68 |

    69 | When active, you will no longer receive notifications in the Web UI when a new version of CWA is released 70 |

    71 |
    72 | 73 |
    74 |

    Automatic Backup Settings

    75 | 76 | {% if cwa_settings['auto_backup_imports'] %} 77 | 78 | {% else %} 79 | 80 | {% endif %} 81 |
    82 |

    83 | When active, a copy of all imported files will be stored in /config/processed_books/imported 84 |

    85 | 86 | {% if cwa_settings['auto_backup_conversions'] %} 87 | 88 | {% else %} 89 | 90 | {% endif %} 91 |
    92 |

    93 | When active, the originals of ingested files that undergo conversion will be stored in /config/processed_books/converted 94 |

    95 | 96 | {% if cwa_settings['auto_backup_epub_fixes'] %} 97 | 98 | {% else %} 99 | 100 | {% endif %} 101 |
    102 |

    103 | When active, the originals of EPUBs processed by the CWA Kindle EPUB Fixer service will be stored in /config/processed_books/fixed_originals 104 |

    105 | 106 |
    107 | 108 | {% if cwa_settings['auto_zip_backups'] %} 109 | 110 | {% else %} 111 | 112 | {% endif %} 113 |
    114 |

    115 | When active, just before midnight each day, the cwa-auto-zipper service will make zip archives of all the backed up converted, imported 116 | and failed files from that day. This is to help keep the subdirectories of /config/processed_books organised and to minimise disk space 117 | usage. 118 |

    119 |
    120 | 121 |
    122 |

    CWA Auto-Conversion Target Format - EPUB by Default

    123 | 124 |

    125 | When the Auto-Convert feature is active, all ingested books will be automatically converted to the format chosen here (except those 126 | formats selected in the ignore list below) 127 |

    128 | 129 | 138 |

    139 | * CWA's Metadata Enforcement service can currently only support file in either EPUB and AZW3 format. Files in other formats will simply 140 | be ignored by the service 141 |

    142 |
    143 | 144 |
    145 |

    CWA Auto-Convert - Ignored Formats

    146 |

    147 | The formats selected here will be ignored by CWA's Auto-Conversion feature when it's active, meaning they will be imported as is. 148 |

    149 |
    150 | {% for format in ignorable_formats -%} 151 | 167 | {% endfor %} 168 |
    169 |
    170 | 171 |
    172 |

    CWA Auto-Ingest - Ignored Formats

    173 |

    174 | The formats selected here will be ignored by CWA's Auto-Ingest feature, meaning files in these formats won't be added to the 175 | library by CWA during the ingest process 176 |

    177 |
    178 | {% for format in ignorable_formats -%} 179 | 195 | {% endfor %} 196 |
    197 |
    198 | 199 |
    200 |
    201 | 202 | 203 |
    204 |
    205 | 206 |
    207 | 208 | 209 | {% endblock %} -------------------------------------------------------------------------------- /root/app/calibre-web/cps/templates/cwa_stats.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | {% block body %} 3 | 4 | {% block header %} 5 | 11 | {% endblock %} 12 | 13 |
    14 |

    {{title}}

    15 | 16 |
    17 |

    Calibre-Web Automated - Server Stats

    18 |
    19 |
    20 |
    Total Books
    21 |
    {{cwa_stats["total_books"]}}
    22 |
    23 |
    24 |
    Books Enforced
    25 |
    {{cwa_stats["cwa_enforcement"]}}
    26 |
    27 |
    28 |
    Books Converted
    29 |
    {{cwa_stats["cwa_conversions"]}}
    30 |
    31 |
    32 |
    Books Fixed
    33 |
    {{cwa_stats["epub_fixes"]}}
    34 |
    35 |
    36 |
    37 | 38 |
    39 | 40 |
    41 |
    42 |

    Calibre-Web Automated Conversion History

    43 | {{_('Click to See More')}} 44 |
    45 |
    46 | 47 | 48 | {% for header in headers_conversion %} 49 | 50 | {% endfor %} 51 | 52 | {% for row in data_conversions|reverse %} 53 | 54 | {% for cell in row %} 55 | 56 | {% endfor %} 57 | 58 | {% endfor %} 59 |
    {{ header }}
    {{ cell }}
    60 |
    61 | 62 |
    63 |
    64 |

    Calibre-Web Automated Import History

    65 | {{_('Click to See More')}} 66 |
    67 |
    68 | 69 | 70 | {% for header in headers_import %} 71 | 72 | {% endfor %} 73 | 74 | {% for row in data_imports|reverse %} 75 | 76 | {% for cell in row %} 77 | 78 | {% endfor %} 79 | 80 | {% endfor %} 81 |
    {{ header }}
    {{ cell }}
    82 |
    83 | 84 |
    85 |
    86 |

    Calibre-Web Automated EPUB Fixer History

    87 | {{_('Click to See More')}} 88 |
    89 |
    90 | 91 | 92 | {% for header in headers_epub_fixer %} 93 | 94 | {% endfor %} 95 | 96 | {% for row in data_epub_fixer|reverse %} 97 | 98 | {% for cell in row %} 99 | 100 | {% endfor %} 101 | 102 | {% endfor %} 103 |
    {{ header }}
    {{ cell }}
    104 |
    105 |

    EPUB Fixer History with Paths & Fixes

    106 | {{_('Click to See More')}} 107 |
    108 |
    109 | 110 | 111 | {% for header in headers_epub_fixer_with_fixes %} 112 | 113 | {% endfor %} 114 | 115 | {% for row in data_epub_fixer_with_fixes|reverse %} 116 | 117 | {% for cell in row %} 118 | 121 | {% endfor %} 122 | 123 | {% endfor %} 124 |
    {{ header }}
    119 |
    {{ cell | replace("\n", "
    ") | safe }}
    120 |
    125 |
    126 | 127 |
    128 |
    129 |

    Calibre-Web Automated Enforcement History

    130 | {{_('Click to See More')}} 131 |
    132 |
    133 | 134 | 135 | {% for header in headers_enforcement %} 136 | 137 | {% endfor %} 138 | 139 | {% for row in data_enforcement|reverse %} 140 | 141 | {% for cell in row %} 142 | 143 | {% endfor %} 144 | 145 | {% endfor %} 146 |
    {{ header }}
    {{ cell }}
    147 |
    148 |

    Enforcement History with Paths

    149 | {{_('Click to See More')}} 150 |
    151 |
    152 | 153 | 154 | {% for header in headers_enforcement_with_paths %} 155 | 156 | {% endfor %} 157 | 158 | {% for row in data_enforcement_with_paths|reverse %} 159 | 160 | {% for cell in row %} 161 | 162 | {% endfor %} 163 | 164 | {% endfor %} 165 |
    {{ header }}
    {{ cell }}
    166 |
    167 | 168 |
    169 | {% endblock %} -------------------------------------------------------------------------------- /root/app/calibre-web/cps/templates/cwa_stats_full.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | {% block body %} 3 |
    4 |

    Calibre-Web Automated Stats

    5 |
    6 |

    {{title}}


    7 | 8 | 9 | {% for header in table_headers %} 10 | 11 | {% endfor %} 12 | 13 | {% for row in data|reverse %} 14 | 15 | {% for cell in row %} 16 | 19 | {% endfor %} 20 | 21 | {% endfor %} 22 |
    {{ header }}
    17 |
    {{ cell | replace("\n", "
    ") | safe }}
    18 |
    23 |
    24 | {% endblock %} -------------------------------------------------------------------------------- /root/app/calibre-web/cps/templates/image.html: -------------------------------------------------------------------------------- 1 | {% macro book_cover(book, alt=None) -%} 2 | {%- set image_title = book.title if book.title else book.name -%} 3 | {%- set image_alt = alt if alt else image_title -%} 4 | {% set srcset = book|get_cover_srcset %} 5 | {{ image_alt }} 11 | {%- endmacro %} 12 | 13 | {% macro series(series, alt=None) -%} 14 | {%- set image_alt = alt if alt else image_title -%} 15 | {% set srcset = series|get_series_srcset %} 16 | {{ title }} 22 | {%- endmacro %} 23 | -------------------------------------------------------------------------------- /root/app/calibre-web/cps/templates/profile_pictures.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {{instance}} | {{title}} 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | {% if g.current_theme == 1 %} 19 | 20 | 21 | 22 | {% endif %} 23 | 24 | 25 | 26 | 106 | 107 | 108 |
    109 |

    {{ title }}

    110 |
    111 | 112 |
    113 |

    This is the admin page for managing profile pictures. This feature is currently in development.

    114 | 115 |

    116 | To upload a profile picture, please use the following form. Note: The image data must be in Base64 format. 117 | You can convert an image to Base64 using various online tools. If you use a website such as 118 | www.base64-image.de, 119 | then make sure you use the version that starts with the line "data:image/png;base64," not the URL version. 120 | To display the image as a round shape, it must already be in PNG format with a round frame applied. 121 | It is recommended to use images no larger than 100x100px to avoid using excessive storage space. 122 |

    123 | 124 |
    125 |
    126 | 127 | 128 |
    129 | 130 | 131 | Enter the exact username for the profile picture assignment. 132 |
    133 | 134 |
    135 | 136 | 137 | Paste the full Base64 encoded PNG image data here. 138 |
    139 | 140 | 141 |
    142 |
    143 | 144 |
    145 | Back to Admin Page 146 |
    147 |
    148 | 149 | 150 | 155 | -------------------------------------------------------------------------------- /root/app/calibre-web/cps/templates/user_edit.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | {% block body %} 3 |
    4 |

    {{title}}

    5 |
    6 | 7 |
    8 | {% if new_user or ( current_user and content.name != "Guest" and current_user.role_admin() ) %} 9 |
    10 | 11 | 12 |
    13 | {% endif %} 14 |
    15 | 16 | 17 |
    18 | {% if ( current_user and current_user.role_passwd() or current_user.role_admin() ) and not content.role_anonymous() %} 19 | {% if current_user and current_user.role_admin() and not new_user and not profile and ( mail_configured and content.email if content.email != None ) %} 20 | {{_('Reset user Password')}} 21 | {% endif %} 22 |
    23 | 24 | 25 |
    26 | {% endif %} 27 |
    28 | 29 | 30 |
    31 | {% if not content.role_anonymous() %} 32 |
    33 | 34 | 39 |
    40 | {% endif %} 41 | 42 |
    43 | 44 | 50 |
    51 | {% if registered_oauth.keys()| length > 0 and not new_user and profile %} 52 | {% for id, name in registered_oauth.items() %} 53 |
    54 | 55 | {% if id not in oauth_status %} 56 | {{_('Link')}} 57 | {% else %} 58 | {{_('Unlink')}} 59 | {% endif %} 60 | {% endfor %} 61 |
    62 | {% endif %} 63 | {% if hardcover_support and not new_user %} 64 |
    65 | 66 | 67 |
    68 | {% endif %} 69 | {% if kobo_support and not new_user %} 70 | 71 |
    72 | {{_('Create/View')}} 73 | 74 |
    75 |
    76 | 77 |
    78 | {% endif %} 79 |
    80 | {% for element in sidebar %} 81 | {% if element['config_show'] %} 82 |
    83 | 84 | 85 |
    86 | {% endif %} 87 | {% endfor %} 88 |
    89 | 90 | 91 |
    92 | {% if ( current_user and current_user.role_admin() and not new_user ) and not simple %} 93 | {{_('Add Allowed/Denied Tags')}} 94 | {{_('Add allowed/Denied Custom Column Values')}} 95 | {% endif %} 96 |
    97 |
    98 | {% if current_user and current_user.role_admin() and not profile %} 99 | {% if not content.role_anonymous() %} 100 |
    101 | 102 | 103 |
    104 | {% endif %} 105 |
    106 | 107 | 108 |
    109 |
    110 | 111 | 112 |
    113 | {% if config.config_uploading %} 114 |
    115 | 116 | 117 |
    118 | {% endif %} 119 |
    120 | 121 | 122 |
    123 |
    124 |
    125 | 126 | 127 |
    128 |
    129 | {% if not content.role_anonymous() %} 130 |
    131 | 132 | 133 |
    134 |
    135 | 136 | 137 |
    138 | {% endif %} 139 | {% endif %} 140 | {% if kobo_support and not content.role_anonymous() and not simple%} 141 |
    142 | 143 | 144 |
    145 | {% endif %} 146 |
    147 |
    148 |
    {{_('Save')}}
    149 | {% if not profile %} 150 |
    {{_('Cancel')}}
    151 | {% endif %} 152 | {% if current_user and current_user.role_admin() and not profile and not new_user and not content.role_anonymous() %} 153 |
    {{_('Delete User')}}
    154 | {% endif %} 155 |
    156 |
    157 |
    158 |
    159 | 160 | 174 | 175 | {% endblock %} 176 | {% block modal %} 177 | {{ restrict_modal() }} 178 | {{ delete_confirm_modal() }} 179 | {% endblock %} 180 | {% block js %} 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | {% endblock %} 190 | -------------------------------------------------------------------------------- /root/etc/s6-overlay/s6-rc.d/cwa-auto-library/dependencies.d/cwa-init: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crocodilestick/Calibre-Web-Automated/3746332a6d04e518f194551caa6363851e0e0e5d/root/etc/s6-overlay/s6-rc.d/cwa-auto-library/dependencies.d/cwa-init -------------------------------------------------------------------------------- /root/etc/s6-overlay/s6-rc.d/cwa-auto-library/run: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | python3 /app/calibre-web-automated/scripts/auto_library.py 4 | 5 | if [[ $? == 1 ]] 6 | then 7 | echo "[cwa-auto-library] Service did not complete successfully (see errors above). Ending service..." 8 | elif [[ $? == 0 ]] 9 | then 10 | echo "[cwa-auto-library] Service completed successfully! Ending service..." 11 | fi -------------------------------------------------------------------------------- /root/etc/s6-overlay/s6-rc.d/cwa-auto-library/type: -------------------------------------------------------------------------------- 1 | oneshot -------------------------------------------------------------------------------- /root/etc/s6-overlay/s6-rc.d/cwa-auto-library/up: -------------------------------------------------------------------------------- 1 | /etc/s6-overlay/s6-rc.d/cwa-auto-library/run -------------------------------------------------------------------------------- /root/etc/s6-overlay/s6-rc.d/cwa-auto-zipper/dependencies.d/cwa-init: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crocodilestick/Calibre-Web-Automated/3746332a6d04e518f194551caa6363851e0e0e5d/root/etc/s6-overlay/s6-rc.d/cwa-auto-zipper/dependencies.d/cwa-init -------------------------------------------------------------------------------- /root/etc/s6-overlay/s6-rc.d/cwa-auto-zipper/run: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo "[cwa-auto-zipper] Starting CWA-Auto-Zipper service..." 4 | echo "[cwa-auto-zipper] Matching internal localtime & timezone with the one provided..." 5 | 6 | tz=$(printcontenv TZ) 7 | if [[ $tz == "" ]] 8 | then 9 | echo "[cwa-auto-zipper] No TZ env found. Defaulting to UTC..." 10 | else 11 | region=$(echo $tz | awk -F '/' '{ print $1 }') 12 | city=$(echo $tz | awk -F '/' '{ print $2 }') 13 | 14 | zoneinfo_file="/usr/share/zoneinfo/$region/$city" 15 | if test -f $zoneinfo_file; then 16 | echo "[cwa-auto-zipper] Zoneinfo for $tz found. Setting /etc/localtime and /etc/timezone to match..." 17 | ln -sfn $zoneinfo_file /etc/localtime 18 | echo $tz > '/etc/timezone' 19 | echo "[cwa-auto-zipper] Timezone & Localtime successfully set to $tz. Initiating Auto-Zipper ..." 20 | else 21 | echo "[cwa-auto-zipper] Zoneinfo $tz not found. Using UTC as default..." 22 | fi 23 | fi 24 | 25 | WAKEUP="23:59" # Wake up at this time tomorrow and run a command 26 | 27 | # Sometimes you want to sleep until a specific time in a bash script. This is useful, for instance 28 | # in a docker container that does a single thing at a specific time on a regular interval, but does not want to be bothered 29 | # with cron or at. The -d option to date is VERY flexible for relative times. 30 | # See https://www.gnu.org/software/coreutils/manual/html_node/Relative-items-in-date-strings.html#Relative-items-in-date-strings 31 | 32 | # This script runs in an infinite loop, waking up every night at 23:59 33 | while : 34 | do 35 | SECS=$(expr `date -d "$WAKEUP" +%s` - `date -d "now" +%s`) 36 | if [[ $SECS -lt 0 ]] 37 | then 38 | SECS=$(expr `date -d "tomorrow $WAKEUP" +%s` - `date -d "now" +%s`) 39 | fi 40 | echo "[cwa-auto-zipper] Next run in $SECS seconds." 41 | sleep $SECS & # We sleep in the background to make the script interruptible via SIGTERM when running in docker 42 | wait $! 43 | python3 /app/calibre-web-automated/scripts/auto_zip.py 44 | if [[ $? == 1 ]] 45 | then 46 | echo "[cwa-auto-zipper] Error occurred during script initialisation (see errors above)." 47 | elif [[ $? == 2 ]] 48 | then 49 | echo "[cwa-auto-zipper] Error occurred while zipping today's files (see errors above)." 50 | elif [[ $? == 3 ]] 51 | then 52 | echo "[cwa-auto-zipper] Error occurred while trying to removed the files that have been zipped (see errors above)." 53 | fi 54 | sleep 60 55 | done 56 | -------------------------------------------------------------------------------- /root/etc/s6-overlay/s6-rc.d/cwa-auto-zipper/type: -------------------------------------------------------------------------------- 1 | longrun -------------------------------------------------------------------------------- /root/etc/s6-overlay/s6-rc.d/cwa-auto-zipper/up: -------------------------------------------------------------------------------- 1 | /etc/s6-overlay/s6-rc.d/cwa-auto-zipper/run -------------------------------------------------------------------------------- /root/etc/s6-overlay/s6-rc.d/cwa-ingest-service/dependencies.d/cwa-auto-library: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crocodilestick/Calibre-Web-Automated/3746332a6d04e518f194551caa6363851e0e0e5d/root/etc/s6-overlay/s6-rc.d/cwa-ingest-service/dependencies.d/cwa-auto-library -------------------------------------------------------------------------------- /root/etc/s6-overlay/s6-rc.d/cwa-ingest-service/run: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # https://github.com/janeczku/calibre-web/wiki/Automatically-import-new-books-(Linux) 4 | 5 | # This script is used to automatically import downloaded ebooks into a Calibre database. 6 | # Reference: https://manual.calibre-ebook.com/generated/en/calibredb.html#add 7 | echo "========== STARTING CWA-INGEST SERVICE ==========" 8 | 9 | WATCH_FOLDER=$(grep -o '"ingest_folder": "[^"]*' /app/calibre-web-automated/dirs.json | grep -o '[^"]*$') 10 | echo "[cwa-ingest-service] Watching folder: $WATCH_FOLDER" 11 | 12 | # Monitor the folder for new files 13 | s6-setuidgid abc inotifywait -m -r --format="%e %w%f" -e close_write -e moved_to "$WATCH_FOLDER" | 14 | while read -r events filepath ; do 15 | # if [[ $(grep "$filepath" ingest-log-test.txt | egrep -o '[0-9]{10}') ]]; then 16 | # CURRENT_TIME=$(date +'%s') 17 | # TIME_OF_MATCH=$(grep "$filepath" ingest-log-test.txt | egrep -o '[0-9]{10}') 18 | # TODO NEED TO GET DIFFERENCE BETWEEN THE 2 TIMES AND IF LESS THAN 60 SECONDS, IGNORE 19 | echo "[cwa-ingest-service] New files detected - $filepath - Starting Ingest Processor..." 20 | python3 /app/calibre-web-automated/scripts/ingest_processor.py "$filepath" # & 21 | # echo "'${filepath}' - $(date +'%s')" >> /config/.ingest_dupe_list 22 | # INGEST_PROCESSOR_PID=$! 23 | # Wait for the ingest processor to finish 24 | # wait $INGEST_PROCESSOR_PID 25 | # if ! [[ $(ls -A "$WATCH_FOLDER") ]]; then 26 | # FILES="${WATCH_FOLDER}/*" 27 | # for f in $FILES 28 | # do 29 | # python3 /app/calibre-web-automated/scripts/ingest_processor.py "$f" & 30 | # INGEST_PROCESSOR_PID=$! 31 | # # Wait for the ingest processor to finish 32 | # wait $INGEST_PROCESSOR_PID 33 | # done 34 | # fi 35 | done 36 | 37 | -------------------------------------------------------------------------------- /root/etc/s6-overlay/s6-rc.d/cwa-ingest-service/type: -------------------------------------------------------------------------------- 1 | longrun -------------------------------------------------------------------------------- /root/etc/s6-overlay/s6-rc.d/cwa-ingest-service/up: -------------------------------------------------------------------------------- 1 | bash run.sh -------------------------------------------------------------------------------- /root/etc/s6-overlay/s6-rc.d/cwa-init/dependencies.d/init-custom-files: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crocodilestick/Calibre-Web-Automated/3746332a6d04e518f194551caa6363851e0e0e5d/root/etc/s6-overlay/s6-rc.d/cwa-init/dependencies.d/init-custom-files -------------------------------------------------------------------------------- /root/etc/s6-overlay/s6-rc.d/cwa-init/run: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | #------------------------------------------------------------------------------------------------------------------------ 4 | # Make sure required directories exist 5 | #------------------------------------------------------------------------------------------------------------------------ 6 | 7 | mkdir -p /config/processed_books/converted 8 | mkdir -p /config/processed_books/imported 9 | mkdir -p /config/processed_books/failed 10 | mkdir -p /config/processed_books/fixed_originals 11 | mkdir -p /config/log_archive 12 | mkdir -p /config/.cwa_conversion_tmp 13 | 14 | #------------------------------------------------------------------------------------------------------------------------ 15 | # Remove any leftover lock files 16 | #------------------------------------------------------------------------------------------------------------------------ 17 | 18 | declare -a lockFiles=("ingest_processor.lock" "convert_library.lock" "cover_enforcer.lock" "kindle_epub_fixer.lock") 19 | 20 | echo "[cwa-init] Checking for leftover lock files from previous instance..." 21 | 22 | counter=0 23 | 24 | for f in "${lockFiles[@]}" 25 | do 26 | if [ -f "/tmp/$f" ] 27 | then 28 | echo "[cwa-init] Leftover $f exists, removing now..." 29 | rm "/tmp/$f" 30 | echo "[cwa-init] Leftover $f removed." 31 | let counter++ 32 | fi 33 | done 34 | 35 | if [[ "$counter" -eq 0 ]] 36 | then 37 | echo "[cwa-init] No leftover lock files to remove. Ending service..." 38 | else 39 | echo "[cwa-init] $counter lock file(s) removed. Ending service..." 40 | fi 41 | 42 | #------------------------------------------------------------------------------------------------------------------------ 43 | # Check for existing app.db and create one from the included example if one doesn't already exist 44 | #------------------------------------------------------------------------------------------------------------------------ 45 | 46 | echo "[cwa-init] Checking for an existing app.db in /config..." 47 | 48 | if [ ! -f /config/app.db ]; then 49 | echo "[cwa-init] No existing app.db found! Creating new one..." 50 | cp /app/calibre-web-automated/empty_library/app.db /config/app.db 51 | else 52 | echo "[cwa-init] Existing app.db found!" 53 | fi 54 | 55 | #------------------------------------------------------------------------------------------------------------------------ 56 | # Ensure correct binary paths in app.db 57 | #------------------------------------------------------------------------------------------------------------------------ 58 | 59 | echo "[cwa-init] Setting binary paths in '/config/app.db' to the correct ones..." 60 | 61 | sqlite3 /config/app.db < 0 ]] 69 | then 70 | echo "[cwa-init] Service could not successfully set binary paths for '/config/app.db' (see errors above)." 71 | fi 72 | 73 | 74 | echo "[cwa-init] CWA-init complete! Service exiting now..." 75 | 76 | #------------------------------------------------------------------------------------------------------------------------ 77 | # Create blank json file for profile pictures if one doesn't exist 78 | #------------------------------------------------------------------------------------------------------------------------ 79 | 80 | if [ ! -f /config/user_profiles.json ]; then 81 | echo "[cwa-init] No existing user_profiles.json found! Creating blank one..." 82 | echo -e "{\n}" > /config/user_profiles.json 83 | else 84 | echo "[cwa-init] Existing user_profiles.json found!" 85 | fi 86 | 87 | #------------------------------------------------------------------------------------------------------------------------ 88 | # Set required permissions 89 | #------------------------------------------------------------------------------------------------------------------------ 90 | 91 | declare -a requiredDirs=("/config" "/calibre-library" "/app/calibre-web-automated") 92 | 93 | dirs=$(printf ", %s" "${requiredDirs[@]}") 94 | dirs=${dirs:1} 95 | 96 | echo "[cwa-init] Recursively setting ownership of everything in$dirs to abc:abc..." 97 | 98 | for d in "${requiredDirs[@]}" 99 | do 100 | chown -R abc:abc $d 101 | if [[ $? == 0 ]] 102 | then 103 | echo "[cwa-init] Successfully set permissions for '$d'!" 104 | elif [[ $? > 0 ]] 105 | then 106 | echo "[cwa-init] Service could not successfully set permissions for '$d' (see errors above)." 107 | fi 108 | done -------------------------------------------------------------------------------- /root/etc/s6-overlay/s6-rc.d/cwa-init/type: -------------------------------------------------------------------------------- 1 | oneshot -------------------------------------------------------------------------------- /root/etc/s6-overlay/s6-rc.d/cwa-init/up: -------------------------------------------------------------------------------- 1 | /etc/s6-overlay/s6-rc.d/cwa-init/run -------------------------------------------------------------------------------- /root/etc/s6-overlay/s6-rc.d/init-adduser/branding: -------------------------------------------------------------------------------- 1 | ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── 2 | 3 | ,gggg, ,ggg, gg ,gg 4 | ,88"""Y8b, ,dPYb, ,dPYb, dP""Y8a 88 ,8P ,dPYb, 5 | d8" `Y8 IP'`Yb IP'`Yb Yb, `88 88 d8' IP'`Yb 6 | d8' 8b d8 I8 8I gg I8 8I `" 88 88 88 I8 8I 7 | ,8I "Y88P' I8 8' "" I8 8' 88 88 88 I8 8' 8 | I8' ,gggg,gg I8 dP gg I8 dP ,gggggg, ,ggg, aaaaaaaa 88 88 88 ,ggg, I8 dP 9 | d8 dP" "Y8I I8dP 88 I8dP 88gg dP""""8I i8" "8i """""""" 88 88 88 i8" "8i I8dP 88gg 10 | Y8, i8' ,8I I8P 88 I8P 8I ,8' 8I I8, ,8I Y8 ,88, 8P I8, ,8I I8P 8I 11 | `Yba,,_____, ,d8, ,d8b,,d8b,_ _,88,_,d8b, ,8I ,dP Y8, `YbadP' Yb,,d8""8b,,dP `YbadP' ,d8b, ,8I 12 | `"Y8888888 P"Y8888P"`Y88P'"Y888P""Y88P'"Y88P"' 8P `Y8888P"Y888 "88" "88" 888P"Y8888P'"Y88P"' 13 | 14 | 15 | █████╗ ██╗ ██╗████████╗ ██████╗ ███╗ ███╗ █████╗ ████████╗███████╗██████╗ 16 | ██╔══██╗██║ ██║╚══██╔══╝██╔═══██╗████╗ ████║██╔══██╗╚══██╔══╝██╔════╝██╔══██╗ 17 | ███████║██║ ██║ ██║ ██║ ██║██╔████╔██║███████║ ██║ █████╗ ██║ ██║ 18 | ██╔══██║██║ ██║ ██║ ██║ ██║██║╚██╔╝██║██╔══██║ ██║ ██╔══╝ ██║ ██║ 19 | ██║ ██║╚██████╔╝ ██║ ╚██████╔╝██║ ╚═╝ ██║██║ ██║ ██║ ███████╗██████╔╝ 20 | ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═╝ ╚══════╝╚═════╝ 21 | 22 | 23 | ~~~ Based on images from linuxserver.io & Calibre-Web by janeczku ~~~ 24 | 25 | ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── 26 | -------------------------------------------------------------------------------- /root/etc/s6-overlay/s6-rc.d/metadata-change-detector/dependencies.d/cwa-auto-library: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crocodilestick/Calibre-Web-Automated/3746332a6d04e518f194551caa6363851e0e0e5d/root/etc/s6-overlay/s6-rc.d/metadata-change-detector/dependencies.d/cwa-auto-library -------------------------------------------------------------------------------- /root/etc/s6-overlay/s6-rc.d/metadata-change-detector/run: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo "========== STARTING METADATA CHANGE DETECTOR ===========" 4 | 5 | # Folder to monitor 6 | WATCH_FOLDER="/app/calibre-web-automated/metadata_change_logs" 7 | echo "[metadata-change-detector] Watching folder: $WATCH_FOLDER" 8 | 9 | # Create the folder if it doesn't exist 10 | mkdir -p "$WATCH_FOLDER" 11 | 12 | # Monitor the folder for new files 13 | s6-setuidgid abc inotifywait -m -e close_write -e moved_to --exclude '^.*\.(swp)$' "$WATCH_FOLDER" | 14 | while read -r directory events filename; do 15 | echo "[metadata-change-detector] New file detected: $filename" 16 | python3 /app/calibre-web-automated/scripts/cover_enforcer.py "--log" "$filename" 17 | done -------------------------------------------------------------------------------- /root/etc/s6-overlay/s6-rc.d/metadata-change-detector/type: -------------------------------------------------------------------------------- 1 | longrun -------------------------------------------------------------------------------- /root/etc/s6-overlay/s6-rc.d/metadata-change-detector/up: -------------------------------------------------------------------------------- 1 | bash run.sh -------------------------------------------------------------------------------- /root/etc/s6-overlay/s6-rc.d/universal-calibre-setup/dependencies.d/init-custom-files: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crocodilestick/Calibre-Web-Automated/3746332a6d04e518f194551caa6363851e0e0e5d/root/etc/s6-overlay/s6-rc.d/universal-calibre-setup/dependencies.d/init-custom-files -------------------------------------------------------------------------------- /root/etc/s6-overlay/s6-rc.d/universal-calibre-setup/run: -------------------------------------------------------------------------------- 1 | #!/usr/bin/with-contenv bash 2 | 3 | if [[ ! -f /usr/bin/apt ]]; then 4 | cat <<-EOF 5 | ******************************************************** 6 | ******************************************************** 7 | * * 8 | * !!!! * 9 | * universal-calibre mod is only supported on images * 10 | * using an Ubuntu base image. * 11 | * * 12 | ******************************************************** 13 | ******************************************************** 14 | EOF 15 | exit 0 16 | fi 17 | 18 | export DEBIAN_FRONTEND="noninteractive" 19 | 20 | CALIBRE_INSTALLED_TEST="$(calibredb --version)" 21 | 22 | if [[ ! $CALIBRE_INSTALLED_TEST =~ "calibredb (calibre [0-9]\.[0-9])" ]]; then 23 | echo "[universal-calibre-setup] USER NOTE: 'Ignore calibredb: command not found' above, nothing is wrong, this just indicates to CWA that Calibre still needs to be installed" 24 | echo "[universal-calibre-setup] Installing Calibre version $(cat /CALIBRE_RELEASE)..." 25 | /app/calibre/calibre_postinstall &> /dev/null 26 | if [[ $? == 0 ]] 27 | then 28 | echo "[universal-calibre-setup] Calibre setup completed successfully! Exiting now..." 29 | else 30 | echo "[universal-calibre-setup] Calibre setup was unsuccessful, 'calibre_postinstall' encountered an error. Exiting now..." 31 | fi 32 | else 33 | echo "[universal-calibre-setup] Skipping setup, Calibre already installed. Exiting now..." 34 | fi -------------------------------------------------------------------------------- /root/etc/s6-overlay/s6-rc.d/universal-calibre-setup/type: -------------------------------------------------------------------------------- 1 | oneshot -------------------------------------------------------------------------------- /root/etc/s6-overlay/s6-rc.d/universal-calibre-setup/up: -------------------------------------------------------------------------------- 1 | /etc/s6-overlay/s6-rc.d/universal-calibre-setup/run -------------------------------------------------------------------------------- /root/etc/s6-overlay/s6-rc.d/user/contents.d/cwa-auto-library: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crocodilestick/Calibre-Web-Automated/3746332a6d04e518f194551caa6363851e0e0e5d/root/etc/s6-overlay/s6-rc.d/user/contents.d/cwa-auto-library -------------------------------------------------------------------------------- /root/etc/s6-overlay/s6-rc.d/user/contents.d/cwa-auto-zipper: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crocodilestick/Calibre-Web-Automated/3746332a6d04e518f194551caa6363851e0e0e5d/root/etc/s6-overlay/s6-rc.d/user/contents.d/cwa-auto-zipper -------------------------------------------------------------------------------- /root/etc/s6-overlay/s6-rc.d/user/contents.d/cwa-ingest-service: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crocodilestick/Calibre-Web-Automated/3746332a6d04e518f194551caa6363851e0e0e5d/root/etc/s6-overlay/s6-rc.d/user/contents.d/cwa-ingest-service -------------------------------------------------------------------------------- /root/etc/s6-overlay/s6-rc.d/user/contents.d/cwa-init: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crocodilestick/Calibre-Web-Automated/3746332a6d04e518f194551caa6363851e0e0e5d/root/etc/s6-overlay/s6-rc.d/user/contents.d/cwa-init -------------------------------------------------------------------------------- /root/etc/s6-overlay/s6-rc.d/user/contents.d/metadata-change-detector: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crocodilestick/Calibre-Web-Automated/3746332a6d04e518f194551caa6363851e0e0e5d/root/etc/s6-overlay/s6-rc.d/user/contents.d/metadata-change-detector -------------------------------------------------------------------------------- /root/etc/s6-overlay/s6-rc.d/user/contents.d/universal-calibre-setup: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crocodilestick/Calibre-Web-Automated/3746332a6d04e518f194551caa6363851e0e0e5d/root/etc/s6-overlay/s6-rc.d/user/contents.d/universal-calibre-setup -------------------------------------------------------------------------------- /scripts/auto_library.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | import shutil 4 | import sqlite3 5 | import sys 6 | 7 | 8 | def main(): 9 | auto_lib = AutoLibrary() 10 | auto_lib.check_for_app_db() 11 | if auto_lib.check_for_existing_library(): 12 | auto_lib.set_library_location() 13 | else: # No existing library found 14 | auto_lib.make_new_library() 15 | auto_lib.set_library_location() 16 | 17 | print(f"[cwa-auto-library] Library location successfully set to: {auto_lib.lib_path}") 18 | sys.exit(0) 19 | 20 | 21 | class AutoLibrary: 22 | def __init__(self): 23 | self.config_dir = "/config" 24 | self.library_dir = "/calibre-library" 25 | self.dirs_path = "/app/calibre-web-automated/dirs.json" 26 | self.app_db = "/config/app.db" 27 | 28 | self.empty_appdb = "/app/calibre-web-automated/empty_library/app.db" 29 | self.empty_metadb = "/app/calibre-web-automated/empty_library/metadata.db" 30 | 31 | self.metadb_path = None 32 | self.lib_path = None 33 | 34 | @property #getter 35 | def metadb_path(self): 36 | return self._metadb_path 37 | 38 | @metadb_path.setter 39 | def metadb_path(self, path): 40 | if path is None: 41 | self._metadb_path = None 42 | self.lib_path = None 43 | else: 44 | self._metadb_path = path 45 | self.lib_path = os.path.dirname(path) 46 | 47 | # Checks config_dir for an existing app.db, if one doesn't already exist it copies an empty one from /app/calibre-web-automated/empty_library/app.db and sets the permissions 48 | def check_for_app_db(self): 49 | files_in_config = [os.path.join(dirpath,f) for (dirpath, dirnames, filenames) in os.walk(self.config_dir) for f in filenames] 50 | db_files = [f for f in files_in_config if "app.db" in f] 51 | if len(db_files) == 0: 52 | print(f"[cwa-auto-library] No app.db found in {self.config_dir}, copying from /app/calibre-web-automated/empty_library/app.db") 53 | shutil.copyfile(self.empty_appdb, f"{self.config_dir}/app.db") 54 | os.system(f"chown -R abc:abc {self.config_dir}") 55 | print(f"[cwa-auto-library] app.db successfully copied to {self.config_dir}") 56 | else: 57 | return 58 | 59 | # Check for a metadata.db file in the given library dir and returns False if one doesn't exist 60 | # and True if one does exist, while also updating metadb_path to the path of the found metadata.db file 61 | # In the case of multiple metadata.db files, the user is notified and the one with the largest filesize is chosen 62 | def check_for_existing_library(self) -> bool: 63 | files_in_library = [os.path.join(dirpath,f) for (dirpath, dirnames, filenames) in os.walk(self.library_dir) for f in filenames] 64 | db_files = [f for f in files_in_library if "metadata.db" in f] 65 | if len(db_files) == 1: 66 | self.metadb_path = db_files[0] 67 | print(f"[cwa-auto-library]: Existing library found at {self.lib_path}, mounting now...") 68 | return True 69 | elif len(db_files) > 1: 70 | print("[cwa-auto-library]: Multiple metadata.db files found in library directory:\n") 71 | for db in db_files: 72 | print(f" - {db} | Size: {os.path.getsize(db)}") 73 | db_sizes = [os.path.getsize(f) for f in db_files] 74 | index_of_biggest_db = max(range(len(db_sizes)), key=db_sizes.__getitem__) 75 | self.metadb_path = db_files[index_of_biggest_db] 76 | print(f"\n[cwa-auto-library]: Automatically mounting the largest database using the following db file - {db_files[index_of_biggest_db]} ...") 77 | print("\n[cwa-auto-library]: If this is unwanted, please ensure only 1 metadata.db file / only your desired Calibre Database exists in '/calibre-library', then restart the container") 78 | return True 79 | else: 80 | return False 81 | 82 | # Sets the library's location in both dirs.json and the CW db 83 | def set_library_location(self): 84 | if self.metadb_path is not None and os.path.exists(self.metadb_path): 85 | self.update_dirs_json() 86 | self.update_calibre_web_db() 87 | return 88 | else: 89 | print("[cwa-auto-library]: ERROR: metadata.db found but not mounted") 90 | sys.exit(1) 91 | 92 | # Uses sql to update CW's app.db with the correct library location (config_calibre_dir in the settings table) 93 | def update_calibre_web_db(self): 94 | if os.path.exists(self.metadb_path): # type: ignore 95 | try: 96 | print("[cwa-auto-library]: Updating Settings Database with library location...") 97 | con = sqlite3.connect(self.app_db) 98 | cur = con.cursor() 99 | cur.execute(f'UPDATE settings SET config_calibre_dir="{self.lib_path}";') 100 | con.commit() 101 | return 102 | except Exception as e: 103 | print("[cwa-auto-library]: ERROR: Could not update Calibre Web Database") 104 | print(e) 105 | sys.exit(1) 106 | else: 107 | print(f"[cwa-auto-library]: ERROR: app.db in {self.app_db} not found") 108 | sys.exit(1) 109 | 110 | # Update the dirs.json file with the new library location (lib_path)) 111 | def update_dirs_json(self): 112 | """Updates the location of the calibre library stored in dirs.json with the found library""" 113 | try: 114 | print("[cwa-auto-library] Updating dirs.json with new library location...") 115 | with open(self.dirs_path) as f: 116 | dirs = json.load(f) 117 | dirs["calibre_library_dir"] = self.lib_path 118 | with open(self.dirs_path, 'w') as f: 119 | json.dump(dirs, f, indent=4) 120 | return 121 | except Exception as e: 122 | print("[cwa-auto-library]: ERROR: Could not update dirs.json") 123 | print(e) 124 | sys.exit(1) 125 | 126 | # Uses the empty metadata.db in /app/calibre-web-automated to create a new library 127 | def make_new_library(self): 128 | print("[cwa-auto-library]: No existing library found. Creating new library...") 129 | shutil.copyfile(self.empty_metadb, f"{self.library_dir}/metadata.db") 130 | os.system(f"chown -R abc:abc {self.library_dir}") 131 | self.metadb_path = f"{self.library_dir}/metadata.db" 132 | return 133 | 134 | 135 | if __name__ == '__main__': 136 | main() -------------------------------------------------------------------------------- /scripts/auto_zip.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | from os.path import isfile, join 4 | from datetime import datetime 5 | import pathlib 6 | from zipfile import ZipFile 7 | 8 | from cwa_db import CWA_DB 9 | 10 | class AutoZipper: 11 | def __init__(self): 12 | self.archive_dirs_stem = "/config/processed_books/" 13 | self.converted_dir = self.archive_dirs_stem + "converted/" 14 | self.failed_dir = self.archive_dirs_stem + "failed/" 15 | self.imported_dir = self.archive_dirs_stem + "imported/" 16 | self.fixed_originals_dir = self.archive_dirs_stem + "fixed_originals/" 17 | 18 | self.archive_dirs = [self.converted_dir, self.failed_dir, self.imported_dir, self.fixed_originals_dir] 19 | 20 | self.current_date = datetime.today().strftime('%Y-%m-%d') 21 | 22 | self.db = CWA_DB() 23 | self.cwa_settings = self.db.cwa_settings 24 | 25 | if self.cwa_settings["auto_zip_backups"]: 26 | self.to_zip = self.get_books_to_zip() 27 | else: 28 | print("[cwa-auto-zipper] Cancelling Auto-Zipper as the service is currently disabled in the cwa-settings panel. Exiting...") 29 | sys.exit(0) 30 | 31 | 32 | def last_mod_date(self, path_to_file) -> str: 33 | """ Returns the date a given file was last modified as a string """ 34 | 35 | stat = os.stat(path_to_file) 36 | return datetime.fromtimestamp(stat.st_mtime).strftime('%Y-%m-%d') #%H:%M:%S 37 | 38 | def get_books_to_zip(self) -> dict[str,list[str]]: 39 | """ Returns a dictionary with the books that are to be zipped together in each dir """ 40 | to_zip = {} 41 | for dir in self.archive_dirs: 42 | dir_name = dir.split('/')[-2] 43 | books = [f for f in os.listdir(dir) if isfile(join(dir, f)) and pathlib.Path(f).suffix != ".zip"] 44 | to_zip_in_dir = [] 45 | for book in books: 46 | if self.last_mod_date(dir + book) == self.current_date: 47 | to_zip_in_dir.append(dir + book) 48 | to_zip |= {dir_name:to_zip_in_dir} 49 | 50 | return to_zip 51 | 52 | def zip_todays_books(self) -> bool: 53 | """ Zips the files in self.to_zip for each respective dir together if new files are found in each. If no files are zipped, the bool returned is false. """ 54 | zip_indicator = False 55 | for dir in self.archive_dirs: 56 | dir_name = dir.split('/')[-2] 57 | if len(self.to_zip[dir_name]) > 0: 58 | zip_indicator = True 59 | with ZipFile(f'{self.archive_dirs_stem}{dir_name}/{self.current_date}-{dir_name}.zip', 'w') as zip: 60 | for file in self.to_zip[dir_name]: 61 | zip.write(file) 62 | 63 | return zip_indicator 64 | 65 | def remove_zipped_files(self) -> None: 66 | """ Deletes files following their successful compression """ 67 | for dir in self.archive_dirs: 68 | dir_name = dir.split('/')[-2] 69 | for file in self.to_zip[dir_name]: 70 | os.remove(file) 71 | 72 | def main(): 73 | try: 74 | zipper = AutoZipper() 75 | print(f"[cwa-auto-zipper] Successfully initiated, processing new files from {zipper.current_date}...\n") 76 | for dir in zipper.archive_dirs: 77 | dir_name = dir.split('/')[-2] 78 | if len(zipper.to_zip[dir_name]) > 0: 79 | print(f"[cwa-auto-zipper] {dir_name.title()} - {len(zipper.to_zip[dir_name])} file(s) found to zip.") 80 | else: 81 | print(f"[cwa-auto-zipper] {dir_name.title()} - no files found to zip.") 82 | except Exception as e: 83 | print(f"[cwa-auto-zipper] AutoZipper could not be initiated due to the following error:\n{e}") 84 | sys.exit(1) 85 | try: 86 | zip_indicator = zipper.zip_todays_books() 87 | if zip_indicator: 88 | print(f"\n[cwa-auto-zipper] All files from {zipper.current_date} successfully zipped! Removing zipped files...") 89 | else: 90 | print(f"\n[cwa-auto-zipper] No files from {zipper.current_date} found to be zipped. Exiting now...") 91 | sys.exit(0) 92 | except Exception as e: 93 | print(f"[cwa-auto-zipper] Files could not be automatically zipped due to the following error:\n{e} ") 94 | sys.exit(2) 95 | try: 96 | zipper.remove_zipped_files() 97 | print(f"[cwa-auto-zipper] All zipped files successfully removed!") 98 | except Exception as e: 99 | print(f"[cwa-auto-zipper] The following error occurred when trying to remove the zipped files:\n{e}") 100 | sys.exit(3) 101 | 102 | print(f"\n[cwa-auto-zipper] Files from {zipper.current_date} successfully processed! Exiting now...") 103 | 104 | 105 | if __name__ == "__main__": 106 | main() -------------------------------------------------------------------------------- /scripts/check-cwa-services.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | RED='\033[0;31m' 4 | GREEN='\033[0;32m' 5 | NC='\033[0m' # No Color 6 | 7 | # Print promt title 8 | echo "====== Calibre-Web Automated -- Status of Monitoring Services ======" 9 | echo "" 10 | 11 | 12 | if s6-rc -a list | grep -q 'cwa-ingest-service'; then 13 | echo -e "- cwa-ingest-service ${GREEN}is running${NC}" 14 | is=true 15 | else 16 | echo -e "- cwa-ingest-service ${RED}is not running${NC}" 17 | is=false 18 | fi 19 | 20 | if s6-rc -a list | grep -q 'metadata-change-detector'; then 21 | echo -e "- metadata-change-detector ${GREEN}is running${NC}" 22 | mc=true 23 | else 24 | echo -e "- metadata-change-detector ${RED}is not running${NC}" 25 | mc=false 26 | fi 27 | 28 | echo "" 29 | 30 | if $is && $mc; then 31 | echo -e "Calibre-Web-Automated was ${GREEN}successfully installed ${NC}and ${GREEN}is running properly!${NC}" 32 | exit 0 33 | else 34 | echo -e "Calibre-Web-Automated was ${RED}not installed successfully${NC}, please check the logs for more information." 35 | if [ "$is" = true ] && [ "$mc" = false ] ; then 36 | exit 1 37 | elif [ "$is" = false ] && [ "$mc" = true ] ; then 38 | exit 2 39 | else 40 | exit 3 41 | fi 42 | fi -------------------------------------------------------------------------------- /scripts/cwa_schema.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE IF NOT EXISTS cwa_enforcement( 2 | id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, 3 | timestamp TEXT NOT NULL, 4 | book_id INTEGER NOT NULL, 5 | book_title TEXT NOT NULL, 6 | author TEXT NOT NULL, 7 | file_path TEXT NOT NULL, 8 | trigger_type TEXT NOT NULL 9 | ); 10 | CREATE TABLE IF NOT EXISTS cwa_import( 11 | id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, 12 | timestamp TEXT NOT NULL, 13 | filename TEXT NOT NULL, 14 | original_backed_up TEXT NOT NULL 15 | ); 16 | CREATE TABLE IF NOT EXISTS cwa_conversions( 17 | id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, 18 | timestamp TEXT NOT NULL, 19 | filename TEXT NOT NULL, 20 | original_format TEXT NOT NULL, 21 | original_backed_up TEXT NOT NULL, 22 | end_format TEXT DEFAULT "" NOT NULL 23 | ); 24 | CREATE TABLE IF NOT EXISTS epub_fixes( 25 | id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, 26 | timestamp TEXT NOT NULL, 27 | filename TEXT NOT NULL, 28 | manually_triggered TEXT NOT NULL, 29 | num_of_fixes_applied TEXT NOT NULL, 30 | original_backed_up TEXT NOT NULL, 31 | file_path TEXT NOT NULL, 32 | fixes_applied TEXT DEFAULT "" 33 | ); 34 | CREATE TABLE IF NOT EXISTS cwa_settings( 35 | default_settings SMALLINT DEFAULT 1 NOT NULL, 36 | auto_backup_imports SMALLINT DEFAULT 1 NOT NULL, 37 | auto_backup_conversions SMALLINT DEFAULT 1 NOT NULL, 38 | auto_zip_backups SMALLINT DEFAULT 1 NOT NULL, 39 | cwa_update_notifications SMALLINT DEFAULT 1 NOT NULL, 40 | auto_convert SMALLINT DEFAULT 1 NOT NULL, 41 | auto_convert_target_format TEXT DEFAULT "epub" NOT NULL, 42 | auto_convert_ignored_formats TEXT DEFAULT "" NOT NULL, 43 | auto_ingest_ignored_formats TEXT DEFAULT "" NOT NULL, 44 | auto_metadata_enforcement SMALLINT DEFAULT 1 NOT NULL, 45 | kindle_epub_fixer SMALLINT DEFAULT 1 NOT NULL, 46 | auto_backup_epub_fixes SMALLINT DEFAULT 1 NOT NULL 47 | ); -------------------------------------------------------------------------------- /scripts/setup-cwa.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Make required directories and files for metadata enforcement 4 | make_dirs () { 5 | install -d -o abc -g abc /app/calibre-web-automated/metadata_change_logs 6 | install -d -o abc -g abc /app/calibre-web-automated/metadata_temp 7 | install -d -o abc -g abc /cwa-book-ingest 8 | install -d -o abc -g abc /calibre-library 9 | } 10 | 11 | # Change ownership & permissions as required 12 | change_script_permissions () { 13 | chmod +x /etc/s6-overlay/s6-rc.d/cwa-auto-library/run 14 | chmod +x /etc/s6-overlay/s6-rc.d/cwa-auto-zipper/run 15 | chmod +x /etc/s6-overlay/s6-rc.d/cwa-ingest-service/run 16 | chmod +x /etc/s6-overlay/s6-rc.d/cwa-init/run 17 | chmod +x /etc/s6-overlay/s6-rc.d/metadata-change-detector/run 18 | chmod +x /etc/s6-overlay/s6-rc.d/universal-calibre-setup/run 19 | chmod +x /app/calibre-web-automated/scripts/check-cwa-services.sh 20 | chmod 775 /app/calibre-web/cps/editbooks.py 21 | chmod 775 /app/calibre-web/cps/admin.py 22 | } 23 | 24 | # Add aliases to .bashrc 25 | add_aliases () { 26 | echo "" | cat >> ~/.bashrc 27 | echo "# Calibre-Web Automated Aliases" | cat >> ~/.bashrc 28 | echo "alias cwa-check='bash /app/calibre-web-automated/scripts/check-cwa-services.sh'" | cat >> ~/.bashrc 29 | echo "alias cwa-change-dirs='nano /app/calibre-web-automated/dirs.json'" | cat >> ~/.bashrc 30 | 31 | echo "cover-enforcer () {" | cat >> ~/.bashrc 32 | echo ' python3 /app/calibre-web-automated/scripts/cover_enforcer.py "$@"' | cat >> ~/.bashrc 33 | echo "}" | cat >> ~/.bashrc 34 | 35 | echo "convert-library () {" | cat >> ~/.bashrc 36 | echo ' python3 /app/calibre-web-automated/scripts/convert_library.py "$@"' | cat >> ~/.bashrc 37 | echo "}" | cat >> ~/.bashrc 38 | 39 | source ~/.bashrc 40 | } 41 | 42 | echo "Running docker image setup script..." 43 | make_dirs 44 | change_script_permissions 45 | add_aliases --------------------------------------------------------------------------------