1.0.0.8 logging updates
This commit is contained in:
+69
-65
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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({
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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,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
@@ -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
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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__)
|
||||||
Reference in New Issue
Block a user