1.0.0.8 logging updates

This commit is contained in:
ponzischeme89
2026-01-18 23:01:03 +13:00
parent 0a1edc0922
commit 7fa9c4d16e
15 changed files with 220 additions and 123 deletions
+69 -65
View File
@@ -1,114 +1,116 @@
<script> <script>
import { onMount } from 'svelte' import { onMount } from "svelte";
import Footer from './components/Footer.svelte' import Footer from "./components/Footer.svelte";
import AppSidebar from './components/AppSidebar.svelte' import AppSidebar from "./components/AppSidebar.svelte";
import * as Sidebar from './lib/components/ui/sidebar' import * as Sidebar from "./lib/components/ui/sidebar";
import { Button } from './lib/components/ui/button' import { Button } from "./lib/components/ui/button";
import SettingsPanel from './components/SettingsPanel.svelte' import SettingsPanel from "./components/SettingsPanel.svelte";
import ScanPanel from './components/ScanPanel.svelte' import ScanPanel from "./components/ScanPanel.svelte";
import HistoryPanel from './components/HistoryPanel.svelte' import HistoryPanel from "./components/HistoryPanel.svelte";
import LibraryPanel from './components/LibraryPanel.svelte' import LibraryPanel from "./components/LibraryPanel.svelte";
import { Menu } from 'lucide-svelte' import { Menu } from "lucide-svelte";
import ToastHost from './components/ToastHost.svelte' import ToastHost from "./components/ToastHost.svelte";
import { healthCheck } from './lib/api.js' import { healthCheck } from "./lib/api.js";
import { currentTheme, themes } from './lib/themeStore.js' import { currentTheme, themes } from "./lib/themeStore.js";
let currentView = 'scanner' let currentView = "scanner";
let apiConfigured = false let apiConfigured = false;
let selectedFiles = [] let selectedFiles = [];
let metadataProvider = 'omdb' let metadataProvider = "omdb";
let scanPanelKey = 0 let scanPanelKey = 0;
let sidebarOpen = true let sidebarOpen = true;
let sidebarCollapsed = false let sidebarCollapsed = false;
let isMobile = false let isMobile = false;
// Apply theme on mount and when it changes // Apply theme on mount and when it changes
function applyTheme(themeName) { function applyTheme(themeName) {
const theme = themes[themeName] const theme = themes[themeName];
if (!theme) return if (!theme) return;
const root = document.documentElement const root = document.documentElement;
Object.entries(theme.colors).forEach(([key, value]) => { Object.entries(theme.colors).forEach(([key, value]) => {
root.style.setProperty(`--${key}`, value) root.style.setProperty(`--${key}`, value);
}) });
if (themeName === 'light') { if (themeName === "light") {
root.classList.add('light-theme') root.classList.add("light-theme");
} else { } else {
root.classList.remove('light-theme') root.classList.remove("light-theme");
} }
} }
function updateLayout() { function updateLayout() {
isMobile = window.innerWidth < 768 isMobile = window.innerWidth < 768;
if (isMobile) { if (isMobile) {
sidebarOpen = false sidebarOpen = false;
} else { } else {
sidebarOpen = true sidebarOpen = true;
} }
} }
onMount(async () => { onMount(async () => {
updateLayout() updateLayout();
const onResize = () => updateLayout() const onResize = () => updateLayout();
window.addEventListener('resize', onResize) window.addEventListener("resize", onResize);
// Initialize theme // Initialize theme
applyTheme($currentTheme) applyTheme($currentTheme);
try { try {
const health = await healthCheck() const health = await healthCheck();
apiConfigured = health.api_key_configured apiConfigured = health.api_key_configured;
} catch (err) { } catch (err) {
console.error('Health check failed:', err) console.error("Health check failed:", err);
} }
return () => { return () => {
window.removeEventListener('resize', onResize) window.removeEventListener("resize", onResize);
} };
}) });
// Watch for theme changes // Watch for theme changes
$: if ($currentTheme) { $: if ($currentTheme) {
applyTheme($currentTheme) applyTheme($currentTheme);
} }
async function checkApiStatus() { async function checkApiStatus() {
try { try {
const health = await healthCheck() const health = await healthCheck();
apiConfigured = health.api_key_configured apiConfigured = health.api_key_configured;
} catch (err) { } catch (err) {
console.error('Health check failed:', err) console.error("Health check failed:", err);
} }
} }
async function navigateTo(view) { async function navigateTo(view) {
currentView = view currentView = view;
// Re-check API status when navigating to scanner (in case settings were just saved) // Re-check API status when navigating to scanner (in case settings were just saved)
if (view === 'scanner') { if (view === "scanner") {
await checkApiStatus() await checkApiStatus();
} }
} }
function handleProcessComplete() { function handleProcessComplete() {
scanPanelKey += 1 scanPanelKey += 1;
} }
function handleSidebarToggle() { function handleSidebarToggle() {
if (isMobile) { if (isMobile) {
sidebarOpen = !sidebarOpen sidebarOpen = !sidebarOpen;
} else { } else {
sidebarCollapsed = !sidebarCollapsed sidebarCollapsed = !sidebarCollapsed;
} }
} }
$: sidebarWidth = isMobile $: sidebarWidth = isMobile
? '15.5rem' ? "15.5rem"
: sidebarCollapsed : sidebarCollapsed
? '3.75rem' ? "3.75rem"
: '16rem' : "16rem";
</script> </script>
<Sidebar.Provider style={`--sidebar-width: ${sidebarWidth}; --header-height: 4rem;`}> <Sidebar.Provider
style={`--sidebar-width: ${sidebarWidth}; --header-height: 4rem;`}
>
{#if isMobile && sidebarOpen} {#if isMobile && sidebarOpen}
<div <div
class="fixed inset-0 z-30 bg-black/40 backdrop-blur-sm" class="fixed inset-0 z-30 bg-black/40 backdrop-blur-sm"
@@ -141,30 +143,32 @@
</Button> </Button>
</div> </div>
{/if} {/if}
{#if !apiConfigured && currentView === 'scanner'} {#if !apiConfigured && currentView === "scanner"}
<div class="border-b border-yellow-500/10 bg-yellow-500/5"> <div class="border-b border-yellow-500/10 bg-yellow-500/5">
<div class="px-6 md:px-8 py-3"> <div class="px-6 md:px-8 py-3">
<p class="text-[13px] text-yellow-100">Configure a metadata source in Settings to get started</p> <p class="text-[13px] text-yellow-100">
Configure a metadata source in Settings to get started
</p>
</div> </div>
</div> </div>
{/if} {/if}
<div class="px-4 sm:px-6 md:px-8 py-6 sm:py-8 md:py-10"> <div class="px-4 sm:px-6 md:px-8 py-6 sm:py-8 md:py-10">
{#if currentView === 'settings'} {#if currentView === "settings"}
<SettingsPanel /> <SettingsPanel />
{:else if currentView === 'history'} {:else if currentView === "history"}
<HistoryPanel /> <HistoryPanel />
{:else if currentView === 'scanner'} {:else if currentView === "scanner"}
{#key scanPanelKey} {#key scanPanelKey}
<ScanPanel <ScanPanel
bind:selectedFilePaths={selectedFiles} bind:selectedFilePaths={selectedFiles}
bind:metadataProvider bind:metadataProvider
apiConfigured={apiConfigured} {apiConfigured}
onOpenSettings={() => navigateTo('settings')} onOpenSettings={() => navigateTo("settings")}
onOpenHistory={() => navigateTo('history')} onOpenHistory={() => navigateTo("history")}
/> />
{/key} {/key}
{:else if currentView === 'library'} {:else if currentView === "library"}
<LibraryPanel /> <LibraryPanel />
{/if} {/if}
</div> </div>
+1 -1
View File
@@ -154,7 +154,7 @@
> >
{#if !collapsed} {#if !collapsed}
<Badge className="bg-white/10 text-text-secondary" <Badge className="bg-white/10 text-text-secondary"
>v1.0.7 Release Candiate</Badge >v1.0.8 Release Candiate</Badge
> >
{:else} {:else}
<Badge className="bg-white/10 text-text-secondary">v</Badge> <Badge className="bg-white/10 text-text-secondary">v</Badge>
+26 -5
View File
@@ -8,13 +8,23 @@
let loading = false; let loading = false;
let error = null; let error = null;
let expanded = {}; let expanded = {};
let page = 0;
const pageSize = 200;
async function loadLibrary() { async function loadLibrary(reset = true) {
loading = true; loading = true;
error = null; error = null;
try { try {
const response = await getLibraryReport(); if (reset) {
items = response.items || []; page = 0;
items = [];
}
const response = await getLibraryReport(pageSize, page * pageSize);
const nextItems = response.items || [];
items = reset ? nextItems : [...items, ...nextItems];
if (nextItems.length > 0) {
page += 1;
}
} catch (err) { } catch (err) {
error = `Failed to load library report: ${err.message}`; error = `Failed to load library report: ${err.message}`;
} finally { } finally {
@@ -26,7 +36,7 @@
expanded = { ...expanded, [key]: !expanded[key] }; expanded = { ...expanded, [key]: !expanded[key] };
} }
onMount(loadLibrary); onMount(() => loadLibrary(true));
</script> </script>
<div class="space-y-6"> <div class="space-y-6">
@@ -41,7 +51,7 @@
variant="outline" variant="outline"
size="sm" size="sm"
className="border-white/15 text-text-secondary hover:bg-white/10" className="border-white/15 text-text-secondary hover:bg-white/10"
on:click={loadLibrary} on:click={() => loadLibrary(true)}
disabled={loading} disabled={loading}
> >
<RefreshCcw class="h-4 w-4" /> <RefreshCcw class="h-4 w-4" />
@@ -148,6 +158,17 @@
{/if} {/if}
</div> </div>
{/each} {/each}
<div class="flex justify-center">
<Button
variant="outline"
size="sm"
className="border-white/15 text-text-secondary hover:bg-white/10"
on:click={() => loadLibrary(false)}
disabled={loading}
>
Load more
</Button>
</div>
</div> </div>
{/if} {/if}
</div> </div>
+52
View File
@@ -7,6 +7,7 @@
processFiles, processFiles,
clearAllSuggestedMatches, clearAllSuggestedMatches,
getFolderRules, getFolderRules,
getScanHistory,
} from "../lib/api.js"; } from "../lib/api.js";
import ResultsList from "./ResultsList.svelte"; import ResultsList from "./ResultsList.svelte";
import TypewriterQuote from "./TypewriterQuote.svelte"; import TypewriterQuote from "./TypewriterQuote.svelte";
@@ -118,7 +119,11 @@
filesFound: 0, filesFound: 0,
message: "", message: "",
scanning: false, scanning: false,
startedAt: null,
estimatedFinishAt: null,
}; };
let scanHistory = [];
let expectedTotalFiles = null;
// Scan cancellation // Scan cancellation
let scanAbortController = null; let scanAbortController = null;
@@ -166,6 +171,12 @@
} catch (err) { } catch (err) {
console.error("Failed to load folder rules:", err); console.error("Failed to load folder rules:", err);
} }
try {
const historyResponse = await getScanHistory(10);
scanHistory = historyResponse.scans || [];
} catch (err) {
console.error("Failed to load scan history:", err);
}
} catch (err) { } catch (err) {
console.error("Failed to load initial data:", err); console.error("Failed to load initial data:", err);
} }
@@ -190,7 +201,19 @@
filesFound: 0, filesFound: 0,
message: "Starting scan...", message: "Starting scan...",
scanning: true, scanning: true,
startedAt: new Date(),
estimatedFinishAt: null,
}; };
expectedTotalFiles = null;
if (scanHistory.length > 0 && directory) {
const normalizedDir = directory.toLowerCase();
const lastMatch = scanHistory.find(
(scan) => (scan.directory || "").toLowerCase() === normalizedDir,
);
if (lastMatch && lastMatch.files_found) {
expectedTotalFiles = lastMatch.files_found;
}
}
addToast({ message: "Scan started.", tone: "info" }); addToast({ message: "Scan started.", tone: "info" });
// Reset files array before starting new scan // Reset files array before starting new scan
@@ -219,6 +242,23 @@
message: data.message, message: data.message,
filesFound: data.filesFound, filesFound: data.filesFound,
}; };
if (
scanProgress.startedAt &&
expectedTotalFiles &&
data.filesFound > 0
) {
const elapsedMs =
new Date().getTime() - scanProgress.startedAt.getTime();
const estimatedTotalMs =
(elapsedMs * expectedTotalFiles) / data.filesFound;
const estimatedFinish = new Date(
scanProgress.startedAt.getTime() + estimatedTotalMs,
);
scanProgress = {
...scanProgress,
estimatedFinishAt: estimatedFinish,
};
}
// Incrementally add files as they're found // Incrementally add files as they're found
const previousLength = files.length; const previousLength = files.length;
@@ -244,6 +284,9 @@
} }
lastScan = new Date().toISOString(); lastScan = new Date().toISOString();
if (data.count != null) {
expectedTotalFiles = data.count;
}
// Save to store // Save to store
scanResults.setScanResults(files, directory); scanResults.setScanResults(files, directory);
@@ -343,6 +386,7 @@
} finally { } finally {
scanning = false; scanning = false;
scanProgress.scanning = false; scanProgress.scanning = false;
scanProgress.estimatedFinishAt = null;
scanAbortController = null; scanAbortController = null;
} }
} }
@@ -715,6 +759,14 @@
> >
<span class="text-text-tertiary">Scanning in progress...</span> <span class="text-text-tertiary">Scanning in progress...</span>
</div> </div>
{#if scanProgress.startedAt}
<div class="mt-2 text-[11px] text-text-tertiary">
Started at {scanProgress.startedAt.toLocaleTimeString()}
{#if scanProgress.estimatedFinishAt}
· Estimated finish {scanProgress.estimatedFinishAt.toLocaleTimeString()}
{/if}
</div>
{/if}
<!-- Progress bar --> <!-- Progress bar -->
<div <div
+2 -2
View File
@@ -338,8 +338,8 @@ export async function getStatistics() {
* GET /api/library - Get library health report * GET /api/library - Get library health report
* Returns: { success, scans: [...] } * Returns: { success, scans: [...] }
*/ */
export async function getLibraryReport(limit = 25) { export async function getLibraryReport(limit = 200, offset = 0) {
return apiFetch(`/library?limit=${limit}`) return apiFetch(`/library?limit=${limit}&offset=${offset}`)
} }
// ============ SCHEDULED SCANS API ============ // ============ SCHEDULED SCANS API ============
+14 -8
View File
@@ -1,6 +1,5 @@
import asyncio import asyncio
import json import json
import logging
import os import os
import threading import threading
import time import time
@@ -20,13 +19,11 @@ from core.subtitle_processor import SubtitleProcessor, SubtitleFormatOptions, SU
from core.keyword_stripper import get_stripper from core.keyword_stripper import get_stripper
from core.file_scanner import FileScanner from core.file_scanner import FileScanner
from core.database import DatabaseManager from core.database import DatabaseManager
from logging_utils import configure_logging, get_logger
# Configure logging # Configure logging
logging.basicConfig( configure_logging()
level=logging.INFO, logger = get_logger(__name__)
format='%(asctime)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)
# Initialize Flask app with static folder for production # Initialize Flask app with static folder for production
static_folder = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'static') static_folder = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'static')
@@ -592,6 +589,8 @@ def stream_scan():
"error": "No directory specified" "error": "No directory specified"
}), 400 }), 400
client_closed = threading.Event()
def generate(): def generate():
"""Generator function that yields SSE-formatted progress updates""" """Generator function that yields SSE-formatted progress updates"""
try: try:
@@ -616,6 +615,10 @@ def stream_scan():
# Stream batches as they're found # Stream batches as they're found
for batch in FileScanner.scan_directory(directory, batch_size=10): for batch in FileScanner.scan_directory(directory, batch_size=10):
if client_closed.is_set():
logger.info("Client disconnected, stopping scan loop")
scan_state["scanning"] = False
return
# Check if client is still connected before processing # Check if client is still connected before processing
try: try:
batch_count += 1 batch_count += 1
@@ -700,7 +703,7 @@ def stream_scan():
} }
yield f"data: {json.dumps(error_data)}\n\n" yield f"data: {json.dumps(error_data)}\n\n"
return Response( response = Response(
stream_with_context(generate()), stream_with_context(generate()),
mimetype='text/event-stream', mimetype='text/event-stream',
headers={ headers={
@@ -709,6 +712,8 @@ def stream_scan():
'Connection': 'keep-alive' 'Connection': 'keep-alive'
} }
) )
response.call_on_close(client_closed.set)
return response
except Exception as e: except Exception as e:
logger.error(f"Stream scan setup error: {e}") logger.error(f"Stream scan setup error: {e}")
@@ -1640,7 +1645,8 @@ def get_library_report():
"""Get library health report with scan files and issue summaries""" """Get library health report with scan files and issue summaries"""
try: try:
limit = request.args.get('limit', 200, type=int) limit = request.args.get('limit', 200, type=int)
latest_files = db.get_latest_scan_files() offset = request.args.get('offset', 0, type=int)
latest_files = db.get_latest_scan_files(limit=limit, offset=offset)
latest_results = db.get_latest_file_results() latest_results = db.get_latest_file_results()
return jsonify({ return jsonify({
+8 -7
View File
@@ -3,9 +3,10 @@ Configuration manager - handles settings persistence
""" """
import json import json
import logging import logging
from logging_utils import get_logger
from pathlib import Path from pathlib import Path
logging.basicConfig(level=logging.INFO) logger = get_logger(__name__)
class ConfigManager: class ConfigManager:
@@ -35,18 +36,18 @@ class ConfigManager:
try: try:
with open(self.file_path, "r") as f: with open(self.file_path, "r") as f:
self.settings.update(json.load(f)) self.settings.update(json.load(f))
logging.info("Settings loaded successfully") logger.info("Settings loaded successfully")
except Exception as e: except Exception as e:
logging.error(f"Error loading settings: {e}") logger.error(f"Error loading settings: {e}")
def save_settings(self): def save_settings(self):
"""Save settings to disk""" """Save settings to disk"""
try: try:
with open(self.file_path, "w") as f: with open(self.file_path, "w") as f:
json.dump(self.settings, f, indent=2) json.dump(self.settings, f, indent=2)
logging.info("Settings saved successfully") logger.info("Settings saved successfully")
except Exception as e: except Exception as e:
logging.error(f"Error saving settings: {e}") logger.error(f"Error saving settings: {e}")
def get(self, key, default=None): def get(self, key, default=None):
"""Get a setting value""" """Get a setting value"""
@@ -55,7 +56,7 @@ class ConfigManager:
def set(self, key, value): def set(self, key, value):
"""Set a setting value""" """Set a setting value"""
self.settings[key] = value self.settings[key] = value
logging.info(f"Setting updated: {key}") logger.info(f"Setting updated: {key}")
def get_all(self): def get_all(self):
"""Get all settings""" """Get all settings"""
@@ -64,4 +65,4 @@ class ConfigManager:
def update_multiple(self, updates): def update_multiple(self, updates):
"""Update multiple settings at once""" """Update multiple settings at once"""
self.settings.update(updates) self.settings.update(updates)
logging.info(f"Updated {len(updates)} settings") logger.info(f"Updated {len(updates)} settings")
+7 -4
View File
@@ -9,8 +9,9 @@ from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker, relationship, scoped_session from sqlalchemy.orm import sessionmaker, relationship, scoped_session
import json import json
import logging import logging
from logging_utils import get_logger
logger = logging.getLogger(__name__) logger = get_logger(__name__)
Base = declarative_base() Base = declarative_base()
@@ -504,11 +505,13 @@ class DatabaseManager:
finally: finally:
session.close() session.close()
def get_latest_scan_files(self): def get_latest_scan_files(self, limit=500, offset=0):
"""Get latest scan entry per file path""" """Get latest scan entry per file path, paged by scan_files.created_at"""
session = self.get_session() session = self.get_session()
try: try:
files = session.query(ScanFile).order_by(ScanFile.created_at.desc()).all() files = session.query(ScanFile).order_by(
ScanFile.created_at.desc()
).offset(offset).limit(limit).all()
latest = {} latest = {}
for file_entry in files: for file_entry in files:
if file_entry.file_path in latest: if file_entry.file_path in latest:
+2 -9
View File
@@ -1,4 +1,5 @@
import logging import logging
from logging_utils import get_logger
import os import os
import re import re
from pathlib import Path from pathlib import Path
@@ -8,15 +9,7 @@ from typing import Generator, List, Dict
# Logging configuration # Logging configuration
# ------------------------------------------------------------ # ------------------------------------------------------------
logger = logging.getLogger("FileScanner") logger = get_logger("FileScanner")
logger.setLevel(logging.INFO) # Change to DEBUG for deep tracing
handler = logging.StreamHandler()
formatter = logging.Formatter(
"%(asctime)s | %(levelname)-8s | %(name)s | %(message)s"
)
handler.setFormatter(formatter)
logger.addHandler(handler)
# ------------------------------------------------------------ # ------------------------------------------------------------
# Import subtitle parser # Import subtitle parser
+2 -1
View File
@@ -7,9 +7,10 @@ from __future__ import annotations
import re import re
import logging import logging
from logging_utils import get_logger
from typing import Optional, List from typing import Optional, List
logger = logging.getLogger(__name__) logger = get_logger(__name__)
class KeywordStripper: class KeywordStripper:
+2 -2
View File
@@ -12,11 +12,11 @@ from __future__ import annotations
import asyncio import asyncio
import aiohttp import aiohttp
import logging import logging
from logging_utils import get_logger
import time import time
from typing import Dict, Optional from typing import Dict, Optional
logging.basicConfig(level=logging.INFO) logger = get_logger(__name__)
logger = logging.getLogger(__name__)
class RateLimiter: class RateLimiter:
+2 -2
View File
@@ -2,6 +2,7 @@ from __future__ import annotations
import re import re
import logging import logging
from logging_utils import get_logger
import textwrap import textwrap
import time import time
import os import os
@@ -25,8 +26,7 @@ except ImportError:
except ImportError: except ImportError:
_HAS_MSVCRT = False _HAS_MSVCRT = False
logger = logging.getLogger("SubtitleProcessor") logger = get_logger("SubtitleProcessor")
logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s")
# ============================================================ # ============================================================
# Sentinel tag for deterministic detection # Sentinel tag for deterministic detection
+16 -16
View File
@@ -3,10 +3,10 @@ TMDb API client - async movie and TV series metadata fetching
""" """
import asyncio import asyncio
import aiohttp import aiohttp
import logging from logging_utils import get_logger
import time import time
logging.basicConfig(level=logging.INFO) logger = get_logger(__name__)
class TMDbClient: class TMDbClient:
@@ -47,7 +47,7 @@ class TMDbClient:
response_time_ms = int((time.time() - start_time) * 1000) response_time_ms = int((time.time() - start_time) * 1000)
if response.status != 200: if response.status != 200:
logging.error(f"TMDb HTTP error {response.status} for movie '{title}'") logger.error(f"TMDb HTTP error {response.status} for movie '{title}'")
# Track failed API call # Track failed API call
if self.db_manager: if self.db_manager:
self.db_manager.track_api_call( self.db_manager.track_api_call(
@@ -71,7 +71,7 @@ class TMDbClient:
) )
return data["results"][0] # Return first match return data["results"][0] # Return first match
logging.warning(f"No TMDb results for movie '{title}'") logger.warning(f"No TMDb results for movie '{title}'")
# Track failed API call (no results) # Track failed API call (no results)
if self.db_manager: if self.db_manager:
self.db_manager.track_api_call( self.db_manager.track_api_call(
@@ -83,7 +83,7 @@ class TMDbClient:
return None return None
except Exception as e: except Exception as e:
logging.error(f"Error searching TMDb for movie '{title}': {e}") logger.error(f"Error searching TMDb for movie '{title}': {e}")
return None return None
async def search_tv(self, title, year=None, language=None): async def search_tv(self, title, year=None, language=None):
@@ -115,7 +115,7 @@ class TMDbClient:
response_time_ms = int((time.time() - start_time) * 1000) response_time_ms = int((time.time() - start_time) * 1000)
if response.status != 200: if response.status != 200:
logging.error(f"TMDb HTTP error {response.status} for TV '{title}'") logger.error(f"TMDb HTTP error {response.status} for TV '{title}'")
# Track failed API call # Track failed API call
if self.db_manager: if self.db_manager:
self.db_manager.track_api_call( self.db_manager.track_api_call(
@@ -139,7 +139,7 @@ class TMDbClient:
) )
return data["results"][0] # Return first match return data["results"][0] # Return first match
logging.warning(f"No TMDb results for TV series '{title}'") logger.warning(f"No TMDb results for TV series '{title}'")
# Track failed API call (no results) # Track failed API call (no results)
if self.db_manager: if self.db_manager:
self.db_manager.track_api_call( self.db_manager.track_api_call(
@@ -151,7 +151,7 @@ class TMDbClient:
return None return None
except Exception as e: except Exception as e:
logging.error(f"Error searching TMDb for TV '{title}': {e}") logger.error(f"Error searching TMDb for TV '{title}': {e}")
return None return None
async def get_movie_details(self, movie_id, language=None): async def get_movie_details(self, movie_id, language=None):
@@ -174,13 +174,13 @@ class TMDbClient:
async with aiohttp.ClientSession() as session: async with aiohttp.ClientSession() as session:
async with session.get(url, params=params) as response: async with session.get(url, params=params) as response:
if response.status != 200: if response.status != 200:
logging.error(f"TMDb HTTP error {response.status} for movie ID {movie_id}") logger.error(f"TMDb HTTP error {response.status} for movie ID {movie_id}")
return None return None
return await response.json() return await response.json()
except Exception as e: except Exception as e:
logging.error(f"Error getting TMDb movie details for ID {movie_id}: {e}") logger.error(f"Error getting TMDb movie details for ID {movie_id}: {e}")
return None return None
async def get_tv_details(self, tv_id, language=None): async def get_tv_details(self, tv_id, language=None):
@@ -203,13 +203,13 @@ class TMDbClient:
async with aiohttp.ClientSession() as session: async with aiohttp.ClientSession() as session:
async with session.get(url, params=params) as response: async with session.get(url, params=params) as response:
if response.status != 200: if response.status != 200:
logging.error(f"TMDb HTTP error {response.status} for TV ID {tv_id}") logger.error(f"TMDb HTTP error {response.status} for TV ID {tv_id}")
return None return None
return await response.json() return await response.json()
except Exception as e: except Exception as e:
logging.error(f"Error getting TMDb TV details for ID {tv_id}: {e}") logger.error(f"Error getting TMDb TV details for ID {tv_id}: {e}")
return None return None
async def get_tv_season(self, tv_id, season_number, language=None): async def get_tv_season(self, tv_id, season_number, language=None):
@@ -233,13 +233,13 @@ class TMDbClient:
async with aiohttp.ClientSession() as session: async with aiohttp.ClientSession() as session:
async with session.get(url, params=params) as response: async with session.get(url, params=params) as response:
if response.status != 200: if response.status != 200:
logging.error(f"TMDb HTTP error {response.status} for TV {tv_id} season {season_number}") logger.error(f"TMDb HTTP error {response.status} for TV {tv_id} season {season_number}")
return None return None
return await response.json() return await response.json()
except Exception as e: except Exception as e:
logging.error(f"Error getting TMDb season data: {e}") logger.error(f"Error getting TMDb season data: {e}")
return None return None
async def fetch_summary(self, title, media_type="movie", year=None, season=None, episode=None, language=None): async def fetch_summary(self, title, media_type="movie", year=None, season=None, episode=None, language=None):
@@ -256,7 +256,7 @@ class TMDbClient:
Returns: Returns:
dict: {plot, title, year, media_type, rating} or None if not found dict: {plot, title, year, media_type, rating} or None if not found
""" """
logging.info(f"Fetching TMDb summary for: {title} (type: {media_type})") logger.info(f"Fetching TMDb summary for: {title} (type: {media_type})")
try: try:
if media_type == "tv": if media_type == "tv":
@@ -327,5 +327,5 @@ class TMDbClient:
} }
except Exception as e: except Exception as e:
logging.error(f"Error fetching TMDb summary for '{title}': {e}") logger.error(f"Error fetching TMDb summary for '{title}': {e}")
return None return None
+2 -1
View File
@@ -3,10 +3,11 @@ TVmaze API client - async TV metadata fetching
""" """
import aiohttp import aiohttp
import logging import logging
from logging_utils import get_logger
import re import re
import time import time
logger = logging.getLogger(__name__) logger = get_logger(__name__)
class TVMazeClient: class TVMazeClient:
+15
View File
@@ -0,0 +1,15 @@
import logging
from typing import Optional
DEFAULT_LOG_FORMAT = "%(asctime)s - %(levelname)s - %(message)s"
def configure_logging(level: int = logging.INFO, fmt: str = DEFAULT_LOG_FORMAT) -> None:
"""Configure application logging."""
logging.basicConfig(level=level, format=fmt)
def get_logger(name: Optional[str] = None) -> logging.Logger:
"""Get a logger by name."""
return logging.getLogger(name or __name__)