Fix: Config initialization and category fallback (#442)

- Added more robust config directory initialisation and file creation
- Fixed category fallback not triggering correctly for one content type
if another is cached
This commit is contained in:
Alex
2026-01-13 18:26:51 +00:00
committed by GitHub
parent fbc3dd2552
commit be5382cd1e
2 changed files with 82 additions and 11 deletions
+71
View File
@@ -284,7 +284,78 @@ def save_config_file(tab_name: str, values: Dict[str, Any]) -> bool:
return False
def initialize_default_configs() -> bool:
"""Initialize config files with default values on first startup.
Creates config files for all settings tabs that don't have one yet,
populating them with field default values. This ensures config files
exist from first startup rather than only being created on explicit save.
Returns:
True if initialization succeeded or was skipped (already initialized),
False if there was an error accessing the config directory.
"""
try:
config_dir = _get_config_dir()
# Check if config directory exists and is writable
if not config_dir.exists():
logger.warning(f"Config directory does not exist: {config_dir}")
return False
# Test writability
test_file = config_dir / ".write_test"
try:
test_file.touch()
test_file.unlink()
except (OSError, PermissionError) as e:
logger.warning(f"Config directory is not writable: {config_dir} - {e}")
return False
initialized_tabs = []
for tab in get_all_settings_tabs():
config_path = _get_config_file_path(tab.name)
# Skip if config file already exists
if config_path.exists():
continue
# Collect default values for all fields
defaults = {}
for field in tab.fields:
# Skip non-value fields
if isinstance(field, (ActionButton, HeadingField)):
continue
# Only include fields that have a non-None default
if field.default is not None:
defaults[field.key] = field.default
# Create config file with defaults if we have any
if defaults:
_ensure_config_dir(tab.name)
try:
with open(config_path, 'w') as f:
json.dump(defaults, f, indent=2)
initialized_tabs.append(tab.name)
except Exception as e:
logger.error(f"Failed to initialize config for {tab.name}: {e}")
if initialized_tabs:
logger.info(f"Initialized default configs for: {initialized_tabs}")
return True
except Exception as e:
logger.error(f"Error during config initialization: {e}")
return False
def sync_env_to_config() -> None:
# Initialize default configs first (for fresh installs)
initialize_default_configs()
for tab in get_all_settings_tabs():
values_to_sync = {}
+11 -11
View File
@@ -12,20 +12,20 @@ import { LanguageMultiSelect } from './LanguageMultiSelect';
import { LANGUAGE_OPTION_ALL, LANGUAGE_OPTION_DEFAULT, getLanguageFilterValues, releaseLanguageMatchesFilter } from '../utils/languageFilters';
// Module-level cache for release search results
// Key format: `${provider}:${provider_id}:${source}`
// Key format: `${provider}:${provider_id}:${source}:${contentType}`
// This persists across modal open/close cycles
const releaseCache = new Map<string, ReleasesResponse>();
function getCacheKey(provider: string, providerId: string, source: string): string {
return `${provider}:${providerId}:${source}`;
function getCacheKey(provider: string, providerId: string, source: string, contentType: string): string {
return `${provider}:${providerId}:${source}:${contentType}`;
}
// Default cache TTL (5 minutes) - sources can override via column_config.cache_ttl_seconds
const DEFAULT_CACHE_TTL_MS = 5 * 60 * 1000;
const cacheTimestamps = new Map<string, number>();
function getCachedReleases(provider: string, providerId: string, source: string): ReleasesResponse | null {
const key = getCacheKey(provider, providerId, source);
function getCachedReleases(provider: string, providerId: string, source: string, contentType: string): ReleasesResponse | null {
const key = getCacheKey(provider, providerId, source, contentType);
const timestamp = cacheTimestamps.get(key);
const cached = releaseCache.get(key);
@@ -49,8 +49,8 @@ function getCachedReleases(provider: string, providerId: string, source: string)
return null;
}
function setCachedReleases(provider: string, providerId: string, source: string, data: ReleasesResponse): void {
const key = getCacheKey(provider, providerId, source);
function setCachedReleases(provider: string, providerId: string, source: string, contentType: string, data: ReleasesResponse): void {
const key = getCacheKey(provider, providerId, source, contentType);
releaseCache.set(key, data);
cacheTimestamps.set(key, Date.now());
}
@@ -833,7 +833,7 @@ export const ReleaseModal = ({
if (releasesBySource[activeTab] !== undefined || loadingBySource[activeTab] || errorBySource[activeTab]) return;
// Check module-level cache first
const cached = getCachedReleases(provider, bookId, activeTab);
const cached = getCachedReleases(provider, bookId, activeTab, contentType);
if (cached) {
setReleasesBySource((prev) => ({ ...prev, [activeTab]: cached }));
return;
@@ -845,7 +845,7 @@ export const ReleaseModal = ({
try {
const response = await getReleases(provider, bookId, activeTab, book.title, book.author, undefined, undefined, contentType);
setCachedReleases(provider, bookId, activeTab, response);
setCachedReleases(provider, bookId, activeTab, contentType, response);
setReleasesBySource((prev) => ({ ...prev, [activeTab]: response }));
} catch (err) {
const message = err instanceof Error ? err.message : 'Failed to fetch releases';
@@ -1550,7 +1550,7 @@ export const ReleaseModal = ({
const bookId = book.provider_id;
// Clear cache and state
const key = getCacheKey(provider, bookId, activeTab);
const key = getCacheKey(provider, bookId, activeTab, contentType);
releaseCache.delete(key);
cacheTimestamps.delete(key);
setExpandedBySource((prev) => {
@@ -1577,7 +1577,7 @@ export const ReleaseModal = ({
const response = await getReleases(
provider, bookId, activeTab, book.title, book.author, false, languagesParam, contentType
);
setCachedReleases(provider, bookId, activeTab, response);
setCachedReleases(provider, bookId, activeTab, contentType, response);
setReleasesBySource((prev) => ({ ...prev, [activeTab]: response }));
} catch (err) {
const message = err instanceof Error ? err.message : 'Failed to fetch releases';