diff --git a/.github/workflows/build-and-publish-docker-image.yml b/.github/workflows/build-and-publish-docker-image.yml index c7b6277a..05e5c7de 100644 --- a/.github/workflows/build-and-publish-docker-image.yml +++ b/.github/workflows/build-and-publish-docker-image.yml @@ -8,7 +8,7 @@ on: workflow_dispatch: env: REGISTRY: ghcr.io - IMAGE_NAME: ${{ github.repository }} + IMAGE_NAME: ${{ github.repository_owner }}/shelfmark jobs: build-and-push-images: runs-on: ubuntu-latest @@ -20,15 +20,9 @@ jobs: strategy: matrix: include: - - suffix: "" - target: cwa-bd - image_name_suffix: "" - - suffix: "-tor" - target: cwa-bd-tor - image_name_suffix: "-tor" - - suffix: "-extbp" - target: cwa-bd-extbp - image_name_suffix: "-extbp" + - target: shelfmark + - target: shelfmark-lite + image_name_suffix: "-lite" steps: - name: Get current date id: date @@ -80,4 +74,68 @@ jobs: with: subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}${{ matrix.image_name_suffix }} subject-digest: ${{ steps.push.outputs.digest }} - push-to-registry: true \ No newline at end of file + push-to-registry: true + + # Create aliases for backwards compatibility + create-aliases: + needs: build-and-push-images + runs-on: ubuntu-latest + if: github.event_name != 'pull_request' + env: + # Legacy name for backwards compatibility (hardcoded so it works after rename) + LEGACY_NAME: calibre-web-automated-book-downloader + steps: + - name: Log in to registry + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Create legacy aliases + run: | + # Current image names (follows repo name) + STANDARD="${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}" + LITE="${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}-lite" + + # Legacy image names (hardcoded for backwards compatibility) + LEGACY="${{ env.REGISTRY }}/${{ github.repository_owner }}/${{ env.LEGACY_NAME }}" + LEGACY_TOR="${LEGACY}-tor" + LEGACY_EXTBP="${LEGACY}-extbp" + + SHA_SHORT=$(echo "${{ github.sha }}" | cut -c1-7) + + # Helper function to create alias with all standard tags + create_alias() { + local SOURCE=$1 + local ALIAS=$2 + + # Always create SHA tag + docker buildx imagetools create -t "${ALIAS}:sha-${SHA_SHORT}" "${SOURCE}:sha-${SHA_SHORT}" + + if [[ "${{ github.ref }}" == refs/tags/v* ]]; then + VERSION="${{ github.ref_name }}" + VERSION_NUM="${VERSION#v}" + MINOR="${VERSION_NUM%.*}" + + docker buildx imagetools create -t "${ALIAS}:latest" "${SOURCE}:latest" + docker buildx imagetools create -t "${ALIAS}:${VERSION_NUM}" "${SOURCE}:${VERSION_NUM}" + docker buildx imagetools create -t "${ALIAS}:${MINOR}" "${SOURCE}:${MINOR}" + docker buildx imagetools create -t "${ALIAS}:${VERSION}" "${SOURCE}:${VERSION}" + else + docker buildx imagetools create -t "${ALIAS}:dev" "${SOURCE}:dev" + fi + } + + # Create legacy aliases pointing to current images + # calibre-web-automated-book-downloader → standard image + create_alias "${STANDARD}" "${LEGACY}" + + # calibre-web-automated-book-downloader-tor → standard image + create_alias "${STANDARD}" "${LEGACY_TOR}" + + # calibre-web-automated-book-downloader-extbp → lite image + create_alias "${LITE}" "${LEGACY_EXTBP}" \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json index 339d2a3f..545cc4cc 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -2,17 +2,17 @@ "version": "0.2.0", "configurations": [ { - "name": "Python Debugger: Current CWABD File", + "name": "Python Debugger: Current Shelfmark File", "type": "debugpy", "request": "launch", "program": "${file}", "console": "integratedTerminal", "justMyCode": false, "env": { - "INGEST_DIR": "/tmp/cwa-book-downloader", - "TEMP_DIR": "/tmp/cwa-book-downloader", + "INGEST_DIR": "/tmp/shelfmark", + "TEMP_DIR": "/tmp/shelfmark", "LOG_LEVEL": "DEBUG", - "LOG_ROOT": "/tmp/cwa-book-downloader", + "LOG_ROOT": "/tmp/shelfmark", "ENABLE_LOGGING": "true", "DOCKERMODE": "false", "DEBUG": "true", @@ -27,7 +27,7 @@ "preLaunchTask": "docker-compose up (dev)", // Spin up dev containers "postDebugTask": "docker-compose down (dev)", // Optional: tear them down "env": { - "INGEST_DIR": "/tmp/cwa-book-downloader" + "INGEST_DIR": "/tmp/shelfmark" }, }, { @@ -38,7 +38,7 @@ "preLaunchTask": "docker-compose up (prod)", "postDebugTask": "docker-compose down (prod)", "env": { - "INGEST_DIR": "/tmp/cwa-book-downloader" + "INGEST_DIR": "/tmp/shelfmark" }, }, { @@ -54,9 +54,9 @@ ], "compounds": [ { - "name": "Launch CWA-BD", + "name": "Launch Shelfmark", "configurations": [ - "Launch cwa-bd app.py", + "Launch Shelfmark app.py", "Launch Browser" ] } diff --git a/Dockerfile b/Dockerfile index 9ace9725..58294c87 100644 --- a/Dockerfile +++ b/Dockerfile @@ -45,7 +45,7 @@ ENV DEBIAN_FRONTEND=noninteractive \ PIP_NO_CACHE_DIR=1 \ PIP_DISABLE_PIP_VERSION_CHECK=1 \ PIP_DEFAULT_TIMEOUT=100 \ - NAME=Calibre-Web-Automated-Book-Downloader \ + NAME=Shelfmark \ PYTHONPATH=/app \ # PUID/PGID will be handled by entrypoint script, but TZ/Locale are still needed LANG=en_US.UTF-8 \ @@ -68,7 +68,14 @@ RUN apt-get update && \ # For debug zip iputils-ping \ # For user switching - sudo && \ + sudo \ + # --- Tor support (activated via USING_TOR=true) --- + tor \ + supervisor \ + iptables && \ + # Configure iptables alternatives for tor.sh compatibility + update-alternatives --set iptables /usr/sbin/iptables-legacy && \ + update-alternatives --set ip6tables /usr/sbin/ip6tables-legacy && \ # Cleanup APT cache *after* all installs in this layer apt-get purge -y --auto-remove -o APT::AutoRemove::RecommendsImportant=false && \ apt-get clean && \ @@ -87,7 +94,7 @@ WORKDIR /app # Install Python dependencies using pip # Copying requirements files separately leverages build cache # Cache mount persists pip cache between builds for faster installs -COPY requirements-base.txt requirements-cwa-bd.txt ./ +COPY requirements-base.txt requirements-shelfmark.txt ./ RUN --mount=type=cache,target=/root/.cache/pip \ pip install -r requirements-base.txt @@ -100,7 +107,7 @@ COPY --from=frontend-builder /frontend/dist /app/frontend-dist # Final setup: permissions and directories in one layer # Only creating directories and setting executable bits. # Ownership will be handled by the entrypoint script. -RUN mkdir -p /var/log/cwa-book-downloader /books && \ +RUN mkdir -p /var/log/shelfmark /books && \ chmod +x /app/entrypoint.sh /app/tor.sh /app/genDebug.sh # Expose the application port @@ -115,7 +122,7 @@ HEALTHCHECK --interval=60s --timeout=60s --start-period=60s --retries=3 \ ENTRYPOINT ["/usr/bin/dumb-init", "--"] -FROM base AS cwa-bd +FROM base AS shelfmark RUN apt-get update && \ apt-get install -y --no-install-recommends \ @@ -140,7 +147,7 @@ RUN apt-get update && \ # Install additional dependencies (requirements file already copied in base stage) RUN --mount=type=cache,target=/root/.cache/pip \ - pip install -r requirements-cwa-bd.txt + pip install -r requirements-shelfmark.txt # Grant read/execute permissions to others RUN chmod -R o+rx /usr/bin/chromium && \ @@ -150,30 +157,7 @@ RUN chmod -R o+rx /usr/bin/chromium && \ # Default command to run the application entrypoint script CMD ["/app/entrypoint.sh"] -FROM cwa-bd AS cwa-bd-tor - -ENV USING_TOR=true - -# Install Tor and dependencies -RUN apt-get update && \ - apt-get install -y --no-install-recommends \ - # --- Tor --- - tor \ - # --- Supervisor --- - supervisor \ - # --- iptables --- - iptables && \ - update-alternatives --set iptables /usr/sbin/iptables-legacy && \ - update-alternatives --set ip6tables /usr/sbin/ip6tables-legacy && \ - # Cleanup APT cache *after* all installs in this layer - apt-get purge -y --auto-remove -o APT::AutoRemove::RecommendsImportant=false && \ - apt-get clean && \ - rm -rf /var/lib/apt/lists/* - -# Override the default command to run Tor -CMD ["/app/entrypoint.sh"] - -FROM base AS cwa-bd-extbp +FROM base AS shelfmark-lite ENV USING_EXTERNAL_BYPASSER=true diff --git a/README_images/downloads.png b/README_images/downloads.png index 3ec8c4b3..67993b9a 100644 Binary files a/README_images/downloads.png and b/README_images/downloads.png differ diff --git a/README_images/homescreen.png b/README_images/homescreen.png index 258d4c11..2d6ed1ae 100644 Binary files a/README_images/homescreen.png and b/README_images/homescreen.png differ diff --git a/README_images/multi-source.png b/README_images/multi-source.png index 112933bc..ceb65890 100644 Binary files a/README_images/multi-source.png and b/README_images/multi-source.png differ diff --git a/README_images/search-results.png b/README_images/search-results.png index 3d6353c6..2501ee66 100644 Binary files a/README_images/search-results.png and b/README_images/search-results.png differ diff --git a/compose/edge/docker-compose.extbp.yml b/compose/edge/docker-compose.lite.yml similarity index 83% rename from compose/edge/docker-compose.extbp.yml rename to compose/edge/docker-compose.lite.yml index fa1a25a9..580ef110 100644 --- a/compose/edge/docker-compose.extbp.yml +++ b/compose/edge/docker-compose.lite.yml @@ -1,7 +1,7 @@ # Uses external Cloudflare bypasser (FlareSolverr/ByParr) instead of built-in Selenium services: - calibre-web-automated-book-downloader-extbp: - image: ghcr.io/calibrain/calibre-web-automated-book-downloader-extbp:dev + shelfmark-lite: + image: ghcr.io/calibrain/shelfmark-lite:dev environment: # TZ: America/New_York EXT_BYPASSER_URL: http://flaresolverr:8191 diff --git a/compose/edge/docker-compose.tor.yml b/compose/edge/docker-compose.tor.yml index fba34fd5..7613e339 100644 --- a/compose/edge/docker-compose.tor.yml +++ b/compose/edge/docker-compose.tor.yml @@ -1,7 +1,7 @@ # Routes all traffic through Tor - requires NET_ADMIN capability services: - calibre-web-automated-book-downloader-tor: - image: ghcr.io/calibrain/calibre-web-automated-book-downloader-tor:dev + shelfmark-tor: + image: ghcr.io/calibrain/shelfmark:dev environment: FLASK_PORT: 8084 # TZ: America/New_York diff --git a/compose/edge/docker-compose.yml b/compose/edge/docker-compose.yml index 0fe3075e..dc919057 100644 --- a/compose/edge/docker-compose.yml +++ b/compose/edge/docker-compose.yml @@ -1,7 +1,7 @@ services: - calibre-web-automated-book-downloader: - image: ghcr.io/calibrain/calibre-web-automated-book-downloader:dev - container_name: calibre-web-automated-book-downloader + shelfmark: + image: ghcr.io/calibrain/shelfmark:dev + container_name: shelfmark environment: # TZ: America/New_York # PUID: 1000 diff --git a/compose/stable/docker-compose.extbp.yml b/compose/stable/docker-compose.extbp.yml deleted file mode 100644 index e0c992fb..00000000 --- a/compose/stable/docker-compose.extbp.yml +++ /dev/null @@ -1,20 +0,0 @@ -# Uses external Cloudflare bypasser (FlareSolverr/ByParr) instead of built-in Selenium -services: - calibre-web-automated-book-downloader-extbp: - image: ghcr.io/calibrain/calibre-web-automated-book-downloader-extbp:latest - environment: - TZ: America/New_York - EXT_BYPASSER_URL: http://flaresolverr:8191 - # UID: 1000 - # GID: 100 - # CWA_DB_PATH: /auth/app.db - ports: - - 8084:8084 - restart: unless-stopped - volumes: - - /tmp/data/calibre-web/ingest:/cwa-book-ingest - - /path/to/config:/config - # - /cwa/config/path/app.db:/auth/app.db:ro - - flaresolverr: - image: ghcr.io/flaresolverr/flaresolverr:latest diff --git a/compose/stable/docker-compose.lite.yml b/compose/stable/docker-compose.lite.yml new file mode 100644 index 00000000..e146fe91 --- /dev/null +++ b/compose/stable/docker-compose.lite.yml @@ -0,0 +1,16 @@ +services: + shelfmark-lite: + image: ghcr.io/calibrain/shelfmark-lite:latest + environment: + # TZ: America/New_York + # EXT_BYPASSER_URL: http://flaresolverr:8191 #If using Flaresolverr + # PUID: 1000 + # PGID: 1000 + ports: + - 8084:8084 + restart: unless-stopped + volumes: + - /path/to/books:/books # Book destination directory + - /path/to/config:/config # App configuration + # Download client mount - path must match your torrent/usenet client's volume exactly + # - /path/to/downloads:/path/to/downloads \ No newline at end of file diff --git a/compose/stable/docker-compose.tor.yml b/compose/stable/docker-compose.tor.yml index fbde64fc..4fef788f 100644 --- a/compose/stable/docker-compose.tor.yml +++ b/compose/stable/docker-compose.tor.yml @@ -1,12 +1,13 @@ # Routes all traffic through Tor - requires NET_ADMIN capability services: - calibre-web-automated-book-downloader-tor: - image: ghcr.io/calibrain/calibre-web-automated-book-downloader-tor:latest + shelfmark-tor: + image: ghcr.io/calibrain/shelfmark:latest environment: FLASK_PORT: 8084 - TZ: America/New_York + # TZ: America/New_York USING_TOR: true - # CWA_DB_PATH: /auth/app.db + # PUID: 1000 + # PGID: 1000 cap_add: - NET_ADMIN - NET_RAW @@ -14,6 +15,7 @@ services: - 8084:8084 restart: unless-stopped volumes: - - /tmp/data/calibre-web/ingest:/cwa-book-ingest - - /path/to/config:/config - # - /cwa/config/path/app.db:/auth/app.db:ro + - /path/to/books:/books # Book destination directory + - /path/to/config:/config # App configuration + # Download client mount - path must match your torrent/usenet client's volume exactly + # - /path/to/downloads:/path/to/downloads diff --git a/compose/stable/docker-compose.yml b/compose/stable/docker-compose.yml index ed006963..f363738c 100644 --- a/compose/stable/docker-compose.yml +++ b/compose/stable/docker-compose.yml @@ -1,15 +1,16 @@ services: - calibre-web-automated-book-downloader: - image: ghcr.io/calibrain/calibre-web-automated-book-downloader:latest - container_name: calibre-web-automated-book-downloader + shelfmark: + image: ghcr.io/calibrain/shelfmark:latest + container_name: shelfmark environment: - TZ: America/New_York - # UID: 1000 - # GID: 100 - # CWA_DB_PATH: /auth/app.db + # TZ: America/New_York + # PUID: 1000 + # PGID: 1000 ports: - 8084:8084 restart: unless-stopped volumes: - - /tmp/data/calibre-web/ingest:/cwa-book-ingest # This is where the books will be downloaded and ingested by your book management application - - /path/to/config:/config # Configuration files and database + - /path/to/books:/books # Book destination directory + - /path/to/config:/config # App configuration + # Download client mount - path must match your torrent/usenet client's volume exactly + # - /path/to/downloads:/path/to/downloads diff --git a/cwa_book_downloader/__init__.py b/cwa_book_downloader/__init__.py deleted file mode 100644 index d9948443..00000000 --- a/cwa_book_downloader/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""CWA Book Downloader - book search and download service.""" diff --git a/cwa_book_downloader/__main__.py b/cwa_book_downloader/__main__.py deleted file mode 100644 index f41dbde4..00000000 --- a/cwa_book_downloader/__main__.py +++ /dev/null @@ -1,8 +0,0 @@ -"""Package entry point for `python -m cwa_book_downloader`.""" - -from cwa_book_downloader.main import app, socketio -from cwa_book_downloader.config.env import FLASK_HOST, FLASK_PORT -from cwa_book_downloader.core.config import config - -if __name__ == "__main__": - socketio.run(app, host=FLASK_HOST, port=FLASK_PORT, debug=config.get("DEBUG", False)) diff --git a/cwa_book_downloader/core/__init__.py b/cwa_book_downloader/core/__init__.py deleted file mode 100644 index 07873a33..00000000 --- a/cwa_book_downloader/core/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -"""Core module - shared models, queue, and utilities.""" - -from cwa_book_downloader.core.models import BookInfo, QueueItem, SearchFilters, QueueStatus -from cwa_book_downloader.core.queue import BookQueue, book_queue -from cwa_book_downloader.core.logger import setup_logger diff --git a/cwa_book_downloader/release_sources/irc/__init__.py b/cwa_book_downloader/release_sources/irc/__init__.py deleted file mode 100644 index d46531eb..00000000 --- a/cwa_book_downloader/release_sources/irc/__init__.py +++ /dev/null @@ -1,14 +0,0 @@ -"""IRC Highway release source plugin. - -This plugin enables searching and downloading ebooks from IRC Highway's -#ebooks channel using the DCC (Direct Client-to-Client) protocol. - -Special thanks to the OpenBooks project (https://github.com/evan-buss/openbooks), -an MIT-licensed IRC ebook downloader written in Go, for serving as inspiration for the -high-level design and reference for IRC Highway best practices, DCC protocol handling, and result parsing. -""" - -# Import to trigger decorator registration -from cwa_book_downloader.release_sources.irc import source # noqa: F401 -from cwa_book_downloader.release_sources.irc import handler # noqa: F401 -from cwa_book_downloader.release_sources.irc import settings # noqa: F401 diff --git a/cwa_book_downloader/release_sources/irc/settings.py b/cwa_book_downloader/release_sources/irc/settings.py deleted file mode 100644 index b1978fe2..00000000 --- a/cwa_book_downloader/release_sources/irc/settings.py +++ /dev/null @@ -1,59 +0,0 @@ -"""IRC settings registration. - -Registers IRC Highway settings for the settings UI. -""" - -from cwa_book_downloader.core.settings_registry import ( - CheckboxField, - HeadingField, - TextField, - register_settings, -) - - -@register_settings( - name="irc", - display_name="IRC Highway", - icon="download", - order=56, -) -def irc_settings(): - """Define IRC source settings.""" - return [ - HeadingField( - key="heading", - title="IRC Highway", - description=( - "Search and download books from IRC Highway #ebooks channel. " - "This source connects via IRC and uses DCC for file transfers. " - "Note: DCC requires direct TCP connections to arbitrary ports, " - "which may not work behind strict firewalls or NAT." - ), - ), - - CheckboxField( - key="IRC_ENABLED", - label="Enable IRC source", - default=False, - description="Enable searching and downloading from IRC Highway", - ), - - TextField( - key="IRC_NICK", - label="Nickname", - placeholder="Leave empty for random", - description="Your IRC nickname. Leave empty to generate a random one.", - env_supported=True, - show_when={"field": "IRC_ENABLED", "value": True}, - ), - - TextField( - key="IRC_SEARCH_BOT", - label="Search bot", - placeholder="search", - default="search", - description="The search bot to query (usually 'search' or 'searchook')", - env_supported=True, - show_when={"field": "IRC_ENABLED", "value": True}, - ), - ] diff --git a/docker-compose.dev.extbp.yml b/docker-compose.dev.extbp.yml index e9f4dad1..12abd648 100644 --- a/docker-compose.dev.extbp.yml +++ b/docker-compose.dev.extbp.yml @@ -1,13 +1,13 @@ -# Local development - External bypasser variant +# Local development - External bypasser variant (lite) services: - calibre-web-automated-book-downloader-extbp-dev: + shelfmark-lite-dev: extends: file: ./compose/edge/docker-compose.extbp.yml - service: calibre-web-automated-book-downloader-extbp + service: shelfmark-lite build: context: . dockerfile: Dockerfile - target: cwa-bd-extbp + target: shelfmark-lite environment: DEBUG: true EXT_BYPASSER_URL: http://flaresolverr:8191 @@ -16,8 +16,8 @@ services: volumes: - ./.local/config:/config - ./.local/books:/books - - ./.local/log:/var/log/cwa-book-downloader - - ./.local/tmp:/tmp/cwa-book-downloader + - ./.local/log:/var/log/shelfmark + - ./.local/tmp:/tmp/shelfmark # Download client mount - path must match your torrent/usenet client's volume exactly # - /path/to/downloads:/path/to/downloads diff --git a/docker-compose.dev.tor.yml b/docker-compose.dev.tor.yml index a2fa10ef..9035beb8 100644 --- a/docker-compose.dev.tor.yml +++ b/docker-compose.dev.tor.yml @@ -1,19 +1,20 @@ # Local development - Tor variant services: - calibre-web-automated-book-downloader-tor-dev: + shelfmark-tor-dev: extends: file: ./compose/edge/docker-compose.tor.yml - service: calibre-web-automated-book-downloader-tor + service: shelfmark-tor build: context: . dockerfile: Dockerfile - target: cwa-bd-tor + target: shelfmark environment: DEBUG: true + USING_TOR: true volumes: - ./.local/config:/config - ./.local/books:/books - - ./.local/log:/var/log/cwa-book-downloader - - ./.local/tmp:/tmp/cwa-book-downloader + - ./.local/log:/var/log/shelfmark + - ./.local/tmp:/tmp/shelfmark # Download client mount - path must match your torrent/usenet client's volume exactly # - /path/to/downloads:/path/to/downloads diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index 00492ed2..9a78b460 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -1,13 +1,13 @@ # Local development - builds from source with debug enabled services: - calibre-web-automated-book-downloader-dev: + shelfmark-dev: extends: file: ./compose/edge/docker-compose.yml - service: calibre-web-automated-book-downloader + service: shelfmark build: context: . dockerfile: Dockerfile - target: cwa-bd + target: shelfmark cap_add: - SYS_PTRACE environment: @@ -15,8 +15,8 @@ services: volumes: - ./.local/config:/config - ./.local/books:/books - - ./.local/log:/var/log/cwa-book-downloader - - ./.local/tmp:/tmp/cwa-book-downloader - - ./cwa_book_downloader:/app/cwa_book_downloader:ro + - ./.local/log:/var/log/shelfmark + - ./.local/tmp:/tmp/shelfmark + - ./shelfmark:/app/shelfmark:ro # Download client mount - path must match your torrent/usenet client's volume exactly # - /path/to/downloads:/path/to/downloads diff --git a/docs/plugin-settings.md b/docs/plugin-settings.md index 17beffbc..a8aab0d6 100644 --- a/docs/plugin-settings.md +++ b/docs/plugin-settings.md @@ -16,7 +16,7 @@ The settings system uses a decorator-based registration pattern. Plugins registe Add settings to your plugin in 3 steps: ```python -from cwa_book_downloader.core.settings_registry import ( +from shelfmark.core.settings_registry import ( register_settings, TextField, PasswordField, @@ -249,7 +249,7 @@ The field will be greyed out with the specified reason when the condition is met Register a group to organize related settings tabs in the sidebar: ```python -from cwa_book_downloader.core.settings_registry import register_group +from shelfmark.core.settings_registry import register_group # Register a group (do this once, usually in a central config file) register_group( @@ -289,7 +289,7 @@ The `general` tab uses `CONFIG_DIR/settings.json` instead of the plugins subdire Use the `config` singleton to read setting values in your plugin code: ```python -from cwa_book_downloader.core.config import config +from shelfmark.core.config import config # Get a setting value with default fallback api_key = config.get("MY_PLUGIN_API_KEY", "") @@ -312,13 +312,13 @@ The config singleton: Here's a complete example for a metadata provider plugin: ```python -# cwa_book_downloader/metadata_providers/my_provider.py +# shelfmark/metadata_providers/my_provider.py -from cwa_book_downloader.metadata_providers.base import ( +from shelfmark.metadata_providers.base import ( MetadataProvider, register_provider, ) -from cwa_book_downloader.core.settings_registry import ( +from shelfmark.core.settings_registry import ( register_settings, HeadingField, TextField, @@ -326,7 +326,7 @@ from cwa_book_downloader.core.settings_registry import ( CheckboxField, ActionButton, ) -from cwa_book_downloader.core.config import config +from shelfmark.core.config import config def _test_connection(): @@ -422,15 +422,15 @@ class MyProvider(MetadataProvider): Here's a complete example for a release source plugin: ```python -# cwa_book_downloader/release_sources/my_source.py +# shelfmark/release_sources/my_source.py -from cwa_book_downloader.release_sources.base import ( +from shelfmark.release_sources.base import ( ReleaseSource, DownloadHandler, register_source, register_handler, ) -from cwa_book_downloader.core.settings_registry import ( +from shelfmark.core.settings_registry import ( register_settings, HeadingField, TextField, @@ -439,7 +439,7 @@ from cwa_book_downloader.core.settings_registry import ( SelectField, ActionButton, ) -from cwa_book_downloader.core.config import config +from shelfmark.core.config import config def _test_source(): diff --git a/docs/release-sources-plugin-guide.md b/docs/release-sources-plugin-guide.md index 5fe85302..55cc7bce 100644 --- a/docs/release-sources-plugin-guide.md +++ b/docs/release-sources-plugin-guide.md @@ -1,6 +1,6 @@ # Release Sources Plugin Development Guide -This guide explains how to create custom release source plugins for the CWA Book Downloader. The plugin system allows you to add new sources for searching and downloading books while integrating seamlessly with the existing queue, progress reporting, and settings infrastructure. +This guide explains how to create custom release source plugins for the Shelfmark. The plugin system allows you to add new sources for searching and downloading books while integrating seamlessly with the existing queue, progress reporting, and settings infrastructure. ## Table of Contents @@ -77,21 +77,21 @@ class MyHandler(DownloadHandler): ... ## Quick Start -Create a new file at `cwa_book_downloader/release_sources/my_source.py`: +Create a new file at `shelfmark/release_sources/my_source.py`: ```python from typing import Callable, List, Optional from threading import Event -from cwa_book_downloader.release_sources import ( +from shelfmark.release_sources import ( Release, ReleaseSource, DownloadHandler, register_source, register_handler, ) -from cwa_book_downloader.metadata_providers import BookMetadata -from cwa_book_downloader.core.models import DownloadTask +from shelfmark.metadata_providers import BookMetadata +from shelfmark.core.models import DownloadTask @register_source("my_source") @@ -123,11 +123,11 @@ class MyHandler(DownloadHandler): return False # Cancellation via cancel_flag ``` -Register the import in `cwa_book_downloader/release_sources/__init__.py`: +Register the import in `shelfmark/release_sources/__init__.py`: ```python # At the bottom of the file -from cwa_book_downloader.release_sources import my_source # noqa: F401, E402 +from shelfmark.release_sources import my_source # noqa: F401, E402 ``` --- @@ -139,8 +139,8 @@ The `ReleaseSource` abstract base class defines the search interface: ```python from abc import ABC, abstractmethod from typing import List -from cwa_book_downloader.metadata_providers import BookMetadata -from cwa_book_downloader.release_sources import Release, ReleaseColumnConfig +from shelfmark.metadata_providers import BookMetadata +from shelfmark.release_sources import Release, ReleaseColumnConfig class ReleaseSource(ABC): """Interface for searching a release source.""" @@ -237,7 +237,7 @@ The `DownloadHandler` abstract base class defines the download interface: from abc import ABC, abstractmethod from typing import Callable, Optional from threading import Event -from cwa_book_downloader.core.models import DownloadTask +from shelfmark.core.models import DownloadTask class DownloadHandler(ABC): """Interface for executing downloads from a source.""" @@ -458,7 +458,7 @@ class BookMetadata: ### Decorators ```python -from cwa_book_downloader.release_sources import register_source, register_handler +from shelfmark.release_sources import register_source, register_handler @register_source("my_source") class MySource(ReleaseSource): @@ -472,7 +472,7 @@ class MyHandler(DownloadHandler): ### Registry Functions ```python -from cwa_book_downloader.release_sources import ( +from shelfmark.release_sources import ( get_source, get_handler, list_available_sources, @@ -497,8 +497,8 @@ available = list_available_sources() ```python # At the bottom of __init__.py -from cwa_book_downloader.release_sources import direct_download # noqa: F401, E402 -from cwa_book_downloader.release_sources import my_source # noqa: F401, E402 +from shelfmark.release_sources import direct_download # noqa: F401, E402 +from shelfmark.release_sources import my_source # noqa: F401, E402 ``` The `noqa` comments suppress linter warnings about unused imports. @@ -510,7 +510,7 @@ The `noqa` comments suppress linter warnings about unused imports. Register plugin settings using the settings registry decorator: ```python -from cwa_book_downloader.core.settings_registry import ( +from shelfmark.core.settings_registry import ( register_settings, HeadingField, TextField, @@ -621,7 +621,7 @@ def test_my_source_connection(): ### Reading Settings ```python -from cwa_book_downloader.core.settings_registry import ( +from shelfmark.core.settings_registry import ( get_setting_value, is_value_from_env, load_config_file, @@ -658,7 +658,7 @@ When a value comes from an ENV var, the UI shows a "locked" badge and the field Customize how releases are displayed in the release modal by overriding `get_column_config()`: ```python -from cwa_book_downloader.release_sources import ( +from shelfmark.release_sources import ( ReleaseColumnConfig, ColumnSchema, ColumnRenderType, @@ -907,7 +907,7 @@ from pathlib import Path from typing import Callable, Dict, List, Optional from threading import Event -from cwa_book_downloader.release_sources import ( +from shelfmark.release_sources import ( Release, ReleaseSource, DownloadHandler, @@ -921,10 +921,10 @@ from cwa_book_downloader.release_sources import ( LeadingCellConfig, LeadingCellType, ) -from cwa_book_downloader.metadata_providers import BookMetadata -from cwa_book_downloader.core.models import DownloadTask -from cwa_book_downloader.core.logger import setup_logger -from cwa_book_downloader.core.settings_registry import ( +from shelfmark.metadata_providers import BookMetadata +from shelfmark.core.models import DownloadTask +from shelfmark.core.logger import setup_logger +from shelfmark.core.settings_registry import ( register_settings, HeadingField, TextField, @@ -934,7 +934,7 @@ from cwa_book_downloader.core.settings_registry import ( ActionButton, load_config_file, ) -from cwa_book_downloader.config.env import INGEST_DIR +from shelfmark.config.env import INGEST_DIR logger = setup_logger(__name__) @@ -1421,7 +1421,7 @@ PasswordField( Use the project's logger for consistent output: ```python -from cwa_book_downloader.core.logger import setup_logger +from shelfmark.core.logger import setup_logger logger = setup_logger(__name__) @@ -1437,13 +1437,13 @@ logger.error("Failures") # Unrecoverable errors When creating a new plugin: -1. Create `cwa_book_downloader/release_sources/my_plugin.py` +1. Create `shelfmark/release_sources/my_plugin.py` 2. Implement `ReleaseSource` subclass with `@register_source("my_plugin")` 3. Implement `DownloadHandler` subclass with `@register_handler("my_plugin")` 4. Add settings with `@register_settings("my_plugin", ...)` -5. Add import to `cwa_book_downloader/release_sources/__init__.py`: +5. Add import to `shelfmark/release_sources/__init__.py`: ```python - from cwa_book_downloader.release_sources import my_plugin # noqa: F401, E402 + from shelfmark.release_sources import my_plugin # noqa: F401, E402 ``` 6. Test `is_available()` returns `True` when configured 7. Test search returns valid `Release` objects diff --git a/entrypoint.sh b/entrypoint.sh index 7d591085..a710317d 100644 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -1,7 +1,7 @@ #!/bin/bash -LOG_DIR=${LOG_ROOT:-/var/log/}/cwa-book-downloader +LOG_DIR=${LOG_ROOT:-/var/log/}/shelfmark mkdir -p $LOG_DIR -LOG_FILE=${LOG_DIR}/cwa-bd_entrypoint.log +LOG_FILE=${LOG_DIR}/shelfmark_entrypoint.log # Cleanup any existing files or folders in the log directory rm -rf $LOG_DIR/* @@ -78,7 +78,7 @@ echo "Username for UID $RUN_UID is $USERNAME" test_write() { folder=$1 - test_file=$folder/calibre-web-automated-book-downloader_TEST_WRITE + test_file=$folder/shelfmark_TEST_WRITE mkdir -p $folder ( echo 0123456789_TEST | sudo -E -u "$USERNAME" HOME=/app tee $test_file > /dev/null @@ -109,6 +109,15 @@ make_writable() { change_ownership $folder chmod -R g+r,g+w $folder || echo "Failed to change group permissions for ${folder}, continuing..." fi + # Fix any misowned subdirectories/files (e.g., from previous runs as root) + if [ -d "$folder" ]; then + misowned_count=$(find "$folder" -mindepth 1 \( ! -user "$RUN_UID" -o ! -group "$RUN_GID" \) 2>/dev/null | wc -l) + if [ "$misowned_count" -gt 0 ]; then + echo "Fixing ownership of $misowned_count files/directories in $folder" + find "$folder" -mindepth 1 \( ! -user "$RUN_UID" -o ! -group "$RUN_GID" \) \ + -exec chown "$RUN_UID:$RUN_GID" {} \; 2>/dev/null || true + fi + fi test_write $folder || echo "Failed to test write to ${folder}, continuing..." } @@ -122,13 +131,27 @@ change_ownership() { } change_ownership /app -change_ownership /var/log/cwa-book-downloader -change_ownership /tmp/cwa-book-downloader +change_ownership /var/log/shelfmark +change_ownership /tmp/shelfmark # Test write to all folders make_writable ${CONFIG_DIR:-/config} make_writable ${INGEST_DIR:-/books} +# Fix permissions on directories configured in settings +echo "Checking for additional configured directories..." +if [ -f /app/scripts/fix_permissions.py ]; then + configured_dirs=$(python3 /app/scripts/fix_permissions.py 2>/dev/null || echo "") + if [ -n "$configured_dirs" ]; then + echo "$configured_dirs" | while read -r dir; do + if [ -n "$dir" ] && [ -d "$dir" ]; then + echo "Checking configured directory: $dir" + make_writable "$dir" + fi + done + fi +fi + # Fallback to root if config dir is still not writable (common on NAS/Unraid after upgrade from v0.4.0) CONFIG_PATH=${CONFIG_DIR:-/config} set +e @@ -165,7 +188,7 @@ fi # upgrades work reliably on customer machines. # Map app LOG_LEVEL (often DEBUG/INFO/...) to gunicorn's --log-level (lowercase). gunicorn_loglevel=$([ "$DEBUG" = "true" ] && echo debug || echo "${LOG_LEVEL:-info}" | tr '[:upper:]' '[:lower:]') -command="gunicorn --log-level ${gunicorn_loglevel} --access-logfile - --error-logfile - --worker-class geventwebsocket.gunicorn.workers.GeventWebSocketWorker --workers 1 -t 300 -b ${FLASK_HOST:-0.0.0.0}:${FLASK_PORT:-8084} cwa_book_downloader.main:app" +command="gunicorn --log-level ${gunicorn_loglevel} --access-logfile - --error-logfile - --worker-class geventwebsocket.gunicorn.workers.GeventWebSocketWorker --workers 1 -t 300 -b ${FLASK_HOST:-0.0.0.0}:${FLASK_PORT:-8084} shelfmark.main:app" # If DEBUG and not using an external bypass if [ "$DEBUG" = "true" ] && [ "$USING_EXTERNAL_BYPASSER" != "true" ]; then @@ -223,10 +246,10 @@ fi # Verify /tmp has at least 1MB of space and is writable/readable echo "Verifying /tmp has enough space" -rm -f /tmp/test.cwa-bd -if dd if=/dev/zero of=/tmp/test.cwa-bd bs=1M count=1 2>/dev/null && \ - [ "$(wc -c < /tmp/test.cwa-bd)" -eq 1048576 ]; then - rm -f /tmp/test.cwa-bd +rm -f /tmp/test.shelfmark +if dd if=/dev/zero of=/tmp/test.shelfmark bs=1M count=1 2>/dev/null && \ + [ "$(wc -c < /tmp/test.shelfmark)" -eq 1048576 ]; then + rm -f /tmp/test.shelfmark echo "Success: /tmp is writable and readable" else echo "Failure: /tmp is not writable or has insufficient space" diff --git a/genDebug.sh b/genDebug.sh index 2a72b30b..469c478b 100755 --- a/genDebug.sh +++ b/genDebug.sh @@ -2,8 +2,8 @@ # Set up log paths LOG_ROOT=${LOG_ROOT:-"/var/log"} -LOG_DIR="$LOG_ROOT/cwa-book-downloader" -OUTPUT_FILE_NAME="cwa-book-downloader-debug_BUILD-${BUILD_VERSION:-local}_RELEASE-${RELEASE_VERSION:-NA}_$(date +%Y%m%d-%H%M%S)" +LOG_DIR="$LOG_ROOT/shelfmark" +OUTPUT_FILE_NAME="shelfmark-debug_BUILD-${BUILD_VERSION:-local}_RELEASE-${RELEASE_VERSION:-NA}_$(date +%Y%m%d-%H%M%S)" OUTPUT_FILE="/tmp/$OUTPUT_FILE_NAME.zip" # Create LOG_DIR if it doesn't exist @@ -103,11 +103,11 @@ echo "" >> "$LOG_DIR/permissions.txt" echo "ls -all ${INGEST_DIR:-/books}" >> "$LOG_DIR/permissions.txt" ls -all ${INGEST_DIR:-/books} >> "$LOG_DIR/permissions.txt" 2>&1 echo "" >> "$LOG_DIR/permissions.txt" -echo "ls -all /var/log/cwa-book-downloader" >> "$LOG_DIR/permissions.txt" -ls -all /var/log/cwa-book-downloader >> "$LOG_DIR/permissions.txt" 2>&1 +echo "ls -all /var/log/shelfmark" >> "$LOG_DIR/permissions.txt" +ls -all /var/log/shelfmark >> "$LOG_DIR/permissions.txt" 2>&1 echo "" >> "$LOG_DIR/permissions.txt" -echo "ls -all /tmp/cwa-book-downloader" >> "$LOG_DIR/permissions.txt" -ls -all /tmp/cwa-book-downloader >> "$LOG_DIR/permissions.txt" 2>&1 +echo "ls -all /tmp/shelfmark" >> "$LOG_DIR/permissions.txt" +ls -all /tmp/shelfmark >> "$LOG_DIR/permissions.txt" 2>&1 echo "" >> "$LOG_DIR/permissions.txt" # Check Iptables (NAT) diff --git a/pyproject.toml b/pyproject.toml index 80ba6a7d..7856c9cc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [project] -name = "cwa-book-downloader" +name = "shelfmark" version = "0.1.0" -description = "Calibre-Web Automated Book Downloader" +description = "Shelfmark - Book Downloader" requires-python = ">=3.10" [tool.pytest.ini_options] diff --git a/readme.md b/readme.md index b6c467f6..344070d3 100644 --- a/readme.md +++ b/readme.md @@ -1,11 +1,12 @@ -# 📚 Book Downloader -*calibre-web-automated-book-downloader* +# 📚 Shelfmark: Book Downloader -Book Downloader +Formerly *Calibre Web Automated Book Downloader (CWABD)* -A unified web interface for searching and downloading books from multiple sources — all in one place. Works out of the box with popular web sources, no configuration required. Add metadata providers, additional release sources, and download clients to create a single hub for building your digital library. +Shelfmark -**Fully standalone** — no external dependencies required. Works great alongside library tools like [Calibre-Web-Automated](https://github.com/crocodilestick/Calibre-Web-Automated) or [Booklore](https://github.com/booklore-app/booklore) for automatic import. +A unified web interface for searching and downloading books from multiple sources - all in one place. Works out of the box with popular web sources, no configuration required. Add metadata providers, additional release sources, and download clients to create a single hub for building your digital library. + +**Fully standalone** - no external dependencies required. Works great alongside library tools like [Calibre-Web-Automated](https://github.com/crocodilestick/Calibre-Web-Automated), [Booklore](https://github.com/booklore-app/booklore) or [Audiobookshelf](https://github.com/advplyr/audiobookshelf) for automatic import. ## ✨ Features @@ -43,7 +44,7 @@ A unified web interface for searching and downloading books from multiple source 1. Download the docker-compose file: ```bash - curl -O https://raw.githubusercontent.com/calibrain/calibre-web-automated-book-downloader/main/compose/stable/docker-compose.yml + curl -O https://raw.githubusercontent.com/calibrain/shelfmark/main/compose/stable/docker-compose.yml ``` 2. Start the service: @@ -61,8 +62,9 @@ That's it! Configure settings through the web interface as needed. ```yaml volumes: - - /your/config/path:/config # Config, database, and artwork cache directory - - /your/download/path:/books # Downloaded books + - /your/config/path:/config # Config, database, and artwork cache directory + - /your/download/path:/books # Downloaded books + - /client/path:/client/path # Optional: For Torrent/Usenet downloads, match your client directory exactly. ``` > **Tip**: Point the download volume to your CWA or Booklore ingest folder for automatic import. @@ -96,6 +98,7 @@ Environment variables work for initial setup and Docker deployments. They serve | `TZ` | Container timezone | `UTC` | | `PUID` / `PGID` | Runtime user/group ID (also supports legacy `UID`/`GID`) | `1000` / `1000` | | `SEARCH_MODE` | `direct` or `universal` | `direct` | +| `USING_TOR` | Enable Tor routing (requires `NET_ADMIN` capability) | `false` | Some of the additional options available in Settings: - **AA Donator Key** - Use your paid account to skip Cloudflare challenges entirely and use faster, direct downloads @@ -113,30 +116,34 @@ Some of the additional options available in Settings: docker compose up -d ``` -### Tor Variant +The full-featured image with built-in Cloudflare bypass. + +#### Enable Tor Routing Routes all traffic through Tor for enhanced privacy: ```bash -curl -O https://raw.githubusercontent.com/calibrain/calibre-web-automated-book-downloader/main/compose/stable/docker-compose.tor.yml +curl -O https://raw.githubusercontent.com/calibrain/shelfmark/main/compose/stable/docker-compose.tor.yml docker compose -f docker-compose.tor.yml up -d ``` **Notes:** - Requires `NET_ADMIN` and `NET_RAW` capabilities - Timezone is auto-detected from Tor exit node -- Custom DNS/proxy settings are ignored +- Custom DNS/proxy settings are ignored when Tor is active + +### Lite +A smaller image without the built-in Cloudflare bypasser. Ideal for: + +- **External bypassers** - Already running FlareSolverr or ByParr for other services +- **Fast downloads** - Using fast download sources +- **Alternative sources only** - Exclusively using Prowlarr, IRC, or other sources +- **Audiobooks** - Downloading from Audiobookshelf-compatible sources -### External Cloudflare Resolver -Use FlareSolverr or ByParr instead of the built-in bypasser: ```bash -curl -O https://raw.githubusercontent.com/calibrain/calibre-web-automated-book-downloader/main/compose/stable/docker-compose.extbp.yml -docker compose -f docker-compose.extbp.yml up -d +curl -O https://raw.githubusercontent.com/calibrain/shelfmark/main/compose/stable/docker-compose.lite.yml +docker compose -f docker-compose.lite.yml up -d ``` -Configure the resolver URL in Settings under the Cloudflare tab. - -**When to use external vs internal bypasser:** -- **External** is useful if you already run FlareSolverr for other services (saves resources) or if you rarely need bypassing -- **Internal** (default) is faster and more reliable for most users - it's optimized specifically for this application +If you need Cloudflare bypass with the Lite image, configure an external resolver (FlareSolverr/ByParr) in Settings under the Cloudflare tab. ## 🔐 Authentication @@ -165,7 +172,7 @@ healthcheck: Logs are available via: - `docker logs ` -- `/var/log/cwa-book-downloader/` inside the container (when `ENABLE_LOGGING=true`) +- `/var/log/shelfmark/` inside the container (when `ENABLE_LOGGING=true`) Log level is configurable via Settings or `LOG_LEVEL` environment variable. @@ -241,4 +248,4 @@ Downloads are written atomically (via intermediate `.crdownload` files) to preve ## Support -For issues or questions, please [file an issue](https://github.com/calibrain/calibre-web-automated-book-downloader/issues) on GitHub. +For issues or questions, please [file an issue](https://github.com/calibrain/shelfmark/issues) on GitHub. diff --git a/requirements-cwa-bd.txt b/requirements-shelfmark.txt similarity index 100% rename from requirements-cwa-bd.txt rename to requirements-shelfmark.txt diff --git a/scripts/fix_permissions.py b/scripts/fix_permissions.py new file mode 100755 index 00000000..a4477a8a --- /dev/null +++ b/scripts/fix_permissions.py @@ -0,0 +1,92 @@ +#!/usr/bin/env python3 +"""Fix permissions on all configured directories. + +This script is called by the entrypoint to ensure all user-configured +directories have correct ownership. It reads directory paths from: +- CONFIG_DIR environment variable +- Config files in CONFIG_DIR/plugins/ + +Outputs directory paths that need permission fixing (one per line). +The entrypoint handles the actual chown operations. +""" + +import json +import os +import sys +from pathlib import Path + + +def get_directories_from_config() -> set[str]: + """Extract all directory paths from config files.""" + directories = set() + + config_dir = Path(os.getenv("CONFIG_DIR", "/config")) + plugins_dir = config_dir / "plugins" + + if not plugins_dir.exists(): + return directories + + # Keys that contain directory paths + directory_keys = { + # Main destinations + "DESTINATION", + "DESTINATION_AUDIOBOOK", + # Content type routing directories + "AA_CONTENT_TYPE_DIR_FICTION", + "AA_CONTENT_TYPE_DIR_NON_FICTION", + "AA_CONTENT_TYPE_DIR_UNKNOWN", + "AA_CONTENT_TYPE_DIR_MAGAZINE", + "AA_CONTENT_TYPE_DIR_COMIC", + "AA_CONTENT_TYPE_DIR_STANDARDS", + "AA_CONTENT_TYPE_DIR_MUSICAL_SCORE", + "AA_CONTENT_TYPE_DIR_OTHER", + # Legacy keys (in case of old configs) + "INGEST_DIR", + "INGEST_DIR_AUDIOBOOK", + "INGEST_DIR_BOOK_FICTION", + "INGEST_DIR_BOOK_NON_FICTION", + "INGEST_DIR_BOOK_UNKNOWN", + "INGEST_DIR_MAGAZINE", + "INGEST_DIR_COMIC_BOOK", + "INGEST_DIR_STANDARDS_DOCUMENT", + "INGEST_DIR_MUSICAL_SCORE", + "INGEST_DIR_OTHER", + "LIBRARY_PATH", + "LIBRARY_PATH_AUDIOBOOK", + } + + # Read all JSON config files + for config_file in plugins_dir.glob("*.json"): + try: + with open(config_file, "r") as f: + config = json.load(f) + + for key in directory_keys: + if key in config: + value = config[key] + if value and isinstance(value, str) and value.startswith("/"): + directories.add(value) + except (json.JSONDecodeError, OSError): + continue + + return directories + + +def main(): + """Output all configured directories that exist.""" + directories = get_directories_from_config() + + # Filter to directories that actually exist + existing = [] + for dir_path in directories: + path = Path(dir_path) + if path.exists() and path.is_dir(): + existing.append(dir_path) + + # Output one directory per line + for dir_path in sorted(existing): + print(dir_path) + + +if __name__ == "__main__": + main() diff --git a/shelfmark/__init__.py b/shelfmark/__init__.py new file mode 100644 index 00000000..2af044a7 --- /dev/null +++ b/shelfmark/__init__.py @@ -0,0 +1 @@ +"""Shelfmark - book search and download service.""" diff --git a/shelfmark/__main__.py b/shelfmark/__main__.py new file mode 100644 index 00000000..2ab2619a --- /dev/null +++ b/shelfmark/__main__.py @@ -0,0 +1,8 @@ +"""Package entry point for `python -m shelfmark`.""" + +from shelfmark.main import app, socketio +from shelfmark.config.env import FLASK_HOST, FLASK_PORT +from shelfmark.core.config import config + +if __name__ == "__main__": + socketio.run(app, host=FLASK_HOST, port=FLASK_PORT, debug=config.get("DEBUG", False)) diff --git a/cwa_book_downloader/api/__init__.py b/shelfmark/api/__init__.py similarity index 100% rename from cwa_book_downloader/api/__init__.py rename to shelfmark/api/__init__.py diff --git a/cwa_book_downloader/api/websocket.py b/shelfmark/api/websocket.py similarity index 100% rename from cwa_book_downloader/api/websocket.py rename to shelfmark/api/websocket.py diff --git a/cwa_book_downloader/bypass/__init__.py b/shelfmark/bypass/__init__.py similarity index 100% rename from cwa_book_downloader/bypass/__init__.py rename to shelfmark/bypass/__init__.py diff --git a/cwa_book_downloader/bypass/external_bypasser.py b/shelfmark/bypass/external_bypasser.py similarity index 93% rename from cwa_book_downloader/bypass/external_bypasser.py rename to shelfmark/bypass/external_bypasser.py index d11e5363..1a2ff4b6 100644 --- a/cwa_book_downloader/bypass/external_bypasser.py +++ b/shelfmark/bypass/external_bypasser.py @@ -7,12 +7,12 @@ from typing import TYPE_CHECKING, Optional import requests -from cwa_book_downloader.bypass import BypassCancelledException -from cwa_book_downloader.core.config import config -from cwa_book_downloader.core.logger import setup_logger +from shelfmark.bypass import BypassCancelledException +from shelfmark.core.config import config +from shelfmark.core.logger import setup_logger if TYPE_CHECKING: - from cwa_book_downloader.download import network + from shelfmark.download import network logger = setup_logger(__name__) @@ -99,7 +99,7 @@ def get_bypassed_page( cancel_flag: Optional[Event] = None ) -> Optional[str]: """Fetch HTML via external bypasser with retries and mirror rotation.""" - from cwa_book_downloader.download import network as network_module + from shelfmark.download import network as network_module sel = selector or network_module.AAMirrorSelector() diff --git a/cwa_book_downloader/bypass/fingerprint.py b/shelfmark/bypass/fingerprint.py similarity index 96% rename from cwa_book_downloader/bypass/fingerprint.py rename to shelfmark/bypass/fingerprint.py index 0af40818..371208f1 100644 --- a/cwa_book_downloader/bypass/fingerprint.py +++ b/shelfmark/bypass/fingerprint.py @@ -3,7 +3,7 @@ import random from typing import Optional -from cwa_book_downloader.core.logger import setup_logger +from shelfmark.core.logger import setup_logger logger = setup_logger(__name__) diff --git a/cwa_book_downloader/bypass/internal_bypasser.py b/shelfmark/bypass/internal_bypasser.py similarity index 98% rename from cwa_book_downloader/bypass/internal_bypasser.py rename to shelfmark/bypass/internal_bypasser.py index 904ec83e..36a642f1 100644 --- a/cwa_book_downloader/bypass/internal_bypasser.py +++ b/shelfmark/bypass/internal_bypasser.py @@ -13,15 +13,15 @@ from urllib.parse import urlparse import requests from seleniumbase import Driver -from cwa_book_downloader.bypass import BypassCancelledException -from cwa_book_downloader.bypass.fingerprint import get_screen_size -from cwa_book_downloader.config import env -from cwa_book_downloader.config.env import LOG_DIR -from cwa_book_downloader.config.settings import RECORDING_DIR -from cwa_book_downloader.core.config import config as app_config -from cwa_book_downloader.core.logger import setup_logger -from cwa_book_downloader.download import network -from cwa_book_downloader.download.network import get_proxies +from shelfmark.bypass import BypassCancelledException +from shelfmark.bypass.fingerprint import get_screen_size +from shelfmark.config import env +from shelfmark.config.env import LOG_DIR +from shelfmark.config.settings import RECORDING_DIR +from shelfmark.core.config import config as app_config +from shelfmark.core.logger import setup_logger +from shelfmark.download import network +from shelfmark.download.network import get_proxies logger = setup_logger(__name__) diff --git a/cwa_book_downloader/config/__init__.py b/shelfmark/config/__init__.py similarity index 100% rename from cwa_book_downloader/config/__init__.py rename to shelfmark/config/__init__.py diff --git a/cwa_book_downloader/config/env.py b/shelfmark/config/env.py similarity index 96% rename from cwa_book_downloader/config/env.py rename to shelfmark/config/env.py index c2225d86..cc9ec9f8 100644 --- a/cwa_book_downloader/config/env.py +++ b/shelfmark/config/env.py @@ -74,7 +74,7 @@ def _is_config_dir_writable() -> bool: def is_covers_cache_enabled() -> bool: """Check if cover caching is enabled (requires setting + writable config dir).""" - from cwa_book_downloader.core.config import config + from shelfmark.core.config import config setting_enabled = config.get("COVERS_CACHE_ENABLED", True) return setting_enabled and _is_config_dir_writable() @@ -85,9 +85,9 @@ def is_covers_cache_enabled() -> bool: CONFIG_DIR = Path(os.getenv("CONFIG_DIR", "/config")) LOG_ROOT = Path(os.getenv("LOG_ROOT", "/var/log/")) -LOG_DIR = LOG_ROOT / "cwa-book-downloader" -LOG_FILE = LOG_DIR / "cwa-book-downloader.log" -TMP_DIR = Path(os.getenv("TMP_DIR", "/tmp/cwa-book-downloader")) +LOG_DIR = LOG_ROOT / "shelfmark" +LOG_FILE = LOG_DIR / "shelfmark.log" +TMP_DIR = Path(os.getenv("TMP_DIR", "/tmp/shelfmark")) INGEST_DIR = Path(os.getenv("INGEST_DIR", "/books")) diff --git a/cwa_book_downloader/config/security.py b/shelfmark/config/security.py similarity index 95% rename from cwa_book_downloader/config/security.py rename to shelfmark/config/security.py index 2f78618c..5d5bc3a0 100644 --- a/cwa_book_downloader/config/security.py +++ b/shelfmark/config/security.py @@ -4,8 +4,8 @@ from typing import Any, Dict from werkzeug.security import generate_password_hash -from cwa_book_downloader.core.logger import setup_logger -from cwa_book_downloader.core.settings_registry import ( +from shelfmark.core.logger import setup_logger +from shelfmark.core.settings_registry import ( register_settings, register_on_save, load_config_file, @@ -21,7 +21,7 @@ logger = setup_logger(__name__) def _clear_builtin_credentials() -> Dict[str, Any]: """Clear built-in credentials to allow public access.""" import json - from cwa_book_downloader.core.settings_registry import _get_config_file_path, _ensure_config_dir + from shelfmark.core.settings_registry import _get_config_file_path, _ensure_config_dir try: config = load_config_file("security") @@ -100,7 +100,7 @@ def _on_save_security(values: Dict[str, Any]) -> Dict[str, Any]: @register_settings("security", "Security", icon="shield", order=5) def security_settings(): """Security and authentication settings.""" - from cwa_book_downloader.config.env import CWA_DB_PATH + from shelfmark.config.env import CWA_DB_PATH cwa_db_available = CWA_DB_PATH is not None and CWA_DB_PATH.exists() diff --git a/cwa_book_downloader/config/settings.py b/shelfmark/config/settings.py similarity index 95% rename from cwa_book_downloader/config/settings.py rename to shelfmark/config/settings.py index 3b1508a9..8e4cbc65 100644 --- a/cwa_book_downloader/config/settings.py +++ b/shelfmark/config/settings.py @@ -4,8 +4,8 @@ import os from pathlib import Path import json -from cwa_book_downloader.config import env -from cwa_book_downloader.core.logger import setup_logger +from shelfmark.config import env +from shelfmark.core.logger import setup_logger logger = setup_logger(__name__) @@ -40,7 +40,7 @@ RECORDING_DIR = env.LOG_DIR / "recording" def _log_external_bypasser_warning() -> None: """Log warning about external bypasser DNS limitations (called after config is available).""" - from cwa_book_downloader.core.config import config + from shelfmark.core.config import config if config.get("USING_EXTERNAL_BYPASSER", False) and config.get("USE_CF_BYPASS", True): logger.warning( "Using external bypasser (FlareSolverr). Note: FlareSolverr uses its own DNS resolution, " @@ -50,7 +50,7 @@ def _log_external_bypasser_warning() -> None: ) -from cwa_book_downloader.core.settings_registry import ( +from shelfmark.core.settings_registry import ( register_settings, register_group, TextField, @@ -118,7 +118,7 @@ _AUDIOBOOK_FORMAT_OPTIONS = [ def _get_metadata_provider_options(): """Build metadata provider options dynamically from enabled providers only.""" - from cwa_book_downloader.metadata_providers import list_providers, is_provider_enabled + from shelfmark.metadata_providers import list_providers, is_provider_enabled options = [] for provider in list_providers(): @@ -142,18 +142,19 @@ def _get_metadata_provider_options_with_none(): def _get_release_source_options(): """Build release source options dynamically from registered sources.""" - from cwa_book_downloader.release_sources import list_available_sources + from shelfmark.release_sources import list_available_sources return [ {"value": source["name"], "label": source["display_name"]} for source in list_available_sources() + if source.get("can_be_default", True) ] _LANGUAGE_OPTIONS = [{"value": lang["code"], "label": lang["language"]} for lang in _SUPPORTED_BOOK_LANGUAGE] def _get_aa_base_url_options(): """Build AA URL options dynamically, including additional mirrors from config.""" - from cwa_book_downloader.core.mirrors import DEFAULT_AA_MIRRORS, get_aa_mirrors + from shelfmark.core.mirrors import DEFAULT_AA_MIRRORS, get_aa_mirrors options = [{"value": "auto", "label": "Auto (Recommended)"}] @@ -171,8 +172,8 @@ def _get_aa_base_url_options(): def _get_zlib_mirror_options(): """Build Z-Library mirror options for SelectField.""" - from cwa_book_downloader.core.mirrors import DEFAULT_ZLIB_MIRRORS - from cwa_book_downloader.core.config import config + from shelfmark.core.mirrors import DEFAULT_ZLIB_MIRRORS + from shelfmark.core.config import config options = [] @@ -195,8 +196,8 @@ def _get_zlib_mirror_options(): def _get_welib_mirror_options(): """Build Welib mirror options for SelectField.""" - from cwa_book_downloader.core.mirrors import DEFAULT_WELIB_MIRRORS - from cwa_book_downloader.core.config import config + from shelfmark.core.mirrors import DEFAULT_WELIB_MIRRORS + from shelfmark.core.config import config options = [] @@ -220,7 +221,7 @@ def _get_welib_mirror_options(): def _clear_covers_cache(current_values: dict) -> dict: """Clear the cover image cache.""" try: - from cwa_book_downloader.core.image_cache import get_image_cache, reset_image_cache + from shelfmark.core.image_cache import get_image_cache, reset_image_cache cache = get_image_cache() count = cache.clear() @@ -243,7 +244,7 @@ def _clear_covers_cache(current_values: dict) -> dict: def _clear_metadata_cache(current_values: dict) -> dict: """Clear the in-memory metadata cache.""" try: - from cwa_book_downloader.core.cache import get_metadata_cache + from shelfmark.core.cache import get_metadata_cache cache = get_metadata_cache() stats_before = cache.stats() @@ -377,9 +378,9 @@ def network_settings(): tor_available = env.TOR_VARIANT_AVAILABLE tor_enabled = env.USING_TOR - # When Tor is enabled (only possible in Tor variant), DNS/proxy settings are overridden - # The Tor variant uses iptables to force ALL traffic through Tor - it cannot be disabled - tor_overrides_network = tor_available # If Tor variant, network settings are always managed by Tor + # When Tor is enabled, DNS/proxy settings are overridden by iptables rules + # Tor uses iptables to force ALL traffic through Tor + tor_overrides_network = tor_enabled # Only override when Tor is actually active return [ SelectField( @@ -436,16 +437,16 @@ def network_settings(): key="USING_TOR", label="Tor Routing", description=( - "All traffic is routed through Tor in this container variant. This cannot be changed." - if tor_available - else "Tor routing is not available in this container variant." + "All traffic is routed through Tor. Requires container restart to change." + if tor_enabled + else "Route all traffic through Tor for enhanced privacy." ), - default=tor_available, # Reflects actual state: True if Tor variant, False otherwise - disabled=True, # Always disabled - Tor state is determined by container variant + default=tor_enabled, # Reflects actual state from env var + disabled=True, # Tor state requires container restart disabled_reason=( - "Tor routing is always active in the Tor container variant." - if tor_available - else "Requires the Tor container variant (calibre-web-automated-book-downloader-tor)." + "Tor routing is active. Set USING_TOR=false and restart to disable." + if tor_enabled + else "Set USING_TOR=true env var and restart with NET_ADMIN/NET_RAW capabilities." ), ), SelectField( @@ -659,7 +660,7 @@ def download_settings(): def _get_fast_source_options(): """Fast download sources - display only, not configurable.""" - from cwa_book_downloader.core.config import config + from shelfmark.core.config import config has_donator_key = bool(config.get("AA_DONATOR_KEY", "")) @@ -691,7 +692,7 @@ def _get_fast_source_defaults(): def _get_slow_source_options(): """Slow download sources - configurable order. All require bypasser.""" - from cwa_book_downloader.core.config import config + from shelfmark.core.config import config bypass_enabled = config.get("USE_CF_BYPASS", True) locked = not bypass_enabled @@ -731,7 +732,7 @@ def _get_slow_source_options(): def _get_slow_source_defaults(): """Default source priority order for slow sources.""" - from cwa_book_downloader.config.env import _LEGACY_ALLOW_USE_WELIB + from shelfmark.config.env import _LEGACY_ALLOW_USE_WELIB return [ {"id": "aa-slow-nowait", "enabled": True}, @@ -900,7 +901,7 @@ def cloudflare_bypass_settings(): @register_settings("mirrors", "Mirrors", icon="globe", order=23, group="direct_download") def mirror_settings(): """Configure download source mirrors.""" - from cwa_book_downloader.core.mirrors import DEFAULT_ZLIB_MIRRORS, DEFAULT_WELIB_MIRRORS + from shelfmark.core.mirrors import DEFAULT_ZLIB_MIRRORS, DEFAULT_WELIB_MIRRORS return [ # === ANNA'S ARCHIVE === diff --git a/shelfmark/core/__init__.py b/shelfmark/core/__init__.py new file mode 100644 index 00000000..f55c6a04 --- /dev/null +++ b/shelfmark/core/__init__.py @@ -0,0 +1,5 @@ +"""Core module - shared models, queue, and utilities.""" + +from shelfmark.core.models import BookInfo, QueueItem, SearchFilters, QueueStatus +from shelfmark.core.queue import BookQueue, book_queue +from shelfmark.core.logger import setup_logger diff --git a/cwa_book_downloader/core/cache.py b/shelfmark/core/cache.py similarity index 97% rename from cwa_book_downloader/core/cache.py rename to shelfmark/core/cache.py index f36d8f57..66111ddb 100644 --- a/cwa_book_downloader/core/cache.py +++ b/shelfmark/core/cache.py @@ -6,7 +6,7 @@ from dataclasses import dataclass from functools import wraps from typing import Any, Callable, Dict, Optional, TypeVar -from cwa_book_downloader.core.logger import setup_logger +from shelfmark.core.logger import setup_logger logger = setup_logger(__name__) @@ -130,7 +130,7 @@ def cacheable( @wraps(func) def wrapper(*args, **kwargs) -> T: # Check if metadata caching is enabled - from cwa_book_downloader.core.config import config + from shelfmark.core.config import config if not config.get("METADATA_CACHE_ENABLED", True): # Caching disabled, execute function directly diff --git a/cwa_book_downloader/core/config.py b/shelfmark/core/config.py similarity index 93% rename from cwa_book_downloader/core/config.py rename to shelfmark/core/config.py index d2bd6135..0cdf61ff 100644 --- a/cwa_book_downloader/core/config.py +++ b/shelfmark/core/config.py @@ -12,7 +12,7 @@ def _get_registry(): """Lazy import of settings registry to avoid circular imports.""" global _registry_module if _registry_module is None: - from cwa_book_downloader.core import settings_registry + from shelfmark.core import settings_registry _registry_module = settings_registry return _registry_module @@ -21,7 +21,7 @@ def _get_env(): """Lazy import of env module for fallback values.""" global _env_module if _env_module is None: - from cwa_book_downloader.config import env + from shelfmark.config import env _env_module = env return _env_module @@ -68,9 +68,9 @@ class Config: # Ensure all settings modules are imported before loading # This handles cases where config is accessed before settings are registered try: - import cwa_book_downloader.config.settings # noqa: F401 - main app settings - import cwa_book_downloader.release_sources # noqa: F401 - plugin settings - import cwa_book_downloader.metadata_providers # noqa: F401 - plugin settings + import shelfmark.config.settings # noqa: F401 - main app settings + import shelfmark.release_sources # noqa: F401 - plugin settings + import shelfmark.metadata_providers # noqa: F401 - plugin settings except ImportError: pass diff --git a/cwa_book_downloader/core/image_cache.py b/shelfmark/core/image_cache.py similarity index 98% rename from cwa_book_downloader/core/image_cache.py rename to shelfmark/core/image_cache.py index da496fe1..681f58c5 100644 --- a/cwa_book_downloader/core/image_cache.py +++ b/shelfmark/core/image_cache.py @@ -10,7 +10,7 @@ from typing import Any, Dict, Optional, Tuple import requests -from cwa_book_downloader.core.logger import setup_logger +from shelfmark.core.logger import setup_logger logger = setup_logger(__name__) @@ -544,8 +544,8 @@ def get_image_cache() -> ImageCacheService: if _instance is None: with _instance_lock: if _instance is None: - from cwa_book_downloader.core.config import config - from cwa_book_downloader.config.env import CONFIG_DIR + from shelfmark.core.config import config + from shelfmark.config.env import CONFIG_DIR cache_dir = CONFIG_DIR / "covers" max_size_mb = config.get("COVERS_CACHE_MAX_SIZE_MB", 500) diff --git a/cwa_book_downloader/core/logger.py b/shelfmark/core/logger.py similarity index 98% rename from cwa_book_downloader/core/logger.py rename to shelfmark/core/logger.py index 3d86cd48..eb0cf985 100644 --- a/cwa_book_downloader/core/logger.py +++ b/shelfmark/core/logger.py @@ -6,7 +6,7 @@ from pathlib import Path from logging.handlers import RotatingFileHandler from typing import Any -from cwa_book_downloader.config.env import LOG_FILE, ENABLE_LOGGING, LOG_LEVEL +from shelfmark.config.env import LOG_FILE, ENABLE_LOGGING, LOG_LEVEL class CustomLogger(logging.Logger): diff --git a/cwa_book_downloader/core/mirrors.py b/shelfmark/core/mirrors.py similarity index 98% rename from cwa_book_downloader/core/mirrors.py rename to shelfmark/core/mirrors.py index 57cf8577..628bb3ad 100644 --- a/cwa_book_downloader/core/mirrors.py +++ b/shelfmark/core/mirrors.py @@ -10,7 +10,7 @@ def _get_config(): """Lazy import of config module to avoid circular imports.""" global _config_module if _config_module is None: - from cwa_book_downloader.core.config import config + from shelfmark.core.config import config _config_module = config return _config_module diff --git a/cwa_book_downloader/core/models.py b/shelfmark/core/models.py similarity index 100% rename from cwa_book_downloader/core/models.py rename to shelfmark/core/models.py diff --git a/cwa_book_downloader/core/naming.py b/shelfmark/core/naming.py similarity index 99% rename from cwa_book_downloader/core/naming.py rename to shelfmark/core/naming.py index 5d154998..bf0fac85 100644 --- a/cwa_book_downloader/core/naming.py +++ b/shelfmark/core/naming.py @@ -5,7 +5,7 @@ import re from pathlib import Path from typing import Dict, Optional, Union -from cwa_book_downloader.core.logger import setup_logger +from shelfmark.core.logger import setup_logger logger = setup_logger(__name__) diff --git a/cwa_book_downloader/core/queue.py b/shelfmark/core/queue.py similarity index 98% rename from cwa_book_downloader/core/queue.py rename to shelfmark/core/queue.py index 7f75d17f..1a0c7116 100644 --- a/cwa_book_downloader/core/queue.py +++ b/shelfmark/core/queue.py @@ -7,8 +7,8 @@ from pathlib import Path from threading import Lock, Event from typing import Dict, List, Optional, Tuple, Any -from cwa_book_downloader.core.config import config as app_config -from cwa_book_downloader.core.models import QueueStatus, QueueItem, DownloadTask +from shelfmark.core.config import config as app_config +from shelfmark.core.models import QueueStatus, QueueItem, DownloadTask class BookQueue: diff --git a/cwa_book_downloader/core/settings_registry.py b/shelfmark/core/settings_registry.py similarity index 98% rename from cwa_book_downloader/core/settings_registry.py rename to shelfmark/core/settings_registry.py index a062f91e..fabf4b57 100644 --- a/cwa_book_downloader/core/settings_registry.py +++ b/shelfmark/core/settings_registry.py @@ -7,7 +7,7 @@ from pathlib import Path from typing import Any, Callable, Dict, List, Optional, Type, Union from threading import Lock -from cwa_book_downloader.core.logger import setup_logger +from shelfmark.core.logger import setup_logger logger = setup_logger(__name__) @@ -232,7 +232,7 @@ def list_registered_settings() -> List[str]: def _get_config_dir() -> Path: """Get the config directory path.""" - from cwa_book_downloader.config.env import CONFIG_DIR + from shelfmark.config.env import CONFIG_DIR return Path(CONFIG_DIR) @@ -530,6 +530,8 @@ def serialize_field(field: SettingsField, tab_name: str, include_value: bool = T # Support callable options for lazy evaluation (avoids circular imports) options = field.options() if callable(field.options) else field.options result["options"] = options + if field.default is not None: + result["default"] = field.default elif isinstance(field, MultiSelectField): # Support callable options for lazy evaluation (avoids circular imports) options = field.options() if callable(field.options) else field.options @@ -634,7 +636,7 @@ def _sync_metadata_provider_selection() -> None: the first enabled provider if the current selection is invalid. """ try: - from cwa_book_downloader.metadata_providers import sync_metadata_provider_selection + from shelfmark.metadata_providers import sync_metadata_provider_selection sync_metadata_provider_selection() except ImportError: pass # Metadata providers module not available @@ -648,7 +650,7 @@ def _apply_dns_settings(config) -> None: a container restart. """ try: - from cwa_book_downloader.download import network + from shelfmark.download import network provider = config.get("CUSTOM_DNS", "auto") use_doh = config.get("USE_DOH", False) @@ -734,7 +736,7 @@ def update_settings(tab_name: str, values: Dict[str, Any]) -> Dict[str, Any]: if save_config_file(tab_name, values_to_save): # Refresh the config singleton so live settings take effect immediately try: - from cwa_book_downloader.core.config import config + from shelfmark.core.config import config config.refresh() except ImportError: pass # Config module not yet available during initial setup diff --git a/cwa_book_downloader/core/utils.py b/shelfmark/core/utils.py similarity index 93% rename from cwa_book_downloader/core/utils.py rename to shelfmark/core/utils.py index 6a18e2bb..93762979 100644 --- a/cwa_book_downloader/core/utils.py +++ b/shelfmark/core/utils.py @@ -1,4 +1,4 @@ -"""Shared utility functions for the CWA Book Downloader.""" +"""Shared utility functions for the Shelfmark.""" import base64 from pathlib import Path @@ -52,7 +52,7 @@ _LEGACY_CONTENT_TYPE_TO_CONFIG_KEY = { def get_destination(is_audiobook: bool = False) -> Path: """Get base destination directory. Audiobooks fall back to main destination.""" - from cwa_book_downloader.core.config import config + from shelfmark.core.config import config if is_audiobook: # Audiobook destination with fallback to main destination @@ -68,7 +68,7 @@ def get_destination(is_audiobook: bool = False) -> Path: def get_aa_content_type_dir(content_type: Optional[str] = None) -> Optional[Path]: """Get override directory for AA content-type routing if configured.""" - from cwa_book_downloader.core.config import config + from shelfmark.core.config import config # Check if content-type routing is enabled (new or legacy setting) if not config.get("AA_CONTENT_TYPE_ROUTING", False) and not config.get("USE_CONTENT_TYPE_DIRECTORIES", False): @@ -92,7 +92,7 @@ def get_aa_content_type_dir(content_type: Optional[str] = None) -> Optional[Path def get_ingest_dir(content_type: Optional[str] = None) -> Path: """DEPRECATED: Use get_destination() and get_aa_content_type_dir() instead.""" - from cwa_book_downloader.core.config import config + from shelfmark.core.config import config # Check new DESTINATION setting first, then legacy INGEST_DIR default_ingest_dir = Path(config.get("DESTINATION", "") or config.get("INGEST_DIR", "/books")) @@ -118,7 +118,7 @@ def transform_cover_url(cover_url: Optional[str], cache_id: str) -> Optional[str return cover_url # Check if cover caching is enabled - from cwa_book_downloader.config.env import is_covers_cache_enabled + from shelfmark.config.env import is_covers_cache_enabled if not is_covers_cache_enabled(): return cover_url diff --git a/cwa_book_downloader/download/__init__.py b/shelfmark/download/__init__.py similarity index 100% rename from cwa_book_downloader/download/__init__.py rename to shelfmark/download/__init__.py diff --git a/cwa_book_downloader/download/archive.py b/shelfmark/download/archive.py similarity index 97% rename from cwa_book_downloader/download/archive.py rename to shelfmark/download/archive.py index 282e90f9..125b7c7f 100644 --- a/cwa_book_downloader/download/archive.py +++ b/shelfmark/download/archive.py @@ -7,11 +7,11 @@ from dataclasses import dataclass from pathlib import Path from typing import List, Optional, Tuple -from cwa_book_downloader.core.logger import setup_logger -from cwa_book_downloader.core.config import config -from cwa_book_downloader.core.naming import parse_naming_template, sanitize_filename -from cwa_book_downloader.core.utils import is_audiobook as check_audiobook -from cwa_book_downloader.download.fs import atomic_write, atomic_move +from shelfmark.core.logger import setup_logger +from shelfmark.core.config import config +from shelfmark.core.naming import parse_naming_template, sanitize_filename +from shelfmark.core.utils import is_audiobook as check_audiobook +from shelfmark.download.fs import atomic_write, atomic_move logger = setup_logger(__name__) diff --git a/cwa_book_downloader/download/fs.py b/shelfmark/download/fs.py similarity index 99% rename from cwa_book_downloader/download/fs.py rename to shelfmark/download/fs.py index 03605080..292d38c4 100644 --- a/cwa_book_downloader/download/fs.py +++ b/shelfmark/download/fs.py @@ -9,7 +9,7 @@ import os import shutil from pathlib import Path -from cwa_book_downloader.core.logger import setup_logger +from shelfmark.core.logger import setup_logger logger = setup_logger(__name__) diff --git a/cwa_book_downloader/download/http.py b/shelfmark/download/http.py similarity index 98% rename from cwa_book_downloader/download/http.py rename to shelfmark/download/http.py index 36a3a40a..6e0a82d6 100644 --- a/cwa_book_downloader/download/http.py +++ b/shelfmark/download/http.py @@ -10,10 +10,10 @@ from urllib.parse import urlparse import requests from tqdm import tqdm -from cwa_book_downloader.download import network -from cwa_book_downloader.download.network import get_proxies -from cwa_book_downloader.core.config import config as app_config -from cwa_book_downloader.core.logger import setup_logger +from shelfmark.download import network +from shelfmark.download.network import get_proxies +from shelfmark.core.config import config as app_config +from shelfmark.core.logger import setup_logger logger = setup_logger(__name__) @@ -27,7 +27,7 @@ def _get_internal_bypasser(): global _internal_bypasser if _internal_bypasser is None: try: - from cwa_book_downloader.bypass import internal_bypasser + from shelfmark.bypass import internal_bypasser _internal_bypasser = internal_bypasser except ImportError as e: raise RuntimeError( @@ -43,7 +43,7 @@ def _get_external_bypasser(): global _external_bypasser if _external_bypasser is None: try: - from cwa_book_downloader.bypass import external_bypasser + from shelfmark.bypass import external_bypasser _external_bypasser = external_bypasser except ImportError as e: raise RuntimeError( diff --git a/cwa_book_downloader/download/network.py b/shelfmark/download/network.py similarity index 99% rename from cwa_book_downloader/download/network.py rename to shelfmark/download/network.py index e7038b49..38d1be3a 100644 --- a/cwa_book_downloader/download/network.py +++ b/shelfmark/download/network.py @@ -9,8 +9,8 @@ from socket import AddressFamily, SocketKind import urllib.parse import ipaddress -from cwa_book_downloader.core.logger import setup_logger -from cwa_book_downloader.core.config import config as app_config +from shelfmark.core.logger import setup_logger +from shelfmark.core.config import config as app_config from datetime import datetime, timedelta @@ -812,7 +812,7 @@ def _looks_like_ip(s: str) -> bool: def _build_aa_urls() -> List[str]: """Build list of available AA URLs from centralized mirror config.""" - from cwa_book_downloader.core.mirrors import get_aa_mirrors + from shelfmark.core.mirrors import get_aa_mirrors return get_aa_mirrors() diff --git a/cwa_book_downloader/download/orchestrator.py b/shelfmark/download/orchestrator.py similarity index 97% rename from cwa_book_downloader/download/orchestrator.py rename to shelfmark/download/orchestrator.py index fb2792b9..9265b73c 100644 --- a/cwa_book_downloader/download/orchestrator.py +++ b/shelfmark/download/orchestrator.py @@ -16,13 +16,13 @@ from pathlib import Path from threading import Event, Lock from typing import Any, Dict, List, Optional, Tuple -from cwa_book_downloader.release_sources import direct_download -from cwa_book_downloader.release_sources.direct_download import SearchUnavailable -from cwa_book_downloader.core.config import config -from cwa_book_downloader.config.env import TMP_DIR -from cwa_book_downloader.core.utils import get_ingest_dir, get_destination, get_aa_content_type_dir, is_audiobook as check_audiobook, transform_cover_url -from cwa_book_downloader.core.naming import build_library_path, same_filesystem, assign_part_numbers, parse_naming_template, sanitize_filename -from cwa_book_downloader.download.archive import ( +from shelfmark.release_sources import direct_download +from shelfmark.release_sources.direct_download import SearchUnavailable +from shelfmark.core.config import config +from shelfmark.config.env import TMP_DIR +from shelfmark.core.utils import get_ingest_dir, get_destination, get_aa_content_type_dir, is_audiobook as check_audiobook, transform_cover_url +from shelfmark.core.naming import build_library_path, same_filesystem, assign_part_numbers, parse_naming_template, sanitize_filename +from shelfmark.download.archive import ( is_archive, process_archive, _get_file_organization, @@ -30,10 +30,10 @@ from cwa_book_downloader.download.archive import ( _get_supported_formats as _get_book_formats, _get_supported_audiobook_formats, ) -from cwa_book_downloader.release_sources import get_handler, get_source_display_name -from cwa_book_downloader.core.logger import setup_logger -from cwa_book_downloader.core.models import BookInfo, DownloadTask, QueueStatus, SearchFilters, SearchMode -from cwa_book_downloader.core.queue import book_queue +from shelfmark.release_sources import get_handler, get_source_display_name +from shelfmark.core.logger import setup_logger +from shelfmark.core.models import BookInfo, DownloadTask, QueueStatus, SearchFilters, SearchMode +from shelfmark.core.queue import book_queue logger = setup_logger(__name__) @@ -278,7 +278,7 @@ def process_directory( # Track whether WebSocket is available for status reporting WEBSOCKET_AVAILABLE = True try: - from cwa_book_downloader.api.websocket import ws_manager + from shelfmark.api.websocket import ws_manager except ImportError: logger.error("WebSocket unavailable - real-time updates disabled") ws_manager = None @@ -660,7 +660,7 @@ def _stage_torrent_path(source: Path) -> Path: # Import atomic file operations from shared module # Re-exported here for backwards compatibility with existing tests/imports -from cwa_book_downloader.download.fs import ( +from shelfmark.download.fs import ( atomic_hardlink as _atomic_hardlink, atomic_copy as _atomic_copy, atomic_move as _atomic_move, diff --git a/cwa_book_downloader/main.py b/shelfmark/main.py similarity index 95% rename from cwa_book_downloader/main.py rename to shelfmark/main.py index 28344197..68a0ee3e 100644 --- a/cwa_book_downloader/main.py +++ b/shelfmark/main.py @@ -16,17 +16,17 @@ from werkzeug.middleware.proxy_fix import ProxyFix from werkzeug.security import check_password_hash from werkzeug.wrappers import Response -from cwa_book_downloader.download import orchestrator as backend -from cwa_book_downloader.release_sources.direct_download import SearchUnavailable -from cwa_book_downloader.config.settings import _SUPPORTED_BOOK_LANGUAGE -from cwa_book_downloader.config.env import ( +from shelfmark.download import orchestrator as backend +from shelfmark.release_sources.direct_download import SearchUnavailable +from shelfmark.config.settings import _SUPPORTED_BOOK_LANGUAGE +from shelfmark.config.env import ( BUILD_VERSION, CWA_DB_PATH, DEBUG, FLASK_HOST, FLASK_PORT, RELEASE_VERSION, ) -from cwa_book_downloader.core.config import config as app_config -from cwa_book_downloader.core.logger import setup_logger -from cwa_book_downloader.core.models import SearchFilters -from cwa_book_downloader.api.websocket import ws_manager +from shelfmark.core.config import config as app_config +from shelfmark.core.logger import setup_logger +from shelfmark.core.models import SearchFilters +from shelfmark.api.websocket import ws_manager logger = setup_logger(__name__) @@ -71,8 +71,8 @@ logger.info(f"Flask-SocketIO initialized with async_mode='{async_mode}'") # This prevents a race condition where the download loop could try to process # a queued task before its handler (e.g., prowlarr) is registered. try: - import cwa_book_downloader.metadata_providers # noqa: F401 - import cwa_book_downloader.release_sources # noqa: F401 + import shelfmark.metadata_providers # noqa: F401 + import shelfmark.release_sources # noqa: F401 logger.debug("Plugin modules loaded successfully") except ImportError as e: logger.warning(f"Failed to import plugin modules: {e}") @@ -152,7 +152,7 @@ def get_auth_mode() -> str: 2. Built-in credentials (if configured) 3. No auth required or error -> "none" """ - from cwa_book_downloader.core.settings_registry import load_config_file + from shelfmark.core.settings_registry import load_config_file try: security_config = load_config_file("security") @@ -223,7 +223,7 @@ werkzeug_logger.addFilter(LogNoiseFilter()) # Set up authentication defaults # The secret key will reset every time we restart, which will # require users to authenticate again -from cwa_book_downloader.config.env import SESSION_COOKIE_SECURE_ENV, string_to_bool +from shelfmark.config.env import SESSION_COOKIE_SECURE_ENV, string_to_bool SESSION_COOKIE_SECURE = string_to_bool(SESSION_COOKIE_SECURE_ENV) @@ -296,14 +296,14 @@ if DEBUG: if app_config.get("USING_EXTERNAL_BYPASSER", False): _stop_gui = lambda: None else: - from cwa_book_downloader.bypass.internal_bypasser import _cleanup_orphan_processes as _stop_gui + from shelfmark.bypass.internal_bypasser import _cleanup_orphan_processes as _stop_gui @app.route('/api/debug', methods=['GET']) @login_required def debug() -> Union[Response, Tuple[Response, int]]: """ This will run the /app/genDebug.sh script, which will generate a debug zip with all the logs - The file will be named /tmp/cwa-book-downloader-debug.zip + The file will be named /tmp/shelfmark-debug.zip And then return it to the user """ try: @@ -486,12 +486,12 @@ def api_config() -> Union[Response, Tuple[Response, int]]: are reflected without requiring a container restart. """ try: - from cwa_book_downloader.metadata_providers import ( + from shelfmark.metadata_providers import ( get_provider_sort_options, get_provider_search_fields, get_provider_default_sort, ) - from cwa_book_downloader.config.env import _is_config_dir_writable + from shelfmark.config.env import _is_config_dir_writable config = { "calibre_web_url": app_config.get("CALIBRE_WEB_URL", ""), @@ -604,8 +604,8 @@ def api_cover(cover_id: str) -> Union[Response, Tuple[Response, int]]: """ try: import base64 - from cwa_book_downloader.core.image_cache import get_image_cache - from cwa_book_downloader.config.env import is_covers_cache_enabled + from shelfmark.core.image_cache import get_image_cache + from shelfmark.config.env import is_covers_cache_enabled # Check if caching is enabled if not is_covers_cache_enabled(): @@ -859,7 +859,7 @@ def api_login() -> Union[Response, Tuple[Response, int]]: Returns: flask.Response: JSON with success status or error message. """ - from cwa_book_downloader.core.settings_registry import load_config_file + from shelfmark.core.settings_registry import load_config_file try: ip_address = get_client_ip() @@ -984,7 +984,7 @@ def api_auth_check() -> Union[Response, Tuple[Response, int]]: flask.Response: JSON with authentication status, whether auth is required, which auth mode is active, and whether user has admin privileges. """ - from cwa_book_downloader.core.settings_registry import load_config_file + from shelfmark.core.settings_registry import load_config_file try: auth_mode = get_auth_mode() @@ -1043,7 +1043,7 @@ def api_metadata_providers() -> Union[Response, Tuple[Response, int]]: flask.Response: JSON with list of providers and their status. """ try: - from cwa_book_downloader.metadata_providers import ( + from shelfmark.metadata_providers import ( list_providers, get_provider, get_provider_kwargs, @@ -1096,7 +1096,7 @@ def api_metadata_search() -> Union[Response, Tuple[Response, int]]: flask.Response: JSON with list of books from metadata provider. """ try: - from cwa_book_downloader.metadata_providers import ( + from shelfmark.metadata_providers import ( get_configured_provider, MetadataSearchOptions, SortOrder, @@ -1168,7 +1168,7 @@ def api_metadata_search() -> Union[Response, Tuple[Response, int]]: books_data = [asdict(book) for book in search_result.books] # Transform cover_url to local proxy URLs when caching is enabled - from cwa_book_downloader.core.utils import transform_cover_url + from shelfmark.core.utils import transform_cover_url for book_dict in books_data: if book_dict.get('cover_url'): cache_id = f"{book_dict['provider']}_{book_dict['provider_id']}" @@ -1201,7 +1201,7 @@ def api_metadata_book(provider: str, book_id: str) -> Union[Response, Tuple[Resp flask.Response: JSON with book details. """ try: - from cwa_book_downloader.metadata_providers import ( + from shelfmark.metadata_providers import ( get_provider, is_provider_registered, get_provider_kwargs, @@ -1225,7 +1225,7 @@ def api_metadata_book(provider: str, book_id: str) -> Union[Response, Tuple[Resp book_dict = asdict(book) # Transform cover_url to local proxy URL when caching is enabled - from cwa_book_downloader.core.utils import transform_cover_url + from shelfmark.core.utils import transform_cover_url if book_dict.get('cover_url'): cache_id = f"{provider}_{book_id}" book_dict['cover_url'] = transform_cover_url(book_dict['cover_url'], cache_id) @@ -1256,12 +1256,12 @@ def api_releases() -> Union[Response, Tuple[Response, int]]: flask.Response: JSON with list of available releases. """ try: - from cwa_book_downloader.metadata_providers import ( + from shelfmark.metadata_providers import ( get_provider, is_provider_registered, get_provider_kwargs, ) - from cwa_book_downloader.release_sources import get_source, list_available_sources, serialize_column_config + from shelfmark.release_sources import get_source, list_available_sources, serialize_column_config from dataclasses import asdict provider = request.args.get('provider', '').strip() @@ -1302,8 +1302,8 @@ def api_releases() -> Union[Response, Tuple[Response, int]]: if source_filter: sources_to_search = [source_filter] else: - # Search all available sources - sources_to_search = [src["name"] for src in list_available_sources()] + # Search only enabled sources + sources_to_search = [src["name"] for src in list_available_sources() if src["enabled"]] # Search each source for releases all_releases = [] @@ -1338,7 +1338,7 @@ def api_releases() -> Union[Response, Tuple[Response, int]]: # Convert book to dict and transform cover_url book_dict = asdict(book) - from cwa_book_downloader.core.utils import transform_cover_url + from shelfmark.core.utils import transform_cover_url if book_dict.get('cover_url'): cache_id = f"{provider}_{book_id}" book_dict['cover_url'] = transform_cover_url(book_dict['cover_url'], cache_id) @@ -1387,7 +1387,7 @@ def api_release_sources() -> Union[Response, Tuple[Response, int]]: flask.Response: JSON list of available release sources. """ try: - from cwa_book_downloader.release_sources import list_available_sources + from shelfmark.release_sources import list_available_sources sources = list_available_sources() return jsonify(sources) except Exception as e: @@ -1405,12 +1405,12 @@ def api_settings_get_all() -> Union[Response, Tuple[Response, int]]: flask.Response: JSON with all settings tabs. """ try: - from cwa_book_downloader.core.settings_registry import serialize_all_settings + from shelfmark.core.settings_registry import serialize_all_settings # Ensure settings are registered by importing settings modules # This triggers the @register_settings decorators - import cwa_book_downloader.config.settings # noqa: F401 - import cwa_book_downloader.config.security # noqa: F401 + import shelfmark.config.settings # noqa: F401 + import shelfmark.config.security # noqa: F401 data = serialize_all_settings(include_values=True) return jsonify(data) @@ -1432,14 +1432,14 @@ def api_settings_get_tab(tab_name: str) -> Union[Response, Tuple[Response, int]] flask.Response: JSON with tab settings and values. """ try: - from cwa_book_downloader.core.settings_registry import ( + from shelfmark.core.settings_registry import ( get_settings_tab, serialize_tab, ) # Ensure settings are registered - import cwa_book_downloader.config.settings # noqa: F401 - import cwa_book_downloader.config.security # noqa: F401 + import shelfmark.config.settings # noqa: F401 + import shelfmark.config.security # noqa: F401 tab = get_settings_tab(tab_name) if not tab: @@ -1467,14 +1467,14 @@ def api_settings_update_tab(tab_name: str) -> Union[Response, Tuple[Response, in flask.Response: JSON with update result. """ try: - from cwa_book_downloader.core.settings_registry import ( + from shelfmark.core.settings_registry import ( get_settings_tab, update_settings, ) # Ensure settings are registered - import cwa_book_downloader.config.settings # noqa: F401 - import cwa_book_downloader.config.security # noqa: F401 + import shelfmark.config.settings # noqa: F401 + import shelfmark.config.security # noqa: F401 tab = get_settings_tab(tab_name) if not tab: @@ -1516,11 +1516,11 @@ def api_settings_execute_action(tab_name: str, action_key: str) -> Union[Respons flask.Response: JSON with action result. """ try: - from cwa_book_downloader.core.settings_registry import execute_action + from shelfmark.core.settings_registry import execute_action # Ensure settings are registered - import cwa_book_downloader.config.settings # noqa: F401 - import cwa_book_downloader.config.security # noqa: F401 + import shelfmark.config.settings # noqa: F401 + import shelfmark.config.security # noqa: F401 # Get current form values if provided (for testing with unsaved values) current_values = request.get_json(silent=True) or {} diff --git a/cwa_book_downloader/metadata_providers/README.md b/shelfmark/metadata_providers/README.md similarity index 92% rename from cwa_book_downloader/metadata_providers/README.md rename to shelfmark/metadata_providers/README.md index 2c23b78d..a43f8dd8 100644 --- a/cwa_book_downloader/metadata_providers/README.md +++ b/shelfmark/metadata_providers/README.md @@ -117,7 +117,7 @@ class MetadataProvider(ABC): ### Provider Registration ```python -from cwa_book_downloader.metadata_providers import register_provider +from shelfmark.metadata_providers import register_provider @register_provider("my_provider") class MyProvider(MetadataProvider): @@ -127,7 +127,7 @@ class MyProvider(MetadataProvider): ### Getting Providers ```python -from cwa_book_downloader.metadata_providers import ( +from shelfmark.metadata_providers import ( get_provider, get_configured_provider, get_provider_kwargs, @@ -155,7 +155,7 @@ exists = is_provider_registered("hardcover") # True ### Sort Options ```python -from cwa_book_downloader.metadata_providers import get_provider_sort_options +from shelfmark.metadata_providers import get_provider_sort_options # Get sort options for a specific provider options = get_provider_sort_options("hardcover") @@ -167,12 +167,12 @@ options = get_provider_sort_options() # Uses METADATA_PROVIDER from config ## Creating a New Provider -1. Create a new file in `cwa_book_downloader/metadata_providers/` (e.g., `my_provider.py`) +1. Create a new file in `shelfmark/metadata_providers/` (e.g., `my_provider.py`) 2. Implement the provider: ```python -from cwa_book_downloader.metadata_providers import ( +from shelfmark.metadata_providers import ( BookMetadata, DisplayField, MetadataProvider, @@ -181,13 +181,13 @@ from cwa_book_downloader.metadata_providers import ( SortOrder, register_provider, ) -from cwa_book_downloader.core.settings_registry import ( +from shelfmark.core.settings_registry import ( register_settings, HeadingField, PasswordField, ActionButton, ) -from cwa_book_downloader.core.config import config +from shelfmark.core.config import config @register_provider("my_provider") @@ -251,7 +251,7 @@ def my_provider_settings(): ```python try: - from cwa_book_downloader.metadata_providers import my_provider # noqa: F401 + from shelfmark.metadata_providers import my_provider # noqa: F401 except ImportError: pass # Provider is optional ``` @@ -273,8 +273,8 @@ def get_provider_kwargs(provider_name: str) -> Dict: Providers should use the `@cacheable` decorator for API calls: ```python -from cwa_book_downloader.core.cache import cacheable -from cwa_book_downloader.config.env import ( +from shelfmark.core.cache import cacheable +from shelfmark.config.env import ( METADATA_CACHE_SEARCH_TTL, METADATA_CACHE_BOOK_TTL, ) @@ -295,7 +295,7 @@ def get_book(self, book_id: str): For providers with rate limits (like Open Library), implement a rate limiter: ```python -from cwa_book_downloader.metadata_providers.openlibrary import RateLimiter +from shelfmark.metadata_providers.openlibrary import RateLimiter # 90 requests per 60 seconds rate_limiter = RateLimiter(max_requests=90, window_seconds=60) diff --git a/cwa_book_downloader/metadata_providers/__init__.py b/shelfmark/metadata_providers/__init__.py similarity index 94% rename from cwa_book_downloader/metadata_providers/__init__.py rename to shelfmark/metadata_providers/__init__.py index f0726dc5..2a7cc606 100644 --- a/cwa_book_downloader/metadata_providers/__init__.py +++ b/shelfmark/metadata_providers/__init__.py @@ -242,7 +242,7 @@ def register_provider_kwargs(name: str): Example: @register_provider_kwargs("hardcover") def _hardcover_kwargs() -> Dict: - from cwa_book_downloader.core.config import config + from shelfmark.core.config import config return {"api_key": config.get("HARDCOVER_API_KEY", "")} """ def decorator(fn): @@ -281,7 +281,7 @@ def is_provider_registered(provider_name: str) -> bool: def is_provider_enabled(provider_name: str) -> bool: """Check if a provider is enabled in settings.""" - from cwa_book_downloader.core.config import config as app_config + from shelfmark.core.config import config as app_config # Refresh config to get latest settings app_config.refresh() @@ -298,7 +298,7 @@ def get_enabled_providers() -> List[str]: def get_configured_provider(content_type: str = "ebook") -> Optional[MetadataProvider]: """Get the currently configured metadata provider for the content type.""" - from cwa_book_downloader.core.config import config as app_config + from shelfmark.core.config import config as app_config # Refresh config to ensure we have the latest saved settings app_config.refresh() @@ -327,7 +327,7 @@ def get_configured_provider(content_type: str = "ebook") -> Optional[MetadataPro def _get_configured_provider_name() -> str: """Get the currently configured metadata provider name from config.""" - from cwa_book_downloader.core.config import config as app_config + from shelfmark.core.config import config as app_config app_config.refresh() return app_config.get("METADATA_PROVIDER", "") @@ -365,7 +365,7 @@ def get_provider_search_fields(provider_name: Optional[str] = None) -> List[Dict def get_provider_default_sort(provider_name: Optional[str] = None) -> str: """Get the default sort order for a metadata provider.""" - from cwa_book_downloader.core.config import config as app_config + from shelfmark.core.config import config as app_config if provider_name is None: provider_name = _get_configured_provider_name() @@ -385,8 +385,8 @@ def sync_metadata_provider_selection() -> None: auto-select the first enabled provider. This should be called after enabling/disabling a provider. """ - from cwa_book_downloader.core.config import config as app_config - from cwa_book_downloader.core.settings_registry import save_config_file, load_config_file + from shelfmark.core.config import config as app_config + from shelfmark.core.settings_registry import save_config_file, load_config_file app_config.refresh() @@ -411,16 +411,16 @@ def sync_metadata_provider_selection() -> None: # Import provider implementations to trigger registration # These must be imported AFTER the base classes and registry are defined try: - from cwa_book_downloader.metadata_providers import hardcover # noqa: F401, E402 + from shelfmark.metadata_providers import hardcover # noqa: F401, E402 except ImportError: pass # Hardcover provider is optional try: - from cwa_book_downloader.metadata_providers import openlibrary # noqa: F401, E402 + from shelfmark.metadata_providers import openlibrary # noqa: F401, E402 except ImportError: pass # Open Library provider is optional try: - from cwa_book_downloader.metadata_providers import googlebooks # noqa: F401, E402 + from shelfmark.metadata_providers import googlebooks # noqa: F401, E402 except ImportError: pass # Google Books provider is optional diff --git a/cwa_book_downloader/metadata_providers/googlebooks.py b/shelfmark/metadata_providers/googlebooks.py similarity index 98% rename from cwa_book_downloader/metadata_providers/googlebooks.py rename to shelfmark/metadata_providers/googlebooks.py index d17e4b4a..ccc72dae 100644 --- a/cwa_book_downloader/metadata_providers/googlebooks.py +++ b/shelfmark/metadata_providers/googlebooks.py @@ -9,9 +9,9 @@ API Documentation: https://developers.google.com/books/docs/v1/using import requests from typing import Any, Dict, List, Optional -from cwa_book_downloader.core.cache import cacheable -from cwa_book_downloader.core.logger import setup_logger -from cwa_book_downloader.core.settings_registry import ( +from shelfmark.core.cache import cacheable +from shelfmark.core.logger import setup_logger +from shelfmark.core.settings_registry import ( register_settings, CheckboxField, PasswordField, @@ -19,8 +19,8 @@ from cwa_book_downloader.core.settings_registry import ( ActionButton, HeadingField, ) -from cwa_book_downloader.core.config import config as app_config -from cwa_book_downloader.metadata_providers import ( +from shelfmark.core.config import config as app_config +from shelfmark.metadata_providers import ( BookMetadata, DisplayField, MetadataProvider, diff --git a/cwa_book_downloader/metadata_providers/hardcover.py b/shelfmark/metadata_providers/hardcover.py similarity index 98% rename from cwa_book_downloader/metadata_providers/hardcover.py rename to shelfmark/metadata_providers/hardcover.py index 3ea755c2..36b2634f 100644 --- a/cwa_book_downloader/metadata_providers/hardcover.py +++ b/shelfmark/metadata_providers/hardcover.py @@ -4,9 +4,9 @@ import requests from datetime import datetime from typing import Any, Dict, List, Optional -from cwa_book_downloader.core.cache import cacheable -from cwa_book_downloader.core.logger import setup_logger -from cwa_book_downloader.core.settings_registry import ( +from shelfmark.core.cache import cacheable +from shelfmark.core.logger import setup_logger +from shelfmark.core.settings_registry import ( register_settings, CheckboxField, PasswordField, @@ -14,8 +14,8 @@ from cwa_book_downloader.core.settings_registry import ( ActionButton, HeadingField, ) -from cwa_book_downloader.core.config import config as app_config -from cwa_book_downloader.metadata_providers import ( +from shelfmark.core.config import config as app_config +from shelfmark.metadata_providers import ( BookMetadata, DisplayField, MetadataProvider, @@ -704,7 +704,7 @@ class HardcoverProvider(MetadataProvider): def _test_hardcover_connection(current_values: Optional[Dict[str, Any]] = None) -> Dict[str, Any]: """Test the Hardcover API connection using current form values.""" - from cwa_book_downloader.core.config import config as app_config + from shelfmark.core.config import config as app_config current_values = current_values or {} @@ -753,7 +753,7 @@ def _test_hardcover_connection(current_values: Optional[Dict[str, Any]] = None) def _save_connected_username(username: Optional[str]) -> None: """Save or clear the connected username in config.""" - from cwa_book_downloader.core.settings_registry import save_config_file, load_config_file + from shelfmark.core.settings_registry import save_config_file, load_config_file config = load_config_file("hardcover") if username: @@ -765,7 +765,7 @@ def _save_connected_username(username: Optional[str]) -> None: def _get_connected_username() -> Optional[str]: """Get the stored connected username.""" - from cwa_book_downloader.core.settings_registry import load_config_file + from shelfmark.core.settings_registry import load_config_file config = load_config_file("hardcover") return config.get("_connected_username") diff --git a/cwa_book_downloader/metadata_providers/openlibrary.py b/shelfmark/metadata_providers/openlibrary.py similarity index 98% rename from cwa_book_downloader/metadata_providers/openlibrary.py rename to shelfmark/metadata_providers/openlibrary.py index 4b7a672f..63c30335 100644 --- a/cwa_book_downloader/metadata_providers/openlibrary.py +++ b/shelfmark/metadata_providers/openlibrary.py @@ -8,16 +8,16 @@ from typing import Any, Deque, Dict, List, Optional import requests -from cwa_book_downloader.core.cache import cacheable -from cwa_book_downloader.core.logger import setup_logger -from cwa_book_downloader.core.settings_registry import ( +from shelfmark.core.cache import cacheable +from shelfmark.core.logger import setup_logger +from shelfmark.core.settings_registry import ( register_settings, CheckboxField, SelectField, ActionButton, HeadingField, ) -from cwa_book_downloader.metadata_providers import ( +from shelfmark.metadata_providers import ( BookMetadata, DisplayField, MetadataProvider, diff --git a/cwa_book_downloader/release_sources/__init__.py b/shelfmark/release_sources/__init__.py similarity index 91% rename from cwa_book_downloader/release_sources/__init__.py rename to shelfmark/release_sources/__init__.py index 7c443376..0d3bbb07 100644 --- a/cwa_book_downloader/release_sources/__init__.py +++ b/shelfmark/release_sources/__init__.py @@ -6,8 +6,8 @@ from enum import Enum from threading import Event from typing import List, Optional, Dict, Type, Callable, Literal, Any -from cwa_book_downloader.core.models import DownloadTask -from cwa_book_downloader.metadata_providers import BookMetadata +from shelfmark.core.models import DownloadTask +from shelfmark.metadata_providers import BookMetadata class ReleaseProtocol(str, Enum): @@ -106,6 +106,13 @@ class LeadingCellConfig: uppercase: bool = False # Force uppercase for badge text +@dataclass +class SourceActionButton: + """Action button configuration for a release source.""" + label: str # Button text (e.g., "Refresh search") + action: str = "expand" # Action type: "expand" triggers expand_search + + @dataclass class ReleaseColumnConfig: """Complete column configuration for a release source.""" @@ -115,6 +122,7 @@ class ReleaseColumnConfig: online_servers: Optional[List[str]] = None # For IRC: list of currently online server nicks cache_ttl_seconds: Optional[int] = None # How long to cache results (default: 5 min) supported_filters: Optional[List[str]] = None # Which filters this source supports: ["format", "language"] + action_button: Optional[SourceActionButton] = None # Custom action button (replaces default expand search) def serialize_column_config(config: ReleaseColumnConfig) -> Dict[str, Any]: @@ -166,6 +174,13 @@ def serialize_column_config(config: ReleaseColumnConfig) -> Dict[str, Any]: if config.supported_filters is not None: result["supported_filters"] = config.supported_filters + # Include action button if specified (replaces default expand search) + if config.action_button is not None: + result["action_button"] = { + "label": config.action_button.label, + "action": config.action_button.action, + } + return result @@ -212,6 +227,7 @@ class ReleaseSource(ABC): name: str # "direct", "prowlarr" display_name: str # "Direct Download", "Prowlarr" supported_content_types: List[str] = ["ebook", "audiobook"] # Content types this source supports + can_be_default: bool = True # Whether this source can be selected as default in settings @abstractmethod def search( @@ -303,6 +319,7 @@ def list_available_sources() -> List[dict]: "display_name": instance.display_name, "enabled": instance.is_available(), "supported_content_types": getattr(instance, 'supported_content_types', ["ebook", "audiobook"]), + "can_be_default": getattr(instance, 'can_be_default', True), }) return result @@ -316,6 +333,6 @@ def get_source_display_name(name: str) -> str: # Import source implementations to trigger registration # These must be imported AFTER the base classes and registry are defined -from cwa_book_downloader.release_sources import direct_download # noqa: F401, E402 -from cwa_book_downloader.release_sources import prowlarr # noqa: F401, E402 -from cwa_book_downloader.release_sources import irc # noqa: F401, E402 +from shelfmark.release_sources import direct_download # noqa: F401, E402 +from shelfmark.release_sources import prowlarr # noqa: F401, E402 +from shelfmark.release_sources import irc # noqa: F401, E402 diff --git a/cwa_book_downloader/release_sources/direct_download.py b/shelfmark/release_sources/direct_download.py similarity index 98% rename from cwa_book_downloader/release_sources/direct_download.py rename to shelfmark/release_sources/direct_download.py index 64513535..708a4467 100644 --- a/cwa_book_downloader/release_sources/direct_download.py +++ b/shelfmark/release_sources/direct_download.py @@ -13,15 +13,15 @@ import requests from bs4 import BeautifulSoup, NavigableString, Tag -from cwa_book_downloader.download import http as downloader -from cwa_book_downloader.download import network -from cwa_book_downloader.config.env import DEBUG_SKIP_SOURCES, TMP_DIR -from cwa_book_downloader.core.config import config -from cwa_book_downloader.core.utils import CONTENT_TYPES -from cwa_book_downloader.core.logger import setup_logger -from cwa_book_downloader.core.models import BookInfo, SearchFilters, DownloadTask -from cwa_book_downloader.metadata_providers import BookMetadata -from cwa_book_downloader.release_sources import ( +from shelfmark.download import http as downloader +from shelfmark.download import network +from shelfmark.config.env import DEBUG_SKIP_SOURCES, TMP_DIR +from shelfmark.core.config import config +from shelfmark.core.utils import CONTENT_TYPES +from shelfmark.core.logger import setup_logger +from shelfmark.core.models import BookInfo, SearchFilters, DownloadTask +from shelfmark.metadata_providers import BookMetadata +from shelfmark.release_sources import ( Release, ReleaseProtocol, ReleaseSource, @@ -64,7 +64,7 @@ _AA_PAGE_SOURCES = frozenset({"aa-slow-nowait", "aa-slow-wait"}) def _get_md5_url_template(source_id: str) -> Optional[str]: """Get URL template for MD5-based sources from centralized config.""" - from cwa_book_downloader.core import mirrors + from shelfmark.core import mirrors if source_id == "zlib": return mirrors.get_zlib_url_template() @@ -75,7 +75,7 @@ def _get_md5_url_template(source_id: str) -> Optional[str]: def _get_libgen_domains() -> List[str]: """Get LibGen domains from centralized config.""" - from cwa_book_downloader.core import mirrors + from shelfmark.core import mirrors return mirrors.get_libgen_mirrors() _LIBGEN_GET_PATTERNS = [ @@ -658,7 +658,7 @@ def _get_download_urls_from_welib( status_callback: Optional[Callable[[str, Optional[str]], None]] = None ) -> List[str]: """Get download URLs from welib.org (bypasser required).""" - from cwa_book_downloader.core import mirrors + from shelfmark.core import mirrors if not _is_source_enabled("welib"): return [] diff --git a/shelfmark/release_sources/irc/__init__.py b/shelfmark/release_sources/irc/__init__.py new file mode 100644 index 00000000..26e00233 --- /dev/null +++ b/shelfmark/release_sources/irc/__init__.py @@ -0,0 +1,11 @@ +"""IRC release source plugin. + +Searches and downloads ebooks from IRC channels via DCC protocol. +Available when IRC server, channel, and nickname are configured in settings. + +Based on OpenBooks (https://github.com/evan-buss/openbooks). +""" + +from shelfmark.release_sources.irc import source # noqa: F401 +from shelfmark.release_sources.irc import handler # noqa: F401 +from shelfmark.release_sources.irc import settings # noqa: F401 diff --git a/shelfmark/release_sources/irc/cache.py b/shelfmark/release_sources/irc/cache.py new file mode 100644 index 00000000..2b4874ec --- /dev/null +++ b/shelfmark/release_sources/irc/cache.py @@ -0,0 +1,289 @@ +"""Persistent file-based cache for IRC search results. + +Stores search results in CONFIG_DIR to survive container restarts. +IRC searches are slow and resource-intensive, so we cache aggressively. +""" + +import hashlib +import json +import time +from dataclasses import asdict +from pathlib import Path +from threading import Lock +from typing import Any, Dict, List, Optional + +from shelfmark.config import env +from shelfmark.core.logger import setup_logger +from shelfmark.release_sources import Release, ReleaseProtocol + +logger = setup_logger(__name__) + +# Cache file location +CACHE_FILE = Path(env.CONFIG_DIR) / "irc_cache.json" + +# Default TTL: 30 days (in seconds) +DEFAULT_CACHE_TTL = 30 * 24 * 60 * 60 + +# Lock for thread-safe file access +_cache_lock = Lock() + + +def _generate_cache_key(provider: str, provider_id: str) -> str: + """Generate a cache key from provider and provider_id.""" + return f"{provider}:{provider_id}" + + +def _load_cache() -> Dict[str, Any]: + """Load cache from disk.""" + try: + if CACHE_FILE.exists(): + return json.loads(CACHE_FILE.read_text()) + except (json.JSONDecodeError, IOError) as e: + logger.warning(f"Failed to load IRC cache: {e}") + return {"entries": {}, "version": 1} + + +def _save_cache(cache: Dict[str, Any]) -> None: + """Save cache to disk.""" + try: + CACHE_FILE.write_text(json.dumps(cache, indent=2)) + except IOError as e: + logger.error(f"Failed to save IRC cache: {e}") + + +def _release_to_dict(release: Release) -> Dict[str, Any]: + """Convert Release to a JSON-serializable dict.""" + data = asdict(release) + # Convert enum to string + if data.get("protocol"): + data["protocol"] = data["protocol"].value if hasattr(data["protocol"], "value") else str(data["protocol"]) + return data + + +def _dict_to_release(data: Dict[str, Any]) -> Release: + """Convert dict back to Release object.""" + # Convert protocol string back to enum + if data.get("protocol"): + try: + data["protocol"] = ReleaseProtocol(data["protocol"]) + except (ValueError, KeyError): + data["protocol"] = None + return Release(**data) + + +def get_cached_results( + provider: str, + provider_id: str, + ttl_seconds: Optional[int] = None +) -> Optional[Dict[str, Any]]: + """ + Get cached search results for a book. + + Args: + provider: Metadata provider name (e.g., "hardcover", "openlibrary") + provider_id: Book ID in the provider's system + ttl_seconds: Cache TTL in seconds (from settings) + + Returns: + Dict with 'releases' (List[Release]) and 'online_servers' (List[str]), + or None if not cached or expired + """ + from shelfmark.core.config import config + + if ttl_seconds is None: + ttl_value = config.get("IRC_CACHE_TTL", DEFAULT_CACHE_TTL) + # Config values are stored as strings, convert to int + ttl_seconds = int(ttl_value) if ttl_value else DEFAULT_CACHE_TTL + + # TTL of 0 means cache forever + if ttl_seconds == 0: + ttl_seconds = float('inf') + + cache_key = _generate_cache_key(provider, provider_id) + + with _cache_lock: + cache = _load_cache() + entry = cache.get("entries", {}).get(cache_key) + + if not entry: + return None + + # Check expiration + cached_at = entry.get("cached_at", 0) + age = time.time() - cached_at + + if age > ttl_seconds: + logger.debug(f"IRC cache expired for '{title}' (age: {age:.0f}s > TTL: {ttl_seconds}s)") + # Don't delete here - let cleanup handle it + return None + + # Convert dicts back to Release objects + releases = [_dict_to_release(r) for r in entry.get("releases", [])] + online_servers = entry.get("online_servers", []) + title = entry.get("title", "") + + logger.info(f"IRC cache hit for '{title}' ({len(releases)} releases, age: {age:.0f}s)") + + return { + "releases": releases, + "online_servers": online_servers, + "cached_at": cached_at, + } + + +def cache_results( + provider: str, + provider_id: str, + title: str, + releases: List[Release], + online_servers: Optional[List[str]] = None +) -> None: + """ + Cache search results for a book. + + Args: + provider: Metadata provider name + provider_id: Book ID in the provider's system + title: Book title (for logging/display) + releases: List of Release objects from search + online_servers: List of online server nicks (optional) + """ + cache_key = _generate_cache_key(provider, provider_id) + + with _cache_lock: + cache = _load_cache() + + if "entries" not in cache: + cache["entries"] = {} + + cache["entries"][cache_key] = { + "provider": provider, + "provider_id": provider_id, + "title": title, + "releases": [_release_to_dict(r) for r in releases], + "online_servers": list(online_servers) if online_servers else [], + "cached_at": time.time(), + } + + _save_cache(cache) + logger.info(f"Cached {len(releases)} IRC releases for '{title}'") + + +def invalidate_cache(provider: str, provider_id: str) -> bool: + """ + Remove a specific entry from the cache. + + Args: + provider: Metadata provider name + provider_id: Book ID in the provider's system + + Returns: + True if entry was found and removed + """ + cache_key = _generate_cache_key(provider, provider_id) + + with _cache_lock: + cache = _load_cache() + entry = cache.get("entries", {}).get(cache_key) + title = entry.get("title", cache_key) if entry else cache_key + + if cache_key in cache.get("entries", {}): + del cache["entries"][cache_key] + _save_cache(cache) + logger.info(f"Invalidated IRC cache for '{title}'") + return True + + return False + + +def clear_cache() -> int: + """ + Clear all cached entries. + + Returns: + Number of entries cleared + """ + with _cache_lock: + cache = _load_cache() + count = len(cache.get("entries", {})) + cache["entries"] = {} + _save_cache(cache) + logger.info(f"Cleared {count} IRC cache entries") + return count + + +def cleanup_expired(ttl_seconds: Optional[int] = None) -> int: + """ + Remove all expired entries from the cache. + + Returns: + Number of entries removed + """ + from shelfmark.core.config import config + + if ttl_seconds is None: + ttl_value = config.get("IRC_CACHE_TTL", DEFAULT_CACHE_TTL) + # Config values are stored as strings, convert to int + ttl_seconds = int(ttl_value) if ttl_value else DEFAULT_CACHE_TTL + + current_time = time.time() + removed = 0 + + with _cache_lock: + cache = _load_cache() + entries = cache.get("entries", {}) + + expired_keys = [ + key for key, entry in entries.items() + if current_time - entry.get("cached_at", 0) > ttl_seconds + ] + + for key in expired_keys: + del entries[key] + removed += 1 + + if removed: + _save_cache(cache) + logger.info(f"Cleaned up {removed} expired IRC cache entries") + + return removed + + +def get_cache_stats() -> Dict[str, Any]: + """ + Get cache statistics. + + Returns: + Dict with cache stats + """ + from shelfmark.core.config import config + + ttl_value = config.get("IRC_CACHE_TTL", DEFAULT_CACHE_TTL) + # Config values are stored as strings, convert to int + ttl_seconds = int(ttl_value) if ttl_value else DEFAULT_CACHE_TTL + current_time = time.time() + + with _cache_lock: + cache = _load_cache() + entries = cache.get("entries", {}) + + total = len(entries) + expired = sum( + 1 for entry in entries.values() + if current_time - entry.get("cached_at", 0) > ttl_seconds + ) + + # Calculate total releases cached + total_releases = sum( + len(entry.get("releases", [])) + for entry in entries.values() + ) + + return { + "total_entries": total, + "expired_entries": expired, + "valid_entries": total - expired, + "total_releases": total_releases, + "ttl_seconds": ttl_seconds, + "cache_file": str(CACHE_FILE), + } diff --git a/cwa_book_downloader/release_sources/irc/client.py b/shelfmark/release_sources/irc/client.py similarity index 93% rename from cwa_book_downloader/release_sources/irc/client.py rename to shelfmark/release_sources/irc/client.py index de49ef44..dd1ff598 100644 --- a/cwa_book_downloader/release_sources/irc/client.py +++ b/shelfmark/release_sources/irc/client.py @@ -1,31 +1,23 @@ """IRC client implementation using raw sockets. -Minimal IRC client for IRC Highway ebook searches. +Minimal IRC client for ebook searches. """ -import random import re import socket import ssl -import string import time from dataclasses import dataclass, field from enum import Enum, auto from typing import Iterator, Optional -from cwa_book_downloader.core.logger import setup_logger +from shelfmark.core.logger import setup_logger from .dcc import DCCOffer, parse_dcc_send logger = setup_logger(__name__) -# IRC Highway server details -DEFAULT_SERVER = "irc.irchighway.net" -DEFAULT_PORT_TLS = 6697 -DEFAULT_PORT_PLAIN = 6667 -DEFAULT_CHANNEL = "ebooks" - # Timing POST_CONNECT_DELAY = 2.0 # Seconds to wait after connect before joining SOCKET_TIMEOUT = 300.0 # 5 minutes - long because we wait for DCC offers @@ -72,19 +64,25 @@ class IRCConnectionError(IRCError): class IRCClient: - """Minimal IRC client for per-request ebook searches on IRC Highway.""" + """Minimal IRC client for per-request ebook searches.""" def __init__( self, - nick: Optional[str] = None, - server: str = DEFAULT_SERVER, - port: Optional[int] = None, + nick: str, + server: str, + port: int, use_tls: bool = True, - version: str = "CWA-BD 1.0", + version: str = "Shelfmark 1.0", ): - self.nick = nick or self._generate_nick() + if not nick: + raise IRCError("IRC nickname is required") + if not server: + raise IRCError("IRC server is required") + if not port: + raise IRCError("IRC port is required") + self.nick = nick self.server = server - self.port = port or (DEFAULT_PORT_TLS if use_tls else DEFAULT_PORT_PLAIN) + self.port = port self.use_tls = use_tls self.version = version @@ -95,12 +93,6 @@ class IRCClient: # Track online servers (elevated users in channel) self.online_servers: set[str] = set() - @staticmethod - def _generate_nick() -> str: - """Generate random nickname like 'cwa_abc123'.""" - suffix = ''.join(random.choices(string.ascii_lowercase + string.digits, k=6)) - return f"cwa_{suffix}" - def connect(self) -> None: """Connect to IRC server, send USER/NICK, and wait for welcome.""" logger.info(f"Connecting to {self.server}:{self.port} (TLS={self.use_tls})") diff --git a/cwa_book_downloader/release_sources/irc/dcc.py b/shelfmark/release_sources/irc/dcc.py similarity index 98% rename from cwa_book_downloader/release_sources/irc/dcc.py rename to shelfmark/release_sources/irc/dcc.py index 087a7dce..b8009440 100644 --- a/cwa_book_downloader/release_sources/irc/dcc.py +++ b/shelfmark/release_sources/irc/dcc.py @@ -11,7 +11,7 @@ from pathlib import Path from threading import Event from typing import Callable, Optional -from cwa_book_downloader.core.logger import setup_logger +from shelfmark.core.logger import setup_logger logger = setup_logger(__name__) diff --git a/cwa_book_downloader/release_sources/irc/handler.py b/shelfmark/release_sources/irc/handler.py similarity index 78% rename from cwa_book_downloader/release_sources/irc/handler.py rename to shelfmark/release_sources/irc/handler.py index e574d9ac..4424db0d 100644 --- a/cwa_book_downloader/release_sources/irc/handler.py +++ b/shelfmark/release_sources/irc/handler.py @@ -7,12 +7,12 @@ from pathlib import Path from threading import Event from typing import Callable, Optional -from cwa_book_downloader.core.config import config -from cwa_book_downloader.core.logger import setup_logger -from cwa_book_downloader.core.models import DownloadTask -from cwa_book_downloader.release_sources import DownloadHandler, register_handler +from shelfmark.core.config import config +from shelfmark.core.logger import setup_logger +from shelfmark.core.models import DownloadTask +from shelfmark.release_sources import DownloadHandler, register_handler -from .client import DEFAULT_CHANNEL, IRCClient +from .client import IRCClient from .dcc import DCCError, download_dcc logger = setup_logger(__name__) @@ -33,7 +33,17 @@ class IRCDownloadHandler(DownloadHandler): download_request = task.task_id logger.info(f"IRC download: {download_request[:60]}...") - nick = config.get("IRC_NICK") or None + # Get IRC settings + server = config.get("IRC_SERVER", "") + port = config.get("IRC_PORT", 6697) + channel = config.get("IRC_CHANNEL", "") + nick = config.get("IRC_NICK", "") + + if not server or not channel or not nick: + logger.warning("IRC not fully configured") + status_callback("failed", "IRC not configured") + return None + client = None def check_cancelled() -> bool: @@ -47,14 +57,14 @@ class IRCDownloadHandler(DownloadHandler): try: # Phase 1: Connect to IRC - status_callback("resolving", "Connecting to IRC") + status_callback("resolving", f"Connecting to {server}") if check_cancelled(): return None - client = IRCClient(nick=nick) + client = IRCClient(nick, server, port) client.connect() - client.join_channel(DEFAULT_CHANNEL) + client.join_channel(channel) # Phase 2: Send download request status_callback("resolving", "Requesting file from bot") @@ -63,7 +73,7 @@ class IRCDownloadHandler(DownloadHandler): return None # Send the full request line to the channel - client.send_message(f"#{DEFAULT_CHANNEL}", download_request) + client.send_message(f"#{channel}", download_request) # Phase 3: Wait for DCC offer status_callback("resolving", "Waiting for bot response") @@ -85,7 +95,7 @@ class IRCDownloadHandler(DownloadHandler): ext = Path(offer.filename).suffix.lstrip('.') or task.format or "epub" # Stage to temp directory (lazy import to avoid circular import) - from cwa_book_downloader.download.orchestrator import get_staging_path + from shelfmark.download.orchestrator import get_staging_path staging_path = get_staging_path(task.task_id, ext) download_dcc( diff --git a/cwa_book_downloader/release_sources/irc/parser.py b/shelfmark/release_sources/irc/parser.py similarity index 98% rename from cwa_book_downloader/release_sources/irc/parser.py rename to shelfmark/release_sources/irc/parser.py index cf851182..ed1f6ff3 100644 --- a/cwa_book_downloader/release_sources/irc/parser.py +++ b/shelfmark/release_sources/irc/parser.py @@ -9,8 +9,8 @@ from dataclasses import dataclass from pathlib import Path from typing import Optional -from cwa_book_downloader.core.config import config -from cwa_book_downloader.core.logger import setup_logger +from shelfmark.core.config import config +from shelfmark.core.logger import setup_logger logger = setup_logger(__name__) diff --git a/shelfmark/release_sources/irc/settings.py b/shelfmark/release_sources/irc/settings.py new file mode 100644 index 00000000..f3fc7265 --- /dev/null +++ b/shelfmark/release_sources/irc/settings.py @@ -0,0 +1,119 @@ +"""IRC settings registration. + +Registers IRC settings for the settings UI. +""" + +from shelfmark.core.settings_registry import ( + ActionButton, + HeadingField, + NumberField, + SelectField, + TextField, + register_settings, +) + + +def _clear_irc_cache(): + """Clear all cached IRC search results.""" + from shelfmark.release_sources.irc.cache import clear_cache, get_cache_stats + + stats = get_cache_stats() + count = clear_cache() + return { + "success": True, + "message": f"Cleared {count} cached searches ({stats['total_releases']} releases)", + } + + +@register_settings( + name="irc", + display_name="IRC", + icon="download", + order=56, +) +def irc_settings(): + """Define IRC source settings.""" + return [ + HeadingField( + key="heading", + title="IRC", + description=( + "Search and download books from IRC ebook channels. " + "This source connects via IRC and uses DCC for file transfers. " + "Configure the connection details below to enable IRC search. " + "Note: DCC requires direct TCP connections to arbitrary ports, " + "which may not work behind strict firewalls or NAT." + ), + ), + + TextField( + key="IRC_SERVER", + label="Server", + placeholder="e.g. irc.example.net", + description="IRC server hostname", + required=True, + env_supported=True, + ), + + NumberField( + key="IRC_PORT", + label="Port", + default=6697, + description="IRC server port (usually 6697 for TLS, 6667 for plain)", + env_supported=True, + ), + + TextField( + key="IRC_CHANNEL", + label="Channel", + placeholder="e.g. ebooks", + description="Channel name without the # prefix", + required=True, + env_supported=True, + ), + + TextField( + key="IRC_NICK", + label="Nickname", + placeholder="e.g. myusername", + description="Your IRC nickname (required). Must be unique on the IRC network.", + required=True, + env_supported=True, + ), + + TextField( + key="IRC_SEARCH_BOT", + label="Search bot", + placeholder="e.g. search", + description="The search bot to query for results", + env_supported=True, + ), + + HeadingField( + key="cache_heading", + title="Search Cache", + description=( + "IRC search results are cached to reduce load on IRC servers. " + "Use the Refresh button in the release modal to force a new search." + ), + ), + + SelectField( + key="IRC_CACHE_TTL", + label="Cache Duration", + description="How long to keep cached search results before they expire.", + options=[ + {"value": "2592000", "label": "30 days"}, + {"value": "0", "label": "Forever (until manually cleared)"}, + ], + default="2592000", # 30 days + ), + + ActionButton( + key="clear_irc_cache", + label="Clear Cache", + description="Remove all cached IRC search results.", + style="danger", + callback=_clear_irc_cache, + ), + ] diff --git a/cwa_book_downloader/release_sources/irc/source.py b/shelfmark/release_sources/irc/source.py similarity index 72% rename from cwa_book_downloader/release_sources/irc/source.py rename to shelfmark/release_sources/irc/source.py index e03c1bd5..ed41a9a3 100644 --- a/cwa_book_downloader/release_sources/irc/source.py +++ b/shelfmark/release_sources/irc/source.py @@ -1,6 +1,6 @@ -"""IRC Highway release source plugin. +"""IRC release source plugin. -Searches IRC Highway #ebooks channel for book releases. +Searches IRC ebook channels for book releases. """ import tempfile @@ -8,11 +8,11 @@ import time from pathlib import Path from typing import List, Optional -from cwa_book_downloader.api.websocket import ws_manager -from cwa_book_downloader.core.config import config -from cwa_book_downloader.core.logger import setup_logger -from cwa_book_downloader.metadata_providers import BookMetadata -from cwa_book_downloader.release_sources import ( +from shelfmark.api.websocket import ws_manager +from shelfmark.core.config import config +from shelfmark.core.logger import setup_logger +from shelfmark.metadata_providers import BookMetadata +from shelfmark.release_sources import ( ColumnColorHint, ColumnRenderType, ColumnSchema, @@ -22,10 +22,11 @@ from cwa_book_downloader.release_sources import ( ReleaseColumnConfig, ReleaseProtocol, ReleaseSource, + SourceActionButton, register_source, ) -from .client import DEFAULT_CHANNEL, IRCClient +from .client import IRCClient from .dcc import DCCError, download_dcc from .parser import SearchResult, extract_results_from_zip, parse_results_file @@ -62,11 +63,12 @@ def _enforce_rate_limit() -> None: @register_source("irc") class IRCReleaseSource(ReleaseSource): - """Search IRC Highway #ebooks for book releases.""" + """Search IRC channels for book releases.""" name = "irc" - display_name = "IRC Highway" + display_name = "IRC" supported_content_types = ["ebook"] # IRC only supports ebooks + can_be_default = False # Exclude from default source options (requires deliberate selection) def __init__(self): # Track online servers from most recent search @@ -74,8 +76,11 @@ class IRCReleaseSource(ReleaseSource): @classmethod def is_available(cls) -> bool: - """Check if IRC is enabled in settings.""" - return config.get("IRC_ENABLED", False) + """Check if IRC is configured (server, channel, and nick are set).""" + server = config.get("IRC_SERVER", "") + channel = config.get("IRC_CHANNEL", "") + nick = config.get("IRC_NICK", "") + return bool(server and channel and nick) def get_column_config(self) -> ReleaseColumnConfig: """Configure UI columns for IRC results.""" @@ -111,6 +116,7 @@ class IRCReleaseSource(ReleaseSource): online_servers=list(self._online_servers) if self._online_servers else None, cache_ttl_seconds=1800, # 30 minutes - IRC searches are slow, cache longer supported_filters=["format"], # IRC has no language metadata + action_button=SourceActionButton(label="Refresh search"), ) def search( @@ -120,7 +126,25 @@ class IRCReleaseSource(ReleaseSource): languages: Optional[List[str]] = None, content_type: str = "ebook" ) -> List[Release]: - """Search IRC Highway for books matching metadata.""" + """Search IRC for books matching metadata. + + The expand_search parameter is repurposed for IRC as a "refresh" flag. + When True, it bypasses the cache and forces a fresh search. + """ + from .cache import get_cached_results, cache_results + + if not self.is_available(): + logger.debug("IRC source is disabled, skipping search") + return [] + + # Check cache first (unless expand_search/refresh is requested) + if not expand_search: + cached = get_cached_results(book.provider, book.provider_id) + if cached: + _emit_status("Using cached results", phase='complete') + self._online_servers = set(cached.get("online_servers", [])) + return cached["releases"] + # Build search query query = self._build_query(book) if not query: @@ -132,36 +156,49 @@ class IRCReleaseSource(ReleaseSource): # Enforce rate limit _enforce_rate_limit() - search_bot = config.get("IRC_SEARCH_BOT", "search") - nick = config.get("IRC_NICK") or None + # Get IRC settings + server = config.get("IRC_SERVER", "") + port = config.get("IRC_PORT", 6697) + channel = config.get("IRC_CHANNEL", "") + nick = config.get("IRC_NICK", "") + search_bot = config.get("IRC_SEARCH_BOT", "") client = None try: # Connect to IRC - _emit_status("Connecting to IRC Highway...", phase='connecting') - client = IRCClient(nick=nick) + _emit_status(f"Connecting to {server}...", phase='connecting') + client = IRCClient(nick, server, port) client.connect() - _emit_status("Joining #ebooks...", phase='connecting') - client.join_channel(DEFAULT_CHANNEL) + _emit_status(f"Joining #{channel}...", phase='connecting') + client.join_channel(channel) # Capture online servers (elevated users in channel) self._online_servers = client.online_servers # Send search request - client.send_message(f"#{DEFAULT_CHANNEL}", f"@{search_bot} {query}") + search_msg = f"@{search_bot} {query}" if search_bot else query + client.send_message(f"#{channel}", search_msg) # Wait for results DCC - this is the long wait - _emit_status("Connected to #ebooks - Waiting for results...", phase='searching') + _emit_status(f"Connected to #{channel} - Waiting for results...", phase='searching') offer = client.wait_for_dcc(timeout=60.0, result_type=True) if not offer: logger.info("No search results received") _emit_status("No results found", phase='complete') client.disconnect() + # Cache empty result to avoid repeated failed searches + cache_results( + book.provider, + book.provider_id, + book.title, + [], + list(self._online_servers) if self._online_servers else None + ) return [] # Download results file - _emit_status("Connected to #ebooks - Downloading results...", phase='downloading') + _emit_status(f"Connected to #{channel} - Downloading results...", phase='downloading') with tempfile.TemporaryDirectory() as tmpdir: result_path = Path(tmpdir) / offer.filename download_dcc(offer, result_path, timeout=30.0) @@ -176,7 +213,18 @@ class IRCReleaseSource(ReleaseSource): # Convert to Release objects results = parse_results_file(content) - return self._convert_to_releases(results) + releases = self._convert_to_releases(results) + + # Cache results + cache_results( + book.provider, + book.provider_id, + book.title, + releases, + list(self._online_servers) if self._online_servers else None + ) + + return releases except DCCError as e: logger.error(f"DCC error during search: {e}") diff --git a/cwa_book_downloader/release_sources/prowlarr/__init__.py b/shelfmark/release_sources/prowlarr/__init__.py similarity index 68% rename from cwa_book_downloader/release_sources/prowlarr/__init__.py rename to shelfmark/release_sources/prowlarr/__init__.py index 57f3c163..56c901d9 100644 --- a/cwa_book_downloader/release_sources/prowlarr/__init__.py +++ b/shelfmark/release_sources/prowlarr/__init__.py @@ -11,14 +11,14 @@ Includes: """ # Import submodules to trigger decorator registration -from cwa_book_downloader.release_sources.prowlarr import source # noqa: F401 -from cwa_book_downloader.release_sources.prowlarr import handler # noqa: F401 -from cwa_book_downloader.release_sources.prowlarr import settings # noqa: F401 +from shelfmark.release_sources.prowlarr import source # noqa: F401 +from shelfmark.release_sources.prowlarr import handler # noqa: F401 +from shelfmark.release_sources.prowlarr import settings # noqa: F401 # Import clients to trigger client registration # This is in a try/except to handle optional dependencies gracefully try: - from cwa_book_downloader.release_sources.prowlarr import clients # noqa: F401 + from shelfmark.release_sources.prowlarr import clients # noqa: F401 except ImportError as e: # Log but don't fail - clients require optional dependencies import logging diff --git a/cwa_book_downloader/release_sources/prowlarr/api.py b/shelfmark/release_sources/prowlarr/api.py similarity index 98% rename from cwa_book_downloader/release_sources/prowlarr/api.py rename to shelfmark/release_sources/prowlarr/api.py index bbcbb0d4..d4163bdb 100644 --- a/cwa_book_downloader/release_sources/prowlarr/api.py +++ b/shelfmark/release_sources/prowlarr/api.py @@ -5,7 +5,7 @@ from urllib.parse import urljoin import requests -from cwa_book_downloader.core.logger import setup_logger +from shelfmark.core.logger import setup_logger logger = setup_logger(__name__) diff --git a/cwa_book_downloader/release_sources/prowlarr/cache.py b/shelfmark/release_sources/prowlarr/cache.py similarity index 97% rename from cwa_book_downloader/release_sources/prowlarr/cache.py rename to shelfmark/release_sources/prowlarr/cache.py index 074d51ea..bb0a3065 100644 --- a/cwa_book_downloader/release_sources/prowlarr/cache.py +++ b/shelfmark/release_sources/prowlarr/cache.py @@ -9,7 +9,7 @@ import time from threading import Lock from typing import Dict, Optional -from cwa_book_downloader.core.logger import setup_logger +from shelfmark.core.logger import setup_logger logger = setup_logger(__name__) diff --git a/cwa_book_downloader/release_sources/prowlarr/clients/__init__.py b/shelfmark/release_sources/prowlarr/clients/__init__.py similarity index 94% rename from cwa_book_downloader/release_sources/prowlarr/clients/__init__.py rename to shelfmark/release_sources/prowlarr/clients/__init__.py index 5c66f760..70fd202f 100644 --- a/cwa_book_downloader/release_sources/prowlarr/clients/__init__.py +++ b/shelfmark/release_sources/prowlarr/clients/__init__.py @@ -293,8 +293,8 @@ def get_all_clients() -> Dict[str, List[Type[DownloadClient]]]: # Import client implementations to trigger registration # These imports are at the bottom to avoid circular imports -from cwa_book_downloader.release_sources.prowlarr.clients import qbittorrent # noqa: F401, E402 -from cwa_book_downloader.release_sources.prowlarr.clients import nzbget # noqa: F401, E402 -from cwa_book_downloader.release_sources.prowlarr.clients import sabnzbd # noqa: F401, E402 -from cwa_book_downloader.release_sources.prowlarr.clients import transmission # noqa: F401, E402 -from cwa_book_downloader.release_sources.prowlarr.clients import deluge # noqa: F401, E402 +from shelfmark.release_sources.prowlarr.clients import qbittorrent # noqa: F401, E402 +from shelfmark.release_sources.prowlarr.clients import nzbget # noqa: F401, E402 +from shelfmark.release_sources.prowlarr.clients import sabnzbd # noqa: F401, E402 +from shelfmark.release_sources.prowlarr.clients import transmission # noqa: F401, E402 +from shelfmark.release_sources.prowlarr.clients import deluge # noqa: F401, E402 diff --git a/cwa_book_downloader/release_sources/prowlarr/clients/deluge.py b/shelfmark/release_sources/prowlarr/clients/deluge.py similarity index 97% rename from cwa_book_downloader/release_sources/prowlarr/clients/deluge.py rename to shelfmark/release_sources/prowlarr/clients/deluge.py index e4dcad76..2f5d918b 100644 --- a/cwa_book_downloader/release_sources/prowlarr/clients/deluge.py +++ b/shelfmark/release_sources/prowlarr/clients/deluge.py @@ -10,14 +10,14 @@ configurable via DELUGE_PORT), which requires the daemon to have import base64 from typing import Any, Optional, Tuple -from cwa_book_downloader.core.config import config -from cwa_book_downloader.core.logger import setup_logger -from cwa_book_downloader.release_sources.prowlarr.clients import ( +from shelfmark.core.config import config +from shelfmark.core.logger import setup_logger +from shelfmark.release_sources.prowlarr.clients import ( DownloadClient, DownloadStatus, register_client, ) -from cwa_book_downloader.release_sources.prowlarr.clients.torrent_utils import ( +from shelfmark.release_sources.prowlarr.clients.torrent_utils import ( extract_torrent_info, ) diff --git a/cwa_book_downloader/release_sources/prowlarr/clients/nzbget.py b/shelfmark/release_sources/prowlarr/clients/nzbget.py similarity index 98% rename from cwa_book_downloader/release_sources/prowlarr/clients/nzbget.py rename to shelfmark/release_sources/prowlarr/clients/nzbget.py index 19bd7332..72b84f5f 100644 --- a/cwa_book_downloader/release_sources/prowlarr/clients/nzbget.py +++ b/shelfmark/release_sources/prowlarr/clients/nzbget.py @@ -9,9 +9,9 @@ from typing import Any, Optional, Tuple import requests -from cwa_book_downloader.core.config import config -from cwa_book_downloader.core.logger import setup_logger -from cwa_book_downloader.release_sources.prowlarr.clients import ( +from shelfmark.core.config import config +from shelfmark.core.logger import setup_logger +from shelfmark.release_sources.prowlarr.clients import ( DownloadClient, DownloadStatus, register_client, diff --git a/cwa_book_downloader/release_sources/prowlarr/clients/qbittorrent.py b/shelfmark/release_sources/prowlarr/clients/qbittorrent.py similarity index 97% rename from cwa_book_downloader/release_sources/prowlarr/clients/qbittorrent.py rename to shelfmark/release_sources/prowlarr/clients/qbittorrent.py index 62956c75..16d81313 100644 --- a/cwa_book_downloader/release_sources/prowlarr/clients/qbittorrent.py +++ b/shelfmark/release_sources/prowlarr/clients/qbittorrent.py @@ -4,14 +4,14 @@ import time from types import SimpleNamespace from typing import List, Optional, Tuple -from cwa_book_downloader.core.config import config -from cwa_book_downloader.core.logger import setup_logger -from cwa_book_downloader.release_sources.prowlarr.clients import ( +from shelfmark.core.config import config +from shelfmark.core.logger import setup_logger +from shelfmark.release_sources.prowlarr.clients import ( DownloadClient, DownloadStatus, register_client, ) -from cwa_book_downloader.release_sources.prowlarr.clients.torrent_utils import ( +from shelfmark.release_sources.prowlarr.clients.torrent_utils import ( extract_torrent_info, ) diff --git a/cwa_book_downloader/release_sources/prowlarr/clients/sabnzbd.py b/shelfmark/release_sources/prowlarr/clients/sabnzbd.py similarity index 98% rename from cwa_book_downloader/release_sources/prowlarr/clients/sabnzbd.py rename to shelfmark/release_sources/prowlarr/clients/sabnzbd.py index 583ed5bd..4cdd023c 100644 --- a/cwa_book_downloader/release_sources/prowlarr/clients/sabnzbd.py +++ b/shelfmark/release_sources/prowlarr/clients/sabnzbd.py @@ -8,9 +8,9 @@ from typing import Any, Optional, Tuple import requests -from cwa_book_downloader.core.config import config -from cwa_book_downloader.core.logger import setup_logger -from cwa_book_downloader.release_sources.prowlarr.clients import ( +from shelfmark.core.config import config +from shelfmark.core.logger import setup_logger +from shelfmark.release_sources.prowlarr.clients import ( DownloadClient, DownloadStatus, register_client, diff --git a/cwa_book_downloader/release_sources/prowlarr/clients/torrent_utils.py b/shelfmark/release_sources/prowlarr/clients/torrent_utils.py similarity index 99% rename from cwa_book_downloader/release_sources/prowlarr/clients/torrent_utils.py rename to shelfmark/release_sources/prowlarr/clients/torrent_utils.py index ad18d425..808e3fc3 100644 --- a/cwa_book_downloader/release_sources/prowlarr/clients/torrent_utils.py +++ b/shelfmark/release_sources/prowlarr/clients/torrent_utils.py @@ -9,7 +9,7 @@ from urllib.parse import parse_qs, urlparse import requests -from cwa_book_downloader.core.logger import setup_logger +from shelfmark.core.logger import setup_logger logger = setup_logger(__name__) diff --git a/cwa_book_downloader/release_sources/prowlarr/clients/transmission.py b/shelfmark/release_sources/prowlarr/clients/transmission.py similarity index 97% rename from cwa_book_downloader/release_sources/prowlarr/clients/transmission.py rename to shelfmark/release_sources/prowlarr/clients/transmission.py index bd6f58ef..8b7ad1ee 100644 --- a/cwa_book_downloader/release_sources/prowlarr/clients/transmission.py +++ b/shelfmark/release_sources/prowlarr/clients/transmission.py @@ -6,14 +6,14 @@ Uses the transmission-rpc library to communicate with Transmission's RPC API. from typing import Optional, Tuple -from cwa_book_downloader.core.config import config -from cwa_book_downloader.core.logger import setup_logger -from cwa_book_downloader.release_sources.prowlarr.clients import ( +from shelfmark.core.config import config +from shelfmark.core.logger import setup_logger +from shelfmark.release_sources.prowlarr.clients import ( DownloadClient, DownloadStatus, register_client, ) -from cwa_book_downloader.release_sources.prowlarr.clients.torrent_utils import ( +from shelfmark.release_sources.prowlarr.clients.torrent_utils import ( extract_torrent_info, parse_transmission_url, ) diff --git a/cwa_book_downloader/release_sources/prowlarr/handler.py b/shelfmark/release_sources/prowlarr/handler.py similarity index 94% rename from cwa_book_downloader/release_sources/prowlarr/handler.py rename to shelfmark/release_sources/prowlarr/handler.py index 4147a2b6..7eec461f 100644 --- a/cwa_book_downloader/release_sources/prowlarr/handler.py +++ b/shelfmark/release_sources/prowlarr/handler.py @@ -5,18 +5,18 @@ from pathlib import Path from threading import Event from typing import Callable, Optional -from cwa_book_downloader.core.config import config -from cwa_book_downloader.core.logger import setup_logger -from cwa_book_downloader.core.models import DownloadTask -from cwa_book_downloader.core.utils import is_audiobook -from cwa_book_downloader.release_sources import DownloadHandler, register_handler -from cwa_book_downloader.release_sources.prowlarr.cache import get_release, remove_release -from cwa_book_downloader.release_sources.prowlarr.clients import ( +from shelfmark.core.config import config +from shelfmark.core.logger import setup_logger +from shelfmark.core.models import DownloadTask +from shelfmark.core.utils import is_audiobook +from shelfmark.release_sources import DownloadHandler, register_handler +from shelfmark.release_sources.prowlarr.cache import get_release, remove_release +from shelfmark.release_sources.prowlarr.clients import ( DownloadState, get_client, list_configured_clients, ) -from cwa_book_downloader.release_sources.prowlarr.utils import get_protocol, get_unique_path +from shelfmark.release_sources.prowlarr.utils import get_protocol, get_unique_path logger = setup_logger(__name__) @@ -259,7 +259,7 @@ class ProwlarrHandler(DownloadHandler): status_callback("resolving", "Staging file") use_copy = config.get("PROWLARR_USENET_ACTION", "move") == "copy" - from cwa_book_downloader.download.orchestrator import get_staging_dir + from shelfmark.download.orchestrator import get_staging_dir staging_dir = get_staging_dir() if source_path.is_dir(): diff --git a/cwa_book_downloader/release_sources/prowlarr/settings.py b/shelfmark/release_sources/prowlarr/settings.py similarity index 96% rename from cwa_book_downloader/release_sources/prowlarr/settings.py rename to shelfmark/release_sources/prowlarr/settings.py index 44e4c894..5976bd52 100644 --- a/cwa_book_downloader/release_sources/prowlarr/settings.py +++ b/shelfmark/release_sources/prowlarr/settings.py @@ -8,7 +8,7 @@ Registers Prowlarr settings as a group with multiple tabs: from typing import Any, Dict, List -from cwa_book_downloader.core.settings_registry import ( +from shelfmark.core.settings_registry import ( register_group, register_settings, CheckboxField, @@ -29,8 +29,8 @@ def _get_indexer_options() -> List[Dict[str, str]]: Returns list of {value: "id", label: "name (protocol)"} options. """ - from cwa_book_downloader.core.config import config - from cwa_book_downloader.core.logger import setup_logger + from shelfmark.core.config import config + from shelfmark.core.logger import setup_logger logger = setup_logger(__name__) @@ -41,7 +41,7 @@ def _get_indexer_options() -> List[Dict[str, str]]: return [] try: - from cwa_book_downloader.release_sources.prowlarr.api import ProwlarrClient + from shelfmark.release_sources.prowlarr.api import ProwlarrClient client = ProwlarrClient(url, api_key) indexers = client.get_enabled_indexers() @@ -74,9 +74,9 @@ def _get_indexer_options() -> List[Dict[str, str]]: def _test_prowlarr_connection(current_values: Dict[str, Any] = None) -> Dict[str, Any]: """Test the Prowlarr connection using current form values.""" - from cwa_book_downloader.core.config import config - from cwa_book_downloader.core.logger import setup_logger - from cwa_book_downloader.release_sources.prowlarr.api import ProwlarrClient + from shelfmark.core.config import config + from shelfmark.core.logger import setup_logger + from shelfmark.release_sources.prowlarr.api import ProwlarrClient logger = setup_logger(__name__) current_values = current_values or {} @@ -99,7 +99,7 @@ def _test_prowlarr_connection(current_values: Dict[str, Any] = None) -> Dict[str def _test_qbittorrent_connection(current_values: Dict[str, Any] = None) -> Dict[str, Any]: """Test the qBittorrent connection using current form values.""" - from cwa_book_downloader.core.config import config + from shelfmark.core.config import config current_values = current_values or {} @@ -125,8 +125,8 @@ def _test_qbittorrent_connection(current_values: Dict[str, Any] = None) -> Dict[ def _test_transmission_connection(current_values: Dict[str, Any] = None) -> Dict[str, Any]: """Test the Transmission connection using current form values.""" - from cwa_book_downloader.core.config import config - from cwa_book_downloader.release_sources.prowlarr.clients.torrent_utils import ( + from shelfmark.core.config import config + from shelfmark.release_sources.prowlarr.clients.torrent_utils import ( parse_transmission_url, ) @@ -163,7 +163,7 @@ def _test_transmission_connection(current_values: Dict[str, Any] = None) -> Dict def _test_deluge_connection(current_values: Dict[str, Any] = None) -> Dict[str, Any]: """Test the Deluge connection using current form values.""" - from cwa_book_downloader.core.config import config + from shelfmark.core.config import config current_values = current_values or {} @@ -198,7 +198,7 @@ def _test_deluge_connection(current_values: Dict[str, Any] = None) -> Dict[str, def _test_nzbget_connection(current_values: Dict[str, Any] = None) -> Dict[str, Any]: """Test the NZBGet connection using current form values.""" import requests - from cwa_book_downloader.core.config import config + from shelfmark.core.config import config current_values = current_values or {} @@ -230,7 +230,7 @@ def _test_nzbget_connection(current_values: Dict[str, Any] = None) -> Dict[str, def _test_sabnzbd_connection(current_values: Dict[str, Any] = None) -> Dict[str, Any]: """Test the SABnzbd connection using current form values.""" import requests - from cwa_book_downloader.core.config import config + from shelfmark.core.config import config current_values = current_values or {} diff --git a/cwa_book_downloader/release_sources/prowlarr/source.py b/shelfmark/release_sources/prowlarr/source.py similarity index 96% rename from cwa_book_downloader/release_sources/prowlarr/source.py rename to shelfmark/release_sources/prowlarr/source.py index 3b3f48b5..4a31b87f 100644 --- a/cwa_book_downloader/release_sources/prowlarr/source.py +++ b/shelfmark/release_sources/prowlarr/source.py @@ -3,10 +3,10 @@ import re from typing import List, Optional -from cwa_book_downloader.core.config import config -from cwa_book_downloader.core.logger import setup_logger -from cwa_book_downloader.metadata_providers import BookMetadata -from cwa_book_downloader.release_sources import ( +from shelfmark.core.config import config +from shelfmark.core.logger import setup_logger +from shelfmark.metadata_providers import BookMetadata +from shelfmark.release_sources import ( Release, ReleaseSource, register_source, @@ -18,9 +18,9 @@ from cwa_book_downloader.release_sources import ( LeadingCellConfig, LeadingCellType, ) -from cwa_book_downloader.release_sources.prowlarr.api import ProwlarrClient -from cwa_book_downloader.release_sources.prowlarr.cache import cache_release -from cwa_book_downloader.release_sources.prowlarr.utils import get_protocol_display +from shelfmark.release_sources.prowlarr.api import ProwlarrClient +from shelfmark.release_sources.prowlarr.cache import cache_release +from shelfmark.release_sources.prowlarr.utils import get_protocol_display logger = setup_logger(__name__) diff --git a/cwa_book_downloader/release_sources/prowlarr/utils.py b/shelfmark/release_sources/prowlarr/utils.py similarity index 100% rename from cwa_book_downloader/release_sources/prowlarr/utils.py rename to shelfmark/release_sources/prowlarr/utils.py diff --git a/src/frontend/index.html b/src/frontend/index.html index 874a8507..1d74dd88 100644 --- a/src/frontend/index.html +++ b/src/frontend/index.html @@ -3,7 +3,7 @@ - + @@ -12,12 +12,12 @@ - + - Book Downloader + Shelfmark