Skip to content

Commit 08e4b71

Browse files
Fix PR #632: Improve metadata provider global enable/disable functionality
- Fix critical circular import between cwa_functions.py and search_metadata.py - Add unified JSON parsing utility for metadata_providers_enabled setting - Enhance error handling for null/empty values and malformed JSON - Improve provider validation with proper attribute checks - Add early return when no active providers available - Standardize boolean logic across all provider enable/disable checks - Remove code duplication across auto_metadata.py, metadata_helper.py, search_metadata.py
1 parent f35a06f commit 08e4b71

5 files changed

Lines changed: 127 additions & 53 deletions

File tree

CONTRIBUTORS

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
CONTRIBUTORS
22

33
This file is automatically generated. DO NOT EDIT MANUALLY.
4-
Generated on: 2025-09-12T21:35:51.343565Z
4+
Generated on: 2025-09-12T21:51:34.133525Z
55

66
Upstream project: https://github.com/janeczku/calibre-web
77
Fork project (Calibre-Web Automated, since 2024): https://github.com/crocodilestick/calibre-web-automated
@@ -298,7 +298,7 @@ Copyright (C) 2024-2025 Calibre-Web Automated contributors
298298
- zhiyue (1 commits)
299299
# Fork Contributors (crocodilestick/calibre-web-automated)
300300

301-
- crocodilestick (650 commits)
301+
- crocodilestick (654 commits)
302302
- jmarmstrong1207 (73 commits)
303303
- demitrix (30 commits)
304304
- sirwolfgang (22 commits)

cps/auto_metadata.py

Lines changed: 23 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -81,29 +81,36 @@ def fetch_metadata_for_book(book_title: str, book_authors: str = "", user_id: Op
8181
provider_hierarchy = get_metadata_provider_hierarchy(cwa_settings)
8282

8383
# Get global enabled map for providers
84-
enabled_map_raw = cwa_settings.get('metadata_providers_enabled', '{}')
85-
try:
86-
if isinstance(enabled_map_raw, str):
87-
s = enabled_map_raw.strip()
88-
if s.startswith("'") and s.endswith("'"):
89-
s = s[1:-1]
90-
enabled_map = json.loads(s or '{}')
91-
elif isinstance(enabled_map_raw, dict):
92-
enabled_map = enabled_map_raw
93-
else:
94-
enabled_map = {}
95-
except Exception:
96-
enabled_map = {}
84+
from cps.cwa_functions import parse_metadata_providers_enabled, validate_and_cleanup_provider_enabled_map
85+
enabled_map_raw = parse_metadata_providers_enabled(
86+
cwa_settings.get('metadata_providers_enabled', '{}')
87+
)
9788

9889
# Get available metadata providers
99-
available_providers = {provider.__id__: provider for provider in cl if provider.active}
90+
available_providers = {
91+
provider.__id__: provider
92+
for provider in cl
93+
if (provider.active and hasattr(provider, '__id__') and provider.__id__)
94+
}
95+
96+
# Early return if no providers available
97+
if not available_providers:
98+
log.warning("No active metadata providers available")
99+
return None
100+
101+
# Validate and cleanup the enabled map
102+
enabled_map = validate_and_cleanup_provider_enabled_map(
103+
enabled_map_raw, list(available_providers.keys())
104+
)
100105

101106
# Try providers in order of preference
102107
for provider_id in provider_hierarchy:
103-
# Skip if globally disabled
104-
if not bool(enabled_map.get(provider_id, True)):
108+
# Check if explicitly disabled (default is enabled if not specified)
109+
is_enabled = enabled_map.get(provider_id, True)
110+
if not is_enabled:
105111
log.debug(f"Provider {provider_id} is globally disabled")
106112
continue
113+
107114
if provider_id not in available_providers:
108115
log.debug(f"Provider {provider_id} not available or inactive")
109116
continue

cps/cwa_functions.py

Lines changed: 82 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,81 @@
6060
## ##
6161
##————————————————————————————————————————————————————————————————————————————##
6262

63+
def parse_metadata_providers_enabled(raw_value):
64+
"""
65+
Parse the metadata_providers_enabled setting from various formats into a dict.
66+
67+
Args:
68+
raw_value: The raw value from database/settings (str, dict, bytes, or None)
69+
70+
Returns:
71+
dict: Provider ID to enabled status mapping. Empty dict on error.
72+
"""
73+
import json
74+
75+
try:
76+
# Handle None/null values
77+
if raw_value is None:
78+
return {}
79+
80+
# Handle bytes (from some database drivers)
81+
if isinstance(raw_value, bytes):
82+
raw_value = raw_value.decode('utf-8', errors='ignore')
83+
84+
# Handle string (most common case)
85+
if isinstance(raw_value, str):
86+
s = raw_value.strip()
87+
# Handle empty strings
88+
if not s:
89+
return {}
90+
# Strip surrounding single quotes if present from schema default
91+
if s.startswith("'") and s.endswith("'"):
92+
s = s[1:-1]
93+
# Handle empty string after quote stripping
94+
if not s:
95+
return {}
96+
data = json.loads(s)
97+
return data if isinstance(data, dict) else {}
98+
99+
# Handle dict (already parsed)
100+
elif isinstance(raw_value, dict):
101+
return raw_value
102+
103+
# Unknown type, return empty dict
104+
else:
105+
return {}
106+
107+
except (json.JSONDecodeError, ValueError, TypeError, AttributeError):
108+
return {}
109+
110+
def validate_and_cleanup_provider_enabled_map(enabled_map, available_provider_ids):
111+
"""
112+
Validate and cleanup the provider enabled map.
113+
114+
Args:
115+
enabled_map (dict): Current provider enabled map
116+
available_provider_ids (list): List of valid provider IDs
117+
118+
Returns:
119+
dict: Cleaned up enabled map with only valid providers
120+
"""
121+
if not isinstance(enabled_map, dict):
122+
return {}
123+
124+
if not isinstance(available_provider_ids, (list, tuple, set)):
125+
return {}
126+
127+
# Keep only valid provider IDs and boolean values
128+
cleaned_map = {}
129+
for provider_id, enabled in enabled_map.items():
130+
if (isinstance(provider_id, str) and
131+
provider_id.strip() and # Non-empty string
132+
provider_id in available_provider_ids):
133+
# Convert to boolean, handling various truthy/falsy values
134+
cleaned_map[provider_id] = bool(enabled)
135+
136+
return cleaned_map
137+
63138
@switch_theme.route("/cwa-switch-theme", methods=["GET", "POST"])
64139
@login_required_if_no_ano
65140
def cwa_switch_theme():
@@ -313,8 +388,13 @@ def set_cwa_settings():
313388
result[setting] = cwa_db.cwa_settings.get(setting, '["ibdb","google","dnb"]')
314389
elif setting == 'metadata_providers_enabled':
315390
# Validate dict mapping provider_id -> bool
316-
if isinstance(json_value, dict) and all(isinstance(k, str) and isinstance(v, bool) for k, v in json_value.items()):
317-
result[setting] = json.dumps(json_value)
391+
if isinstance(json_value, dict):
392+
# Just validate the basic structure - provider validation happens at runtime
393+
cleaned_map = {}
394+
for k, v in json_value.items():
395+
if isinstance(k, str) and isinstance(v, bool):
396+
cleaned_map[k] = v
397+
result[setting] = json.dumps(cleaned_map)
318398
else:
319399
result[setting] = cwa_db.cwa_settings.get(setting, '{}')
320400
else:

cps/metadata_helper.py

Lines changed: 7 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -59,25 +59,17 @@ def fetch_and_apply_metadata(book_id: int, user_enabled: bool = False) -> bool:
5959
provider_hierarchy = ["google", "douban", "dnb", "ibdb", "comicvine"]
6060

6161
# Global provider enablement map
62-
enabled_map_raw = cwa_settings.get('metadata_providers_enabled', '{}')
63-
try:
64-
if isinstance(enabled_map_raw, str):
65-
s = enabled_map_raw.strip()
66-
if s.startswith("'") and s.endswith("'"):
67-
s = s[1:-1]
68-
enabled_map = json.loads(s or '{}')
69-
elif isinstance(enabled_map_raw, dict):
70-
enabled_map = enabled_map_raw
71-
else:
72-
enabled_map = {}
73-
except Exception:
74-
enabled_map = {}
62+
from cps.cwa_functions import parse_metadata_providers_enabled
63+
enabled_map = parse_metadata_providers_enabled(
64+
cwa_settings.get('metadata_providers_enabled', '{}')
65+
)
7566

7667
# Try each provider in order
7768
metadata_found = False
7869
for provider_id in provider_hierarchy:
79-
# Skip if globally disabled
80-
if not bool(enabled_map.get(provider_id, True)):
70+
# Check if explicitly disabled (default is enabled if not specified)
71+
is_enabled = enabled_map.get(provider_id, True)
72+
if not is_enabled:
8173
log.debug(f"Provider {provider_id} is globally disabled")
8274
continue
8375
try:

cps/search_metadata.py

Lines changed: 13 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -78,25 +78,20 @@ def _get_global_provider_enabled_map() -> dict:
7878
from cwa_db import CWA_DB # type: ignore
7979
cwa_db = CWA_DB()
8080
settings = cwa_db.get_cwa_settings()
81-
raw = settings.get('metadata_providers_enabled', '{}')
82-
if isinstance(raw, bytes):
83-
raw = raw.decode('utf-8', errors='ignore')
84-
if isinstance(raw, str):
85-
s = raw.strip()
86-
# Strip surrounding single quotes if present from default schema value
87-
if s.startswith("'") and s.endswith("'"):
88-
s = s[1:-1]
89-
try:
90-
data = json.loads(s or '{}')
91-
except Exception:
92-
return {}
93-
return data if isinstance(data, dict) else {}
94-
elif isinstance(raw, dict):
95-
return raw
96-
except Exception:
97-
# On any failure, treat as all enabled
81+
82+
if not settings:
83+
log.warning("Could not get CWA settings for provider enabled map")
84+
return {}
85+
86+
from cps.cwa_functions import parse_metadata_providers_enabled
87+
return parse_metadata_providers_enabled(
88+
settings.get('metadata_providers_enabled', '{}')
89+
)
90+
except Exception as e:
91+
# On any failure, treat as all enabled (empty dict = all default to enabled)
92+
log.warning(f"Error loading provider enabled map: {e}")
9893
return {}
99-
return {}
94+
# Remove redundant return
10095

10196
@meta.route("/metadata/provider")
10297
@user_login_required

0 commit comments

Comments
 (0)