5390bbf615
Build and Push Docker Image / build (push) Successful in 2m6s
Docs Check / Markdown lint (push) Successful in 1m58s
Licence Check / Licence compatibility and copyright header verification (push) Successful in 2m4s
Docs Check / Mermaid diagram parse check (push) Successful in 1m58s
CI / Security audit (push) Successful in 1m48s
CI / Tests & coverage (push) Successful in 1m59s
CI / Swagger Validation & Coverage (push) Successful in 1m47s
324 lines
9.1 KiB
JavaScript
324 lines
9.1 KiB
JavaScript
// 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: [],
|
|
users: [],
|
|
movieMap: new Map(), // tmdbId -> request
|
|
tvMap: new Map(), // tvdbId -> request
|
|
userMap: new Map(), // id -> user
|
|
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
|
|
* @param {boolean} force - Whether to force a refresh regardless of TTL
|
|
* @returns {Promise<void>}
|
|
*/
|
|
async refreshCache(force = false) {
|
|
if (!force && !this.isCacheExpired()) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
logToFile('[OmbiRetriever] Refreshing cache');
|
|
|
|
// Fetch requests and users in parallel
|
|
const [movieRequests, tvRequests, users] = await Promise.all([
|
|
this.client.getMovieRequests(),
|
|
this.client.getTvRequests(),
|
|
this.client.getUsers()
|
|
]);
|
|
|
|
// Update cache
|
|
this.cache.movieRequests = movieRequests;
|
|
this.cache.tvRequests = tvRequests;
|
|
this.cache.users = users;
|
|
this.cache.lastFetch = Date.now();
|
|
|
|
// Build lookup maps
|
|
this.cache.movieMap.clear();
|
|
this.cache.tvMap.clear();
|
|
this.cache.userMap.clear();
|
|
|
|
// Build user map (id -> user)
|
|
if (Array.isArray(users)) {
|
|
users.forEach(user => {
|
|
if (user && user.id) {
|
|
this.cache.userMap.set(user.id, user);
|
|
}
|
|
});
|
|
}
|
|
|
|
// 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, ${users.length} users`);
|
|
} catch (error) {
|
|
logToFile(`[OmbiRetriever] Cache refresh failed: ${error.message}`);
|
|
// Don't throw error, continue with stale cache if available
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Hydrates requestedUser on a single request using the userMap cache
|
|
* @param {Object} req - The request object
|
|
* @returns {Object} Hydrated request object
|
|
* @private
|
|
*/
|
|
_hydrateRequest(req) {
|
|
if (!req) return req;
|
|
|
|
const reqUserId = req.requestedUserId || req.RequestedUserId;
|
|
if (reqUserId && this.cache.userMap.has(reqUserId)) {
|
|
const cachedUser = this.cache.userMap.get(reqUserId);
|
|
|
|
let requestedUser = req.requestedUser || req.RequestedUser;
|
|
|
|
// If requestedUser is not an object or is empty/null, populate it
|
|
if (!requestedUser || typeof requestedUser !== 'object' || Object.keys(requestedUser).length === 0) {
|
|
const hydratedUser = {
|
|
id: cachedUser.id,
|
|
userName: cachedUser.userName,
|
|
alias: cachedUser.alias || cachedUser.Alias || '',
|
|
userAlias: cachedUser.userAlias || cachedUser.UserAlias || '',
|
|
normalizedUserName: cachedUser.normalizedUserName || cachedUser.NormalizedUserName || ''
|
|
};
|
|
|
|
return {
|
|
...req,
|
|
requestedUser: hydratedUser,
|
|
RequestedUser: hydratedUser
|
|
};
|
|
}
|
|
}
|
|
return req;
|
|
}
|
|
|
|
/**
|
|
* Hydrates requestedUser on a list of requests using the userMap cache
|
|
* @param {Array} requests - Array of request objects
|
|
* @returns {Array} Array of hydrated request objects
|
|
* @private
|
|
*/
|
|
_hydrateRequests(requests) {
|
|
if (!Array.isArray(requests)) return [];
|
|
return requests.map(req => this._hydrateRequest(req));
|
|
}
|
|
|
|
/**
|
|
* Get all movie requests
|
|
* @param {boolean} force - Whether to force refresh from API
|
|
* @returns {Promise<Array>} Array of movie request objects
|
|
*/
|
|
async getMovieRequests(force = false) {
|
|
await this.refreshCache(force);
|
|
return this._hydrateRequests(this.cache.movieRequests);
|
|
}
|
|
|
|
/**
|
|
* Get all TV requests
|
|
* @param {boolean} force - Whether to force refresh from API
|
|
* @returns {Promise<Array>} Array of TV request objects
|
|
*/
|
|
async getTvRequests(force = false) {
|
|
await this.refreshCache(force);
|
|
return this._hydrateRequests(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._hydrateRequest(this.cache.movieMap.get(tmdbId));
|
|
}
|
|
|
|
// Try IMDB ID as fallback
|
|
if (imdbId && this.cache.movieMap.has(imdbId)) {
|
|
return this._hydrateRequest(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._hydrateRequest(this.cache.tvMap.get(tvdbId));
|
|
}
|
|
|
|
// Try TMDB ID as fallback
|
|
if (tmdbId && this.cache.tvMap.has(tmdbId)) {
|
|
return this._hydrateRequest(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;
|