Files
profilarr/backend/app/importer/__init__.py
Samuel Chau d7d6b13e46 feat(profiles): radarr/sonarr split functionality (#215)
- added option to set radarr/sonarr specific scores that profilarr's compiler will handle on import
- revise design for arr settings container - now styled as a table
- completely rewrote import module. Now uses connection pooling to reuse connections.
- fixed import progress bug where 1 failed format causes all other formats to be labelled as failed (even if they succeeded)
- fixed bug where on pull sync wasn't working
- improve styling for link / unlink database modals
- fixed issue where 0 score formats were removed in selective mode
2025-08-11 01:51:51 +09:30

326 lines
10 KiB
Python

"""Main import module entry point."""
import sys
import logging
from typing import Dict, Any, List
from .strategies import FormatStrategy, ProfileStrategy
from .logger import reset_import_logger
logger = logging.getLogger(__name__)
def handle_import_request(request: Dict[str, Any]) -> Dict[str, Any]:
"""
Handle an import request.
Args:
request: Request dictionary containing:
- arrID: ID of the arr_config to use
- strategy: 'format' or 'profile'
- filenames: List of filenames to import
- dryRun: Optional boolean for dry-run mode (default: false)
Returns:
Import results with added/updated/failed counts
"""
from ..db import get_db
try:
# Extract request parameters
arr_id = request.get('arrID')
strategy_type = request.get('strategy')
filenames = request.get('filenames', [])
dry_run = request.get('dryRun', False)
# Validate inputs
if not arr_id:
return {'success': False, 'error': 'arrID is required'}
if strategy_type not in ['format', 'profile']:
return {
'success': False,
'error': 'strategy must be "format" or "profile"'
}
if not filenames:
return {'success': False, 'error': 'filenames list is required'}
# Load arr_config from database
with get_db() as conn:
cursor = conn.execute("SELECT * FROM arr_config WHERE id = ?",
(arr_id, ))
arr_config = cursor.fetchone()
if not arr_config:
return {
'success': False,
'error': f'arr_config {arr_id} not found'
}
# Select strategy
strategy_map = {'format': FormatStrategy, 'profile': ProfileStrategy}
strategy_class = strategy_map[strategy_type]
strategy = strategy_class(arr_config)
# Execute import with new logger
import_logger = reset_import_logger()
# Show start message
dry_run_text = " [DRY RUN]" if dry_run else ""
print(f"Starting {strategy_type} import for {arr_config['name']} ({arr_config['type']}): {len(filenames)} items{dry_run_text}", file=sys.stderr)
result = strategy.execute(filenames, dry_run=dry_run)
added = result.get('added', 0)
updated = result.get('updated', 0)
failed = result.get('failed', 0)
# Determine status
is_partial = failed > 0 and (added > 0 or updated > 0)
is_success = failed == 0
result['success'] = is_success or is_partial
if is_partial:
result['status'] = "partial"
elif is_success:
result['status'] = "success"
else:
result['status'] = "failed"
result['arr_config_id'] = arr_id
result['arr_config_name'] = arr_config['name']
result['strategy'] = strategy_type
# Complete logging
import_logger.complete()
return result
except Exception as e:
logger.exception("Import request failed")
return {'success': False, 'error': str(e)}
def handle_scheduled_import(task_id: int) -> Dict[str, Any]:
"""
Handle a scheduled import task.
Args:
task_id: ID from scheduled_tasks table
Returns:
Import results
"""
from ..db import get_db
import json
try:
# Find arr_config for this task
with get_db() as conn:
cursor = conn.execute(
"SELECT * FROM arr_config WHERE import_task_id = ?",
(task_id, ))
arr_config = cursor.fetchone()
if not arr_config:
return {
'success': False,
'error': f'No arr_config found for task {task_id}'
}
# Parse data_to_sync
data_to_sync = json.loads(arr_config['data_to_sync'] or '{}')
# Build import requests
results = []
# Import custom formats
format_names = data_to_sync.get('customFormats', [])
if format_names:
# Remove .yml extension if present
format_names = [f.replace('.yml', '') for f in format_names]
request = {
'arrID': arr_config['id'],
'strategy': 'format',
'filenames': format_names
}
result = handle_import_request(request)
results.append(result)
# Import profiles
profile_names = data_to_sync.get('profiles', [])
if profile_names:
# Remove .yml extension if present
profile_names = [p.replace('.yml', '') for p in profile_names]
request = {
'arrID': arr_config['id'],
'strategy': 'profile',
'filenames': profile_names
}
result = handle_import_request(request)
results.append(result)
# Combine results
total_added = sum(r.get('added', 0) for r in results)
total_updated = sum(r.get('updated', 0) for r in results)
total_failed = sum(r.get('failed', 0) for r in results)
is_partial = total_failed > 0 and (total_added > 0
or total_updated > 0)
is_success = total_failed == 0
status = "failed"
if is_partial:
status = "partial"
elif is_success:
status = "success"
combined_result = {
'success': is_success or is_partial,
'status': status,
'task_id': task_id,
'arr_config_id': arr_config['id'],
'arr_config_name': arr_config['name'],
'added': total_added,
'updated': total_updated,
'failed': total_failed,
'results': results
}
# Update sync status
_update_sync_status(arr_config['id'], combined_result)
return combined_result
except Exception as e:
logger.exception(f"Scheduled import {task_id} failed")
return {'success': False, 'error': str(e)}
def handle_pull_import(arr_config_id: int) -> Dict[str, Any]:
"""
Handle an on-pull import for a specific ARR config.
This mirrors scheduled import behavior but is triggered immediately
during a git pull (not scheduled).
"""
from ..db import get_db
import json
try:
# Load arr_config by id
with get_db() as conn:
cursor = conn.execute("SELECT * FROM arr_config WHERE id = ?",
(arr_config_id, ))
arr_config = cursor.fetchone()
if not arr_config:
return {
'success': False,
'error': f'arr_config {arr_config_id} not found'
}
# Parse data_to_sync
data_to_sync = json.loads(arr_config['data_to_sync'] or '{}')
results: List[Dict[str, Any]] = []
# Import custom formats
format_names = data_to_sync.get('customFormats', [])
if format_names:
format_names = [f.replace('.yml', '') for f in format_names]
request = {
'arrID': arr_config['id'],
'strategy': 'format',
'filenames': format_names,
}
result = handle_import_request(request)
results.append(result)
# Import profiles
profile_names = data_to_sync.get('profiles', [])
if profile_names:
profile_names = [p.replace('.yml', '') for p in profile_names]
request = {
'arrID': arr_config['id'],
'strategy': 'profile',
'filenames': profile_names,
}
result = handle_import_request(request)
results.append(result)
# Combine results
total_added = sum(r.get('added', 0) for r in results)
total_updated = sum(r.get('updated', 0) for r in results)
total_failed = sum(r.get('failed', 0) for r in results)
is_partial = total_failed > 0 and (total_added > 0
or total_updated > 0)
is_success = total_failed == 0
status = "failed"
if is_partial:
status = "partial"
elif is_success:
status = "success"
combined_result = {
'success': is_success or is_partial,
'status': status,
'arr_config_id': arr_config['id'],
'arr_config_name': arr_config['name'],
'added': total_added,
'updated': total_updated,
'failed': total_failed,
'results': results,
}
# Update sync status
_update_sync_status(arr_config['id'], combined_result)
return combined_result
except Exception as e:
logger.exception(f"Pull import for arr_config {arr_config_id} failed")
return {
'success': False,
'error': str(e),
}
def _update_sync_status(config_id: int, result: Dict[str, Any]) -> None:
"""Update arr_config sync status after scheduled import."""
from ..db import get_db
from datetime import datetime
try:
total = result.get('added', 0) + result.get('updated', 0) + result.get(
'failed', 0)
successful = result.get('added', 0) + result.get('updated', 0)
sync_percentage = int((successful / total * 100) if total > 0 else 0)
with get_db() as conn:
conn.execute(
"""
UPDATE arr_config
SET last_sync_time = ?,
sync_percentage = ?
WHERE id = ?
""", (datetime.now(), sync_percentage, config_id))
conn.commit()
logger.info(
f"Updated sync status for arr_config #{config_id}: {sync_percentage}%"
)
except Exception as e:
logger.error(f"Failed to update sync status: {e}")
# Export main functions
__all__ = [
'handle_import_request', 'handle_scheduled_import', 'handle_pull_import'
]