diff --git a/Dockerfile b/Dockerfile index d56952e5..5d7f22f4 100644 --- a/Dockerfile +++ b/Dockerfile @@ -18,7 +18,8 @@ ENV DEBIAN_FRONTEND=noninteractive \ # UID/GID will be handled by entrypoint script, but TZ/Locale are still needed LANG=en_US.UTF-8 \ LANGUAGE=en_US:en \ - LC_ALL=en_US.UTF-8 + LC_ALL=en_US.UTF-8 \ + APP_ENV=prod # Set ARG for build-time expansion (FLASK_PORT), ENV for runtime access ENV FLASK_PORT=8084 @@ -63,6 +64,11 @@ RUN pip install --no-cache-dir -r requirements.txt && \ # Clean root's pip cache rm -rf /root/.cache +# Add this line to grant read/execute permissions to others +RUN chmod -R o+rx /usr/bin/chromium && \ + chmod -R o+rx /usr/bin/chromedriver && \ + chmod -R o+w /usr/local/lib/python3.10/site-packages/seleniumbase/drivers/ + # Our custom wanabe curl RUN echo "#!/bin/sh" > /usr/local/bin/pyrequests && \ diff --git a/app.py b/app.py index 08d06dbc..9c5145b1 100644 --- a/app.py +++ b/app.py @@ -10,7 +10,7 @@ import typing from logger import setup_logger from config import _SUPPORTED_BOOK_LANGUAGE, BOOK_LANGUAGE -from env import FLASK_HOST, FLASK_PORT, FLASK_DEBUG +from env import FLASK_HOST, FLASK_PORT, FLASK_DEBUG, APP_ENV import backend from models import SearchFilters @@ -249,11 +249,12 @@ def internal_error(error: Exception) -> Union[Response, Tuple[Response, int]]: logger.error_trace(f"500 error: {error}") return jsonify({"error": "Internal server error"}), 500 + if __name__ == '__main__': # Register all routes with /request prefix register_dual_routes(app) - logger.info(f"Starting Flask application on {FLASK_HOST}:{FLASK_PORT}") + logger.info(f"Starting Flask application on {FLASK_HOST}:{FLASK_PORT} IN {APP_ENV} mode") app.run( host=FLASK_HOST, port=FLASK_PORT, diff --git a/book_manager.py b/book_manager.py index 72dde991..b67201a8 100644 --- a/book_manager.py +++ b/book_manager.py @@ -339,7 +339,7 @@ def _get_download_url(link: str, title: str) -> str: if countdown: sleep_time = int(countdown[0].text) logger.info(f"Waiting {sleep_time}s for {title}") - time.sleep(sleep_time + 5) + time.sleep(sleep_time) url = _get_download_url(link, title) else: url = download_links[0]['href'] diff --git a/cloudflare_bypasser.py b/cloudflare_bypasser.py index a33a2f0f..07458675 100644 --- a/cloudflare_bypasser.py +++ b/cloudflare_bypasser.py @@ -21,10 +21,21 @@ logger = setup_logger(__name__) network.init() DRIVER = None +DISPLAY = None LAST_USED = None LOCKED = threading.Lock() TENTATIVE_CURRENT_URL = None +def _reset_pyautogui_display_state(): + try: + import pyautogui + import Xlib.display + pyautogui._pyautogui_x11._display = ( + Xlib.display.Display(os.environ['DISPLAY']) + ) + except Exception as e: + logger.warning(f"Error resetting pyautogui display state: {e}") + def _is_bypassed(sb) -> bool: try: title = sb.get_title().lower() @@ -78,7 +89,6 @@ def _bypass(sb, max_retries: int = MAX_RETRY) -> None: sb.uc_gui_click_captcha() except Exception as e: time.sleep(DEFAULT_SLEEP) - sb.reconnect(DEFAULT_SLEEP) sb.uc_gui_click_captcha() if _is_bypassed(sb): @@ -127,8 +137,8 @@ def _get(url, retry : int = MAX_RETRY): try: logger.info(f"SB_GET: {url}") sb = _get_driver() - sb.uc_open_with_disconnect(url) - time.sleep(1) + sb.uc_open_with_reconnect(url, DEFAULT_SLEEP) + time.sleep(DEFAULT_SLEEP) _bypass(sb) return sb.page_source except Exception as e: @@ -157,27 +167,49 @@ def _init_driver(): return driver def _get_driver(): - global DRIVER + global DRIVER, DISPLAY global LAST_USED LAST_USED = time.time() + if env.DOCKERMODE and env.USE_CF_BYPASS and not DISPLAY: + from pyvirtualdisplay import Display + display = Display(visible=False, size=(800, 600)) + display.start() + logger.info("Display started") + DISPLAY = display + time.sleep(DEFAULT_SLEEP) + _reset_pyautogui_display_state() if not DRIVER: return _init_driver() return DRIVER def _reset_driver(): logger.info("Resetting driver...") - global DRIVER + global DRIVER, DISPLAY if DRIVER: - DRIVER.quit() + try: + DRIVER.quit() + DRIVER = None + except Exception as e: + logger.warning(f"Error quitting driver: {e}") + time.sleep(0.5) + if DISPLAY: + try: + DISPLAY.stop() + DISPLAY = None + except Exception as e: + logger.warning(f"Error stopping display: {e}") + time.sleep(0.5) try: - os.system("pkill -f xvfb") + os.system("pkill -f Xvfb") except Exception as e: - logger.warning(f"Error killing xvfb: {e}") + logger.warning(f"Error killing Xvfb: {e}") + time.sleep(0.5) try: os.system("pkill -f chrom") except Exception as e: logger.warning(f"Error killing chrom: {e}") - DRIVER = None + time.sleep(0.5) + def _cleanup_driver(): global LOCKED diff --git a/config.py b/config.py index c1884c49..9f663f12 100644 --- a/config.py +++ b/config.py @@ -90,9 +90,3 @@ if CUSTOM_SCRIPT: logger.warn(f"CUSTOM_SCRIPT {CUSTOM_SCRIPT} is not executable") CUSTOM_SCRIPT = "" -# Docker settings -if env.DOCKERMODE and env.USE_CF_BYPASS: - from pyvirtualdisplay import Display - display = Display(visible=False, size=(800, 600)) - display.start() - logger.info("Display started") diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index 2b8b75bf..5aaa6e34 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -10,6 +10,7 @@ services: environment: LOG_LEVEL: debug FLASK_DEBUG: true - USE_DOH: true - CUSTOM_DNS: cloudflare + APP_ENV: dev + # USE_DOH: true + # CUSTOM_DNS: cloudflare diff --git a/docker-compose.tor.dev.yml b/docker-compose.tor.dev.yml index d126d139..36677d01 100644 --- a/docker-compose.tor.dev.yml +++ b/docker-compose.tor.dev.yml @@ -12,4 +12,5 @@ services: - NET_RAW environment: LOG_LEVEL: debug - FLASK_DEBUG: true \ No newline at end of file + FLASK_DEBUG: true + APP_ENV: dev \ No newline at end of file diff --git a/docker-compose.tor.yml b/docker-compose.tor.yml index 6fbea80e..d2e24d68 100644 --- a/docker-compose.tor.yml +++ b/docker-compose.tor.yml @@ -9,6 +9,7 @@ services: FLASK_DEBUG: false ENABLE_TOR: true TZ: America/New_York + APP_ENV: prod ports: - 8084:8084 restart: unless-stopped diff --git a/docker-compose.yml b/docker-compose.yml index 98221850..20475861 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -8,6 +8,9 @@ services: USE_BOOK_TITLE: true FLASK_DEBUG: false TZ: America/New_York + APP_ENV: prod + UID: 1000 + GID: 100 ports: - 8084:8084 restart: unless-stopped diff --git a/entrypoint.sh b/entrypoint.sh index fa12c216..645cb727 100644 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -38,5 +38,14 @@ change_ownership /app change_ownership /var/log/cwa-book-downloader change_ownership /cwa-book-ingest -# Switch to the user (either newly created or existing) and execute the main command -exec sudo -E -u "$USERNAME" python3 -m app +# Set the command to run based on the environment +is_prod=$(echo "$APP_ENV" | tr '[:upper:]' '[:lower:]') +if [ "$is_prod" = "prod" ]; then + command="gunicorn -b 0.0.0.0:${FLASK_PORT:-8084} app:app" +else + command="python3 app.py" +fi + +echo "Running command: '$command' as '$USERNAME' in '$APP_ENV' mode" + +exec sudo -E -u "$USERNAME" $command diff --git a/env.py b/env.py index 0b057530..16b9c1b0 100644 --- a/env.py +++ b/env.py @@ -32,6 +32,7 @@ DOCKERMODE = string_to_bool(os.getenv("DOCKERMODE", "false")) _CUSTOM_DNS = os.getenv("CUSTOM_DNS", "").strip() USE_DOH = string_to_bool(os.getenv("USE_DOH", "false")) BYPASS_RELEASE_INACTIVE_MIN = int(os.getenv("BYPASS_RELEASE_INACTIVE_MIN", "5")) +APP_ENV = os.getenv("APP_ENV", "prod").lower() # Logging settings LOG_FILE = LOG_DIR / "cwa-bookd-downloader.log" diff --git a/requirements.txt b/requirements.txt index b77399bd..30f80184 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,3 +6,5 @@ pyvirtualdisplay dnspython pyautogui seleniumbase +gunicorn +python-xlib \ No newline at end of file