feat(ombi): Add Ombi PALDRA integration for request management
Docs Check / Markdown lint (push) Successful in 1m43s
Licence Check / Licence compatibility and copyright header verification (push) Successful in 2m1s
CI / Security audit (push) Successful in 2m48s
Docs Check / Mermaid diagram parse check (push) Successful in 3m8s
CI / Tests & coverage (push) Failing after 3m33s
CI / Swagger Validation & Coverage (push) Successful in 3m34s
Build and Push Docker Image / build (push) Successful in 4m36s
Docs Check / Markdown lint (push) Successful in 1m43s
Licence Check / Licence compatibility and copyright header verification (push) Successful in 2m1s
CI / Security audit (push) Successful in 2m48s
Docs Check / Mermaid diagram parse check (push) Successful in 3m8s
CI / Tests & coverage (push) Failing after 3m33s
CI / Swagger Validation & Coverage (push) Successful in 3m34s
Build and Push Docker Image / build (push) Successful in 4m36s
- Add OmbiRetriever extending ArrRetriever for PALDRA compliance - Add OmbiClient for low-level Ombi API communication - Add getOmbiInstances() to config.js following multi-instance pattern - Register Ombi in PALDRA registry with Ombi-specific methods - Add external ID matching (TMDB/TVDB/IMDB) to Ombi requests - Update DownloadMatcher to be async and enrich downloads with Ombi links - Add getOmbiLink/getOmbiSearchLink helpers to DownloadAssembler - Implement new service icon layout (Ombi + Sonarr/Radarr icons) - Add CSS styling for service icons - Update dashboard routes to include Ombi configuration - Extend OpenAPI with Ombi tag and NormalizedDownload properties - Update documentation (README, ARCHITECTURE, SECURITY, CHANGELOG) - Add Ombi configuration to .env.sample
This commit is contained in:
@@ -0,0 +1,130 @@
|
||||
// Copyright (c) 2026 Gordon Bolton. MIT License.
|
||||
const axios = require('axios');
|
||||
const { logToFile } = require('../utils/logger');
|
||||
|
||||
/**
|
||||
* Ombi API client for fetching requests and searching media.
|
||||
* Provides integration with Ombi request management system.
|
||||
*/
|
||||
class OmbiClient {
|
||||
constructor(url, apiKey) {
|
||||
this.url = url.replace(/\/$/, ''); // Remove trailing slash
|
||||
this.apiKey = apiKey;
|
||||
this.axios = axios.create({
|
||||
headers: { 'ApiKey': this.apiKey },
|
||||
timeout: 10000
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all movie requests from Ombi
|
||||
* @returns {Promise<Array>} Array of movie request objects
|
||||
*/
|
||||
async getMovieRequests() {
|
||||
try {
|
||||
const response = await this.axios.get(`${this.url}/api/v1/Request/movie`);
|
||||
return response.data || [];
|
||||
} catch (error) {
|
||||
logToFile(`[OmbiClient] Movie requests error: ${error.message}`);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all TV requests from Ombi
|
||||
* @returns {Promise<Array>} Array of TV request objects
|
||||
*/
|
||||
async getTvRequests() {
|
||||
try {
|
||||
const response = await this.axios.get(`${this.url}/api/v1/Request/tv`);
|
||||
return response.data || [];
|
||||
} catch (error) {
|
||||
logToFile(`[OmbiClient] TV requests error: ${error.message}`);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Search for movies by TMDB ID
|
||||
* @param {string} tmdbId - TheMovieDB ID
|
||||
* @returns {Promise<Object|null>} Search result object or null if not found
|
||||
*/
|
||||
async searchMovieByTmdbId(tmdbId) {
|
||||
if (!tmdbId) return null;
|
||||
|
||||
try {
|
||||
const response = await this.axios.get(`${this.url}/api/v1/Search/movie/${tmdbId}`);
|
||||
return response.data || null;
|
||||
} catch (error) {
|
||||
logToFile(`[OmbiClient] Movie search error for TMDB ID ${tmdbId}: ${error.message}`);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Search for movies by IMDB ID
|
||||
* @param {string} imdbId - IMDB ID
|
||||
* @returns {Promise<Object|null>} Search result object or null if not found
|
||||
*/
|
||||
async searchMovieByImdbId(imdbId) {
|
||||
if (!imdbId) return null;
|
||||
|
||||
try {
|
||||
const response = await this.axios.get(`${this.url}/api/v1/Search/movie/imdb/${imdbId}`);
|
||||
return response.data || null;
|
||||
} catch (error) {
|
||||
logToFile(`[OmbiClient] Movie search error for IMDB ID ${imdbId}: ${error.message}`);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Search for TV shows by TVDB ID
|
||||
* @param {string} tvdbId - TheTVDB ID
|
||||
* @returns {Promise<Object|null>} Search result object or null if not found
|
||||
*/
|
||||
async searchTvByTvdbId(tvdbId) {
|
||||
if (!tvdbId) return null;
|
||||
|
||||
try {
|
||||
const response = await this.axios.get(`${this.url}/api/v1/Search/tv/${tvdbId}`);
|
||||
return response.data || null;
|
||||
} catch (error) {
|
||||
logToFile(`[OmbiClient] TV search error for TVDB ID ${tvdbId}: ${error.message}`);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Search for TV shows by TMDB ID
|
||||
* @param {string} tmdbId - TheMovieDB ID
|
||||
* @returns {Promise<Object|null>} Search result object or null if not found
|
||||
*/
|
||||
async searchTvByTmdbId(tmdbId) {
|
||||
if (!tmdbId) return null;
|
||||
|
||||
try {
|
||||
const response = await this.axios.get(`${this.url}/api/v1/Search/tv/tmdb/${tmdbId}`);
|
||||
return response.data || null;
|
||||
} catch (error) {
|
||||
logToFile(`[OmbiClient] TV search error for TMDB ID ${tmdbId}: ${error.message}`);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test connection to Ombi API
|
||||
* @returns {Promise<boolean>} True if connection is successful
|
||||
*/
|
||||
async testConnection() {
|
||||
try {
|
||||
const response = await this.axios.get(`${this.url}/api/v1/Request/movie`);
|
||||
return response.status === 200;
|
||||
} catch (error) {
|
||||
logToFile(`[OmbiClient] Connection test failed: ${error.message}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = OmbiClient;
|
||||
@@ -0,0 +1,260 @@
|
||||
// Copyright (c) 2026 Gordon Bolton. MIT License.
|
||||
const ArrRetriever = require('./ArrRetriever');
|
||||
const OmbiClient = require('./OmbiClient');
|
||||
const { logToFile } = require('../utils/logger');
|
||||
|
||||
/**
|
||||
* Ombi data retriever with caching support.
|
||||
* Extends ArrRetriever for PALDRA compliance.
|
||||
* Manages Ombi request data and provides lookup maps for efficient matching.
|
||||
*/
|
||||
class OmbiRetriever extends ArrRetriever {
|
||||
constructor(instanceConfig) {
|
||||
super(instanceConfig);
|
||||
this.client = new OmbiClient(this.url, this.apiKey);
|
||||
this.baseUrl = this.url;
|
||||
this.cache = {
|
||||
movieRequests: [],
|
||||
tvRequests: [],
|
||||
movieMap: new Map(), // tmdbId -> request
|
||||
tvMap: new Map(), // tvdbId -> request
|
||||
lastFetch: 0,
|
||||
ttl: 5 * 60 * 1000 // 5 minutes TTL
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get retriever type
|
||||
* @returns {string} The retriever type
|
||||
*/
|
||||
getRetrieverType() {
|
||||
return 'ombi';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the unique instance ID
|
||||
* @returns {string} The instance ID
|
||||
*/
|
||||
getInstanceId() {
|
||||
return this.id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get tags from Ombi (not applicable, returns empty array)
|
||||
* @returns {Promise<Array>} Empty array (Ombi doesn't have tags)
|
||||
*/
|
||||
async getTags() {
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get queue from Ombi (active requests)
|
||||
* @returns {Promise<Object>} Queue object with records array
|
||||
*/
|
||||
async getQueue() {
|
||||
await this.refreshCache();
|
||||
return {
|
||||
records: [...this.cache.movieRequests, ...this.cache.tvRequests]
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get history from Ombi (not applicable, returns empty records)
|
||||
* @param {Object} options - Optional parameters (ignored for Ombi)
|
||||
* @returns {Promise<Object>} History object with empty records array
|
||||
*/
|
||||
async getHistory(options = {}) {
|
||||
return {
|
||||
records: []
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Test connection to Ombi
|
||||
* @returns {Promise<boolean>} True if connection is successful
|
||||
*/
|
||||
async testConnection() {
|
||||
return await this.client.testConnection();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if cache is expired
|
||||
* @returns {boolean} True if cache needs refresh
|
||||
*/
|
||||
isCacheExpired() {
|
||||
return Date.now() - this.cache.lastFetch > this.cache.ttl;
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh cached data from Ombi API
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async refreshCache() {
|
||||
if (!this.isCacheExpired()) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
logToFile('[OmbiRetriever] Refreshing cache');
|
||||
|
||||
// Fetch requests in parallel
|
||||
const [movieRequests, tvRequests] = await Promise.all([
|
||||
this.client.getMovieRequests(),
|
||||
this.client.getTvRequests()
|
||||
]);
|
||||
|
||||
// Update cache
|
||||
this.cache.movieRequests = movieRequests;
|
||||
this.cache.tvRequests = tvRequests;
|
||||
this.cache.lastFetch = Date.now();
|
||||
|
||||
// Build lookup maps
|
||||
this.cache.movieMap.clear();
|
||||
this.cache.tvMap.clear();
|
||||
|
||||
// Build movie map (tmdbId -> request)
|
||||
movieRequests.forEach(request => {
|
||||
if (request.theMovieDbId) {
|
||||
this.cache.movieMap.set(request.theMovieDbId, request);
|
||||
}
|
||||
if (request.imdbId) {
|
||||
this.cache.movieMap.set(request.imdbId, request);
|
||||
}
|
||||
});
|
||||
|
||||
// Build TV map (tvdbId -> request, fallback to tmdbId)
|
||||
tvRequests.forEach(request => {
|
||||
if (request.theTvDbId) {
|
||||
this.cache.tvMap.set(request.theTvDbId, request);
|
||||
}
|
||||
if (request.theMovieDbId) {
|
||||
this.cache.tvMap.set(request.theMovieDbId, request);
|
||||
}
|
||||
});
|
||||
|
||||
logToFile(`[OmbiRetriever] Cache refreshed: ${movieRequests.length} movies, ${tvRequests.length} TV shows`);
|
||||
} catch (error) {
|
||||
logToFile(`[OmbiRetriever] Cache refresh failed: ${error.message}`);
|
||||
// Don't throw error, continue with stale cache if available
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all movie requests
|
||||
* @returns {Promise<Array>} Array of movie request objects
|
||||
*/
|
||||
async getMovieRequests() {
|
||||
await this.refreshCache();
|
||||
return this.cache.movieRequests;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all TV requests
|
||||
* @returns {Promise<Array>} Array of TV request objects
|
||||
*/
|
||||
async getTvRequests() {
|
||||
await this.refreshCache();
|
||||
return this.cache.tvRequests;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find movie request by external ID
|
||||
* @param {string} tmdbId - TheMovieDB ID
|
||||
* @param {string} imdbId - IMDB ID (optional fallback)
|
||||
* @returns {Promise<Object|null>} Request object or null if not found
|
||||
*/
|
||||
async findMovieRequest(tmdbId, imdbId = null) {
|
||||
await this.refreshCache();
|
||||
|
||||
// Try TMDB ID first
|
||||
if (tmdbId && this.cache.movieMap.has(tmdbId)) {
|
||||
return this.cache.movieMap.get(tmdbId);
|
||||
}
|
||||
|
||||
// Try IMDB ID as fallback
|
||||
if (imdbId && this.cache.movieMap.has(imdbId)) {
|
||||
return this.cache.movieMap.get(imdbId);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find TV request by external ID
|
||||
* @param {string} tvdbId - TheTVDB ID
|
||||
* @param {string} tmdbId - TheMovieDB ID (optional fallback)
|
||||
* @returns {Promise<Object|null>} Request object or null if not found
|
||||
*/
|
||||
async findTvRequest(tvdbId, tmdbId = null) {
|
||||
await this.refreshCache();
|
||||
|
||||
// Try TVDB ID first
|
||||
if (tvdbId && this.cache.tvMap.has(tvdbId)) {
|
||||
return this.cache.tvMap.get(tvdbId);
|
||||
}
|
||||
|
||||
// Try TMDB ID as fallback
|
||||
if (tmdbId && this.cache.tvMap.has(tmdbId)) {
|
||||
return this.cache.tvMap.get(tmdbId);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Search for movie by external ID (for fallback when no request found)
|
||||
* @param {string} tmdbId - TheMovieDB ID
|
||||
* @param {string} imdbId - IMDB ID (optional fallback)
|
||||
* @returns {Promise<Object|null>} Search result object or null if not found
|
||||
*/
|
||||
async searchMovie(tmdbId, imdbId = null) {
|
||||
if (tmdbId) {
|
||||
const result = await this.client.searchMovieByTmdbId(tmdbId);
|
||||
if (result) return result;
|
||||
}
|
||||
|
||||
if (imdbId) {
|
||||
const result = await this.client.searchMovieByImdbId(imdbId);
|
||||
if (result) return result;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Search for TV show by external ID (for fallback when no request found)
|
||||
* @param {string} tvdbId - TheTVDB ID
|
||||
* @param {string} tmdbId - TheMovieDB ID (optional fallback)
|
||||
* @returns {Promise<Object|null>} Search result object or null if not found
|
||||
*/
|
||||
async searchTv(tvdbId, tmdbId = null) {
|
||||
if (tvdbId) {
|
||||
const result = await this.client.searchTvByTvdbId(tvdbId);
|
||||
if (result) return result;
|
||||
}
|
||||
|
||||
if (tmdbId) {
|
||||
const result = await this.client.searchTvByTmdbId(tmdbId);
|
||||
if (result) return result;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cache statistics
|
||||
* @returns {Object} Cache statistics
|
||||
*/
|
||||
getCacheStats() {
|
||||
return {
|
||||
movieRequests: this.cache.movieRequests.length,
|
||||
tvRequests: this.cache.tvRequests.length,
|
||||
movieMapSize: this.cache.movieMap.size,
|
||||
tvMapSize: this.cache.tvMap.size,
|
||||
lastFetch: this.cache.lastFetch,
|
||||
age: Date.now() - this.cache.lastFetch
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = OmbiRetriever;
|
||||
Reference in New Issue
Block a user