Files
dashcaddy/dashcaddy-api/routes/arr/detect.js

284 lines
11 KiB
JavaScript

const express = require('express');
const { APP_PORTS, ARR_SERVICES } = require('../../constants');
module.exports = function(ctx, helpers) {
const router = express.Router();
// Detect running arr services and their configurations
router.get('/arr/detect', ctx.asyncHandler(async (req, res) => {
const containers = await ctx.docker.client.listContainers({ all: false });
const detected = {
plex: null,
radarr: null,
sonarr: null,
overseerr: null,
lidarr: null,
prowlarr: null
};
// Service detection patterns
const servicePatterns = ARR_SERVICES;
for (const container of containers) {
const containerName = container.Names[0]?.replace(/^\//, '').toLowerCase() || '';
const image = container.Image.toLowerCase();
for (const [service, config] of Object.entries(servicePatterns)) {
if (config.names.some(n => containerName.includes(n) || image.includes(n))) {
// Find the exposed port
const portInfo = container.Ports.find(p => p.PrivatePort === config.port);
const exposedPort = portInfo?.PublicPort || config.port;
detected[service] = {
containerId: container.Id,
containerName: container.Names[0]?.replace(/^\//, ''),
image: container.Image,
port: exposedPort,
status: container.State,
url: helpers.getServiceUrl(containerName, exposedPort)
};
// Get API key for arr services (not Plex or Overseerr)
if (['radarr', 'sonarr', 'lidarr', 'prowlarr'].includes(service)) {
detected[service].apiKey = await helpers.getArrApiKey(containerName);
}
}
}
}
// Get Plex token if Plex is detected
if (detected.plex) {
detected.plex.token = await helpers.getPlexToken(detected.plex.containerName);
}
res.json({
success: true,
services: detected,
summary: {
plexReady: !!(detected.plex?.token),
radarrReady: !!(detected.radarr?.apiKey),
sonarrReady: !!(detected.sonarr?.apiKey),
overseerrRunning: !!detected.overseerr
}
});
}, 'arr-detect'));
// Smart Detect: Unified discovery of all arr services
router.get('/arr/smart-detect', ctx.asyncHandler(async (req, res) => {
const serviceList = ['plex', 'radarr', 'sonarr', 'prowlarr', 'seerr'];
const defaultPorts = APP_PORTS;
const result = {};
// 1. Scan Docker containers
let containers = [];
try { containers = await ctx.docker.client.listContainers({ all: false }); } catch (e) { /* Docker not available */ }
const servicePatterns = ARR_SERVICES;
const dockerDetected = {};
for (const container of containers) {
const containerName = container.Names[0]?.replace(/^\//, '').toLowerCase() || '';
const image = container.Image.toLowerCase();
for (const [svc, config] of Object.entries(servicePatterns)) {
if (config.names.some(n => containerName.includes(n) || image.includes(n))) {
const portInfo = container.Ports.find(p => p.PrivatePort === config.port);
dockerDetected[svc] = {
containerId: container.Id,
containerName: container.Names[0]?.replace(/^\//, ''),
port: portInfo?.PublicPort || config.port,
status: container.State
};
}
}
}
// 2. Load services.json for external entries
let storedServices = [];
try {
const data = await ctx.servicesStateManager.read();
storedServices = Array.isArray(data) ? data : data.services || [];
} catch (e) { /* ignore */ }
// 3. Load stored credentials
const storedCreds = {};
const seedboxBaseUrl = await ctx.credentialManager.retrieve('arr.seedbox.baseurl');
for (const svc of serviceList) {
const credKey = svc === 'plex' ? 'arr.plex.token' : `arr.${svc}.apikey`;
const apiKey = await ctx.credentialManager.retrieve(credKey);
const metadata = await ctx.credentialManager.getMetadata(credKey);
if (apiKey) {
storedCreds[svc] = { apiKey, metadata };
}
}
// 4. Build detection result for each service
for (const svc of serviceList) {
const entry = {
status: 'not_found',
source: null,
url: null,
hasApiKey: false,
hasToken: false,
containerId: null,
containerName: null,
version: null
};
// Check Docker first
if (dockerDetected[svc]) {
const dc = dockerDetected[svc];
entry.containerId = dc.containerId;
entry.containerName = dc.containerName;
entry.source = 'local';
entry.url = `http://localhost:${dc.port}`;
if (svc === 'plex') {
// Try to get Plex token from container
try {
const token = await helpers.getPlexToken(dc.containerName);
if (token) {
entry.hasToken = true;
entry.status = 'connected';
// Store for later use
await ctx.credentialManager.store('arr.plex.token', token, {
service: 'plex', source: 'local', url: entry.url,
lastVerified: new Date().toISOString()
});
} else {
entry.status = 'needs_key';
}
} catch (e) { entry.status = 'needs_key'; }
} else if (svc === 'seerr') {
entry.status = 'connected';
// Check what Overseerr has configured using Plex-based session auth
try {
const session = await helpers.getOverseerrSession();
if (session) {
entry.hasApiKey = true;
const configuredServices = { radarr: false, sonarr: false, plex: false };
try {
const radarrCheck = await ctx.fetchT(`http://host.docker.internal:${dc.port}/api/v1/settings/radarr`, {
headers: { 'Cookie': session.cookie },
signal: AbortSignal.timeout(5000)
});
if (radarrCheck.ok) {
const radarrSettings = await radarrCheck.json();
configuredServices.radarr = Array.isArray(radarrSettings) ? radarrSettings.length > 0 : !!radarrSettings;
}
} catch (e) { /* ignore */ }
try {
const sonarrCheck = await ctx.fetchT(`http://host.docker.internal:${dc.port}/api/v1/settings/sonarr`, {
headers: { 'Cookie': session.cookie },
signal: AbortSignal.timeout(5000)
});
if (sonarrCheck.ok) {
const sonarrSettings = await sonarrCheck.json();
configuredServices.sonarr = Array.isArray(sonarrSettings) ? sonarrSettings.length > 0 : !!sonarrSettings;
}
} catch (e) { /* ignore */ }
try {
const plexCheck = await ctx.fetchT(`http://host.docker.internal:${dc.port}/api/v1/settings/plex`, {
headers: { 'Cookie': session.cookie },
signal: AbortSignal.timeout(5000)
});
if (plexCheck.ok) {
const plexSettings = await plexCheck.json();
configuredServices.plex = !!plexSettings?.ip;
}
} catch (e) { /* ignore */ }
entry.configuredServices = configuredServices;
}
} catch (e) { /* ignore */ }
} else {
// arr services - try to get API key from container
try {
const key = await helpers.getArrApiKey(dc.containerName);
if (key) {
entry.hasApiKey = true;
entry.status = 'connected';
} else {
entry.status = storedCreds[svc] ? 'connected' : 'needs_key';
entry.hasApiKey = !!storedCreds[svc];
}
} catch (e) {
entry.status = storedCreds[svc] ? 'connected' : 'needs_key';
entry.hasApiKey = !!storedCreds[svc];
}
}
}
// Check external services from services.json
if (entry.status === 'not_found') {
const externalService = storedServices.find(s => s.id === svc && s.isExternal);
if (externalService?.externalUrl) {
entry.source = 'external';
entry.url = externalService.externalUrl;
if (storedCreds[svc]) {
entry.hasApiKey = true;
entry.version = storedCreds[svc].metadata?.version || null;
// Verify connection is still good
const test = await helpers.testServiceConnection(svc, entry.url, storedCreds[svc].apiKey);
entry.status = test.success ? 'connected' : 'error';
if (test.success) entry.version = test.version;
} else {
entry.status = 'needs_key';
}
}
}
// Check stored credentials with metadata URL
if (entry.status === 'not_found' && storedCreds[svc]?.metadata?.url) {
entry.source = 'stored';
entry.url = storedCreds[svc].metadata.url;
entry.hasApiKey = true;
entry.version = storedCreds[svc].metadata?.version || null;
entry.status = 'connected';
}
// For plex, also check stored token
if (svc === 'plex' && entry.status === 'not_found' && storedCreds.plex) {
entry.hasToken = true;
entry.source = 'stored';
entry.url = storedCreds.plex.metadata?.url || `http://localhost:${defaultPorts.plex}`;
entry.status = 'connected';
}
result[svc] = entry;
}
// 5. Detect seedbox base URL pattern
let detectedSeedboxUrl = seedboxBaseUrl || null;
if (!detectedSeedboxUrl) {
const externalUrls = storedServices
.filter(s => s.isExternal && s.externalUrl)
.map(s => s.externalUrl);
if (externalUrls.length > 0) {
// Find common base URL pattern
try {
const url = new URL(externalUrls[0]);
const pathParts = url.pathname.split('/').filter(p => p);
if (pathParts.length >= 2) {
detectedSeedboxUrl = `${url.origin}/${pathParts[0]}`;
}
} catch (e) { /* ignore */ }
}
}
// Summary
const statuses = Object.values(result);
const summary = {
totalDetected: statuses.filter(s => s.status !== 'not_found').length,
fullyConnected: statuses.filter(s => s.status === 'connected').length,
needsApiKey: statuses.filter(s => s.status === 'needs_key').length,
errors: statuses.filter(s => s.status === 'error').length,
readyForAutoConnect: statuses.filter(s => s.status === 'connected').length >= 2
};
res.json({ success: true, services: result, seedboxBaseUrl: detectedSeedboxUrl, summary });
}, 'smart-detect'));
return router;
};