Skip to content

Commit 6baca4d

Browse files
committed
feat(amazon): Add system & user setting for Amazon region
Adds a setting to the system and user settings pages to allow users to select their Amazon region. This setting does nothing in this PR, but will be used in a PR further up the stack to select the correct Amazon regional site when searching for books.
1 parent a81e781 commit 6baca4d

10 files changed

Lines changed: 267 additions & 9 deletions

cps/admin.py

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99
import re
1010
import json
1111
import operator
12-
import time
1312
import sys
1413
import string
1514
import requests
@@ -26,7 +25,7 @@
2625
from markupsafe import Markup
2726
from .cw_login import current_user
2827
from flask_babel import gettext as _
29-
from flask_babel import get_locale, format_time, format_datetime, format_timedelta
28+
from flask_babel import get_locale, format_time, format_timedelta
3029
from sqlalchemy import and_
3130
from sqlalchemy.orm.attributes import flag_modified
3231
from sqlalchemy.exc import IntegrityError, OperationalError, InvalidRequestError
@@ -39,6 +38,10 @@
3938
valid_email, check_username
4039
from .embed_helper import get_calibre_binarypath
4140
from .gdriveutils import is_gdrive_ready, gdrive_support
41+
from .metadata_provider_settings import (
42+
apply_user_metadata_provider_regions,
43+
get_metadata_provider_context,
44+
)
4245
from .render_template import render_title_template, get_sidebar_config
4346
from .services.worker import WorkerThread
4447
from .usermanagement import user_login_required
@@ -1688,6 +1691,7 @@ def new_user():
16881691
config=config, translations=translations,
16891692
languages=languages, title=_("Add New User"), page="newuser",
16901693
kobo_support=kobo_support, registered_oauth=oauth_bb.oauth_check,
1694+
**get_metadata_provider_context(content),
16911695
opds_root_order_string=opds_context["opds_root_order_string"],
16921696
opds_hidden_entries_string=opds_context["opds_hidden_entries_string"],
16931697
opds_root_labels=opds_context["opds_root_labels"],
@@ -1940,6 +1944,7 @@ def edit_user(user_id):
19401944
hidden_custom_shelf_ids=hidden_custom_shelf_ids,
19411945
hidden_custom_shelves=hidden_custom_shelves,
19421946
visible_public_shelves=visible_public_shelves,
1947+
**get_metadata_provider_context(content),
19431948
opds_root_order_string=opds_context["opds_root_order_string"],
19441949
opds_hidden_entries_string=opds_context["opds_hidden_entries_string"],
19451950
opds_root_labels=opds_context["opds_root_labels"],
@@ -2530,6 +2535,7 @@ def _handle_new_user(to_save, content, languages, translations, kobo_support):
25302535
translations=translations,
25312536
languages=languages, title=_("Add new user"), page="newuser",
25322537
kobo_support=kobo_support, registered_oauth=oauth_bb.oauth_check,
2538+
**get_metadata_provider_context(content),
25332539
opds_root_order_string=opds_context["opds_root_order_string"],
25342540
opds_hidden_entries_string=opds_context["opds_hidden_entries_string"],
25352541
opds_root_labels=opds_context["opds_root_labels"],
@@ -2543,6 +2549,7 @@ def _handle_new_user(to_save, content, languages, translations, kobo_support):
25432549
content.denied_column_value = config.config_denied_column_value
25442550
# No default value for kobo sync shelf setting
25452551
content.kobo_only_shelves_sync = to_save.get("kobo_only_shelves_sync", 0) == "on"
2552+
apply_user_metadata_provider_regions(content, to_save)
25462553
ub.session.add(content)
25472554
ub.session.commit()
25482555
flash(_("User '%(user)s' created", user=content.name), category="success")
@@ -2635,6 +2642,7 @@ def _handle_edit_user(to_save, content, languages, translations, kobo_support):
26352642
# Auto-send and metadata fetch settings
26362643
content.auto_send_enabled = to_save.get("auto_send_enabled") == "on"
26372644
content.auto_metadata_fetch = to_save.get("auto_metadata_fetch") == "on"
2645+
apply_user_metadata_provider_regions(content, to_save)
26382646

26392647
# OPDS root order
26402648
opds_order_raw = to_save.get("opds_root_order", "").strip()
@@ -2808,6 +2816,7 @@ def _handle_edit_user(to_save, content, languages, translations, kobo_support):
28082816
content=content,
28092817
config=config,
28102818
registered_oauth=oauth_bb.oauth_check,
2819+
**get_metadata_provider_context(content),
28112820
opds_root_order_string=opds_context["opds_root_order_string"],
28122821
opds_hidden_entries_string=opds_context["opds_hidden_entries_string"],
28132822
opds_root_labels=opds_context["opds_root_labels"],
@@ -3113,4 +3122,4 @@ def restore_calibre_db():
31133122
except Exception:
31143123
pass
31153124
except Exception:
3116-
pass
3125+
pass

cps/config_sql.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,9 @@ class _Settings(_Base):
9090
config_kobo_sync = Column(Boolean, default=False)
9191
config_kobo_sync_magic_shelves = Column(Boolean, default=False)
9292

93+
# Metadata provider settings
94+
config_amazon_region = Column(String, default="")
95+
9396
# Sync read progress to Hardcover - should this be renamed?
9497
config_hardcover_sync = Column(Boolean, default=False)
9598
# Sync annotations to Hardcover
@@ -372,6 +375,8 @@ def load(self):
372375
else:
373376
setattr(self, k, v)
374377

378+
self.amazon_region = self.config_amazon_region or constants.AMAZON_REGIONS[0]
379+
375380
# Enforce unified logging to stdout for Docker deployments
376381
if self.config_logfile not in (logger.LOG_TO_STDOUT, logger.LOG_TO_STDERR):
377382
self.config_logfile = logger.LOG_TO_STDOUT

cps/constants.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -232,3 +232,26 @@ def _read_text(path: str, default: str = "") -> str:
232232
"zh_Hans_CN": _("Chinese (Simplified, China)"),
233233
"zh_Hant_TW": _("Chinese (Traditional, Taiwan)"),
234234
}
235+
236+
# NOTE: Keep "com" as the first entry, whichever region is first is the default region until one is chosen by the admin
237+
# or the user. All other regions should be kept in alphabetical order.
238+
AMAZON_REGIONS = [
239+
"com",
240+
"ae",
241+
"ca",
242+
"co.jp",
243+
"co.uk",
244+
"com.au",
245+
"com.br",
246+
"com.mx",
247+
"de",
248+
"es",
249+
"fr",
250+
"ie",
251+
"in",
252+
"it",
253+
"nl",
254+
"sa",
255+
"se",
256+
"sg",
257+
]

cps/cwa_functions.py

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,10 @@
3939
from .tasks.database import TaskReconnectDatabase
4040
from .tasks.auto_send import TaskAutoSend
4141
from .tasks.ops import TaskConvertLibraryRun, TaskEpubFixerRun
42+
from .metadata_provider_settings import (
43+
apply_config_metadata_provider_regions,
44+
get_metadata_provider_context,
45+
)
4246

4347
switch_theme = Blueprint('switch_theme', __name__)
4448
library_refresh = Blueprint('library_refresh', __name__)
@@ -968,6 +972,7 @@ def set_cwa_settings():
968972

969973
# Save Kobo Sync Magic Shelves setting (stored in app.db, not cwa.db)
970974
config.config_kobo_sync_magic_shelves = 'config_kobo_sync_magic_shelves' in request.form
975+
apply_config_metadata_provider_regions(config, request.form)
971976
config.save()
972977

973978
duplicate_criteria_changed = False
@@ -1053,10 +1058,13 @@ def set_cwa_settings():
10531058
next_scan_run = get_next_duplicate_scan_run(cwa_settings)
10541059

10551060
return render_title_template("cwa_settings.html", title=_("Calibre-Web Automated User Settings"), page="cwa-settings",
1056-
cwa_settings=cwa_settings, ignorable_formats=ignorable_formats, target_formats=target_formats,
1057-
automerge_options=automerge_options, autoingest_options=autoingest_options,
1058-
hardcover_token_available=hardcover_token_available,
1059-
next_duplicate_scan_run=next_scan_run, config=config)
1061+
cwa_settings=cwa_settings, ignorable_formats=ignorable_formats, target_formats=target_formats,
1062+
automerge_options=automerge_options, autoingest_options=autoingest_options,
1063+
hardcover_token_available=hardcover_token_available,
1064+
next_duplicate_scan_run=next_scan_run, config=config,
1065+
**get_metadata_provider_context(
1066+
amazon_region=getattr(config, "config_amazon_region", ""),
1067+
))
10601068

10611069

10621070
def get_next_duplicate_scan_run(settings):

cps/metadata_provider_settings.py

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
# -*- coding: utf-8 -*-
2+
# Calibre-Web Automated – fork of Calibre-Web
3+
# Copyright (C) 2026 Calibre-Web Automated contributors
4+
# SPDX-License-Identifier: GPL-3.0-or-later
5+
6+
"""Helpers for metadata provider region settings."""
7+
8+
from collections.abc import Sequence
9+
from typing import Any
10+
11+
from . import constants
12+
13+
14+
def validate_provider_region(value: Any, allowed_regions: Sequence[str]) -> str:
15+
"""Return a normalized region value or an empty string when invalid."""
16+
if value is None:
17+
return ""
18+
19+
region = str(value).strip().lower()
20+
return region if region in allowed_regions else ""
21+
22+
23+
def validate_amazon_region(value: Any) -> str:
24+
"""Return a validated Amazon region."""
25+
return validate_provider_region(value, constants.AMAZON_REGIONS)
26+
27+
28+
def get_effective_amazon_region(
29+
user: Any | None = None,
30+
*,
31+
config_amazon_region: Any | None = None,
32+
) -> str:
33+
"""Return the effective Amazon region using user, config, then default fallback."""
34+
user_region = validate_amazon_region(getattr(user, "amazon_region", None)) if user is not None else ""
35+
if user_region:
36+
return user_region
37+
38+
config_region = validate_amazon_region(config_amazon_region)
39+
if config_region:
40+
return config_region
41+
42+
return constants.AMAZON_REGIONS[0]
43+
44+
45+
def apply_config_metadata_provider_regions(config_obj: Any, form_data: dict[str, Any]) -> None:
46+
"""Apply validated metadata provider regions to a config-like object."""
47+
config_obj.config_amazon_region = validate_amazon_region(form_data.get("amazon_region"))
48+
49+
50+
def apply_user_metadata_provider_regions(user: Any, form_data: dict[str, Any]) -> None:
51+
"""Apply validated metadata provider regions to a user-like object."""
52+
user.amazon_region = validate_amazon_region(form_data.get("amazon_region"))
53+
54+
55+
def get_metadata_provider_context(
56+
user: Any | None = None,
57+
*,
58+
amazon_region: str | None = None,
59+
) -> dict[str, Any]:
60+
"""Return shared template context for metadata provider region settings."""
61+
return {
62+
"amazon_regions": constants.AMAZON_REGIONS,
63+
"amazon_region": amazon_region
64+
if amazon_region is not None
65+
else getattr(user, "amazon_region", "")
66+
if user is not None
67+
else "",
68+
}

cps/templates/cwa_settings.html

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -734,6 +734,19 @@ <h4 class="settings-section-header">🔖 {{_('Hardcover Auto-Fetch Settings')}}<
734734
</div>
735735
</div>
736736

737+
<div class="settings-container">
738+
<div class="form-group">
739+
<h4 class="settings-section-header">{{_('Metadata Provider Settings')}}</h4>
740+
<label for="amazon_region" style="padding-right: 10px; margin-bottom: 16px !important;">{{_('Choose an Amazon region')}}:</label>
741+
<select class="cwa-settings-select" name="amazon_region" id="amazon_region">
742+
<option value="">({{ _('Default') }})</option>
743+
{% for region in amazon_regions -%}
744+
<option value="{{ region }}"{% if amazon_region == region %} selected{% endif %}>www.amazon.{{ region }}</option>
745+
{% endfor %}
746+
</select>
747+
</div>
748+
</div>
749+
737750
<div class="settings-container">
738751
<h4 class="settings-section-header">{{_('Web UI Settings')}}</h4>
739752

cps/templates/user_edit.html

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -292,6 +292,19 @@ <h4 class="settings-section-header">{{_('Book Language Filter')}}</h4>
292292
</div>
293293
{% endif %}
294294

295+
<div class="settings-container" style="max-width: none; margin-inline: 0rem;">
296+
<div class="form-group" style="margin-bottom: 20px; display: flex; flex-direction: column; gap: 0.5rem;">
297+
<h4 class="settings-section-header">{{_('Metadata Provider Settings')}}</h4>
298+
<label for="amazon_region" style="margin-bottom: 8px !important;">{{_('Choose an Amazon region')}}:</label>
299+
<select class="form-control" name="amazon_region" id="amazon_region">
300+
<option value="">({{ _('Global Default') }})</option>
301+
{% for region in amazon_regions -%}
302+
<option value="{{ region }}"{% if amazon_region == region %} selected{% endif %}>www.amazon.{{ region }}</option>
303+
{% endfor %}
304+
</select>
305+
</div>
306+
</div>
307+
295308
<!-- OAuth & API Integrations Card -->
296309
{% if (registered_oauth.keys()| length > 0 and not new_user and profile) or (hardcover_support and not new_user) or (kobo_support and not new_user) %}
297310
<div class="settings-container" style="max-width: none; margin-inline: 0rem;">

cps/ub.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -287,6 +287,7 @@ class User(UserBase, Base):
287287
auto_send_enabled = Column(Boolean, default=False)
288288
# Allow entering additional email addresses on send-to-eReader
289289
allow_additional_ereader_emails = Column(Boolean, default=True)
290+
amazon_region = Column(String, default=None)
290291

291292

292293
if oauth_support:
@@ -335,6 +336,7 @@ def __init__(self):
335336
self.role = None
336337
self.name = None
337338
self.auto_send_enabled = False
339+
self.amazon_region = None
338340
self.loadSettings()
339341

340342
def loadSettings(self):
@@ -356,6 +358,7 @@ def loadSettings(self):
356358
self.kobo_only_shelves_sync = data.kobo_only_shelves_sync
357359
self.hardcover_token = data.hardcover_token
358360
self.auto_send_enabled = data.auto_send_enabled
361+
self.amazon_region = data.amazon_region
359362
def role_admin(self):
360363
return False
361364

@@ -926,6 +929,16 @@ def migrate_user_table(engine, _session):
926929
print(f"[Migration] Warning: Could not update duplicates sidebar setting: {e}")
927930
_session.rollback()
928931

932+
# Migration to add Amazon region setting
933+
try:
934+
_session.query(exists().where(User.amazon_region)).scalar()
935+
_session.commit()
936+
except exc.OperationalError:
937+
with engine.connect() as conn:
938+
trans = conn.begin()
939+
conn.execute(text("ALTER TABLE user ADD column 'amazon_region' String DEFAULT ''"))
940+
trans.commit()
941+
929942
def migrate_oauth_provider_table(engine, _session):
930943
try:
931944
_session.query(exists().where(OAuthProvider.oauth_base_url)).scalar()
@@ -1017,6 +1030,21 @@ def migrate_config_table(engine, _session):
10171030
# Don't raise - let CWA continue without this feature
10181031
pass
10191032

1033+
# Add Amazon region configuration
1034+
try:
1035+
_session.execute(text("SELECT config_amazon_region FROM settings LIMIT 1"))
1036+
_session.commit()
1037+
except exc.OperationalError:
1038+
try:
1039+
_safe_session_rollback(_session, "settings.config_amazon_region")
1040+
_run_ddl_with_retry(
1041+
engine,
1042+
"ALTER TABLE settings ADD column 'config_amazon_region' String DEFAULT ''",
1043+
)
1044+
except Exception as e:
1045+
log.error("Failed to add config_amazon_region column: %s", e)
1046+
pass
1047+
10201048

10211049
def migrate_magic_shelf_table(engine, _session):
10221050
"""Migrate magic_shelf table to add new columns."""

cps/web.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
import json
99
import mimetypes
1010
import chardet # dependency of requests
11-
import copy
1211
import importlib
1312
import re
1413
import zipfile
@@ -43,6 +42,10 @@
4342
from .cw_babel import get_available_locale
4443
from .usermanagement import login_required_if_no_ano
4544
from .kobo_sync_status import remove_synced_book
45+
from .metadata_provider_settings import (
46+
apply_user_metadata_provider_regions,
47+
get_metadata_provider_context,
48+
)
4649
from . import magic_shelf
4750
from .render_template import render_title_template
4851
from .kobo_sync_status import change_archived_books
@@ -55,7 +58,6 @@
5558
# CWA Imports
5659
import sqlite3
5760
import time
58-
import time
5961

6062
import sys
6163
sys.path.insert(1, '/app/calibre-web-automated/scripts/')
@@ -2432,6 +2434,8 @@ def change_profile(kobo_support, hardcover_support, local_oauth_check, oauth_sta
24322434
current_user.auto_send_enabled = to_save.get("auto_send_enabled") == "on"
24332435
current_user.auto_metadata_fetch = to_save.get("auto_metadata_fetch") == "on"
24342436
current_user.allow_additional_ereader_emails = to_save.get("allow_additional_ereader_emails") == "on"
2437+
2438+
apply_user_metadata_provider_regions(current_user, to_save)
24352439

24362440
# Handle hidden magic shelf templates and custom shelves
24372441
from . import magic_shelf
@@ -2648,6 +2652,7 @@ def change_profile(kobo_support, hardcover_support, local_oauth_check, oauth_sta
26482652
kobo_support=kobo_support,
26492653
hardcover_support=hardcover_support,
26502654
registered_oauth=local_oauth_check,
2655+
**get_metadata_provider_context(current_user),
26512656
oauth_status=oauth_status)
26522657

26532658
val = 0
@@ -2774,6 +2779,7 @@ def profile():
27742779
title=_(f"{current_user.name.capitalize()}'s Profile", name=current_user.name),
27752780
page="me",
27762781
registered_oauth=local_oauth_check,
2782+
**get_metadata_provider_context(current_user),
27772783
oauth_status=oauth_status)
27782784

27792785

0 commit comments

Comments
 (0)