diff --git a/package.json b/package.json index 8204998..372de7e 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "hpagent": "^1.2.0", "openai": "^4.0.0", "pg": "^8.12.0", + "pg-pool": "^3.6.2", "puppeteer": "^23.1.0", "redis": "^4.7.0", "sharp": "^0.33.5", diff --git a/src/cache/index.ts b/src/cache/index.ts index 4cc68de..ba354c1 100644 --- a/src/cache/index.ts +++ b/src/cache/index.ts @@ -2,26 +2,21 @@ import { RedisClientType, createClient } from 'redis'; let cacheClient: RedisClientType | null = null; -export async function getCacheClient(): Promise { - // If the client is already created, return it - if (cacheClient) return cacheClient; - - if (!process.env.REDIS_URL) { - console.warn('Redis URL is not defined. Cache is disabled.'); - return null; +export function getCacheClient(): RedisClientType | null { + if (!cacheClient && process.env.REDIS_URL) { + console.log('Initializing Redis Cache Client', process.env.REDIS_URL); + cacheClient = createClient({ url: process.env.REDIS_URL }); + cacheClient.on('error', (err) => console.error('Redis Client Error', err)); + cacheClient.on('connect', () => console.log('Connected to Redis')); + cacheClient.connect().catch((err) => { + console.error('Failed to connect to Redis:', err); + cacheClient = null; // Avoid using an invalid client + }); } - console.log('Creating Redis Cache Client', process.env.REDIS_URL); - if (cacheClient) return cacheClient; - - cacheClient = createClient({ url: process.env.REDIS_URL }); - cacheClient.on('error', (err) => console.error('Redis Client Error', err)); - - await cacheClient.connect(); return cacheClient; } - export async function closeCacheClient(): Promise { if (cacheClient) { await cacheClient.quit(); diff --git a/src/context/index.ts b/src/context/index.ts new file mode 100644 index 0000000..715352b --- /dev/null +++ b/src/context/index.ts @@ -0,0 +1,29 @@ +// context.ts +import { RedisClientType } from 'redis'; +import { Pool, Client as PgClient } from 'pg'; +import { getCacheClient, closeCacheClient } from '../cache'; +import { getDBClient, closeDBClient } from '../repository'; + +export interface AppContext { + cacheClient: RedisClientType | null; + dbClient: Pool | PgClient | null; +} + +let context: AppContext = { + cacheClient: null, + dbClient: null, +}; + +export async function initializeContext(): Promise { + context.cacheClient = await getCacheClient(); + context.dbClient = await getDBClient(); +} + +export function getContext(): AppContext { + return context; +} + +export async function closeContext(): Promise { + await closeCacheClient(); + await closeDBClient(); +} diff --git a/src/handlers/catalogHandler.ts b/src/handlers/catalogHandler.ts index 5340ed3..72b7025 100644 --- a/src/handlers/catalogHandler.ts +++ b/src/handlers/catalogHandler.ts @@ -1,9 +1,9 @@ import axios from 'axios'; import { getRatingsfromDB, scrapeRatings } from '../utils/ratingScrapers'; import { CINEMETA_BASE_URL, CINEMETA_CATALOG_URL } from '../constants/urls'; -import { closeCacheClient, getCacheClient } from '../cache'; import { DEFAULT_PROVIDERS } from '../constants/costants'; import { isDatabaseConnected } from '../repository'; +import { getContext } from '../context'; async function fetchCatalog(url: string, providers: string[]): Promise { const response = await axios.get(url); @@ -38,17 +38,13 @@ export async function bestYearByYearCatalog(type: string, extra: any, providers: return fetchCatalog(url, providers); } -export async function handleCatalogRequest({ id, type, extra, config }: any) { +export async function handleCatalogRequest({ id, type, extra, config }: any): Promise { let providers = DEFAULT_PROVIDERS; if (config && config.providers) { providers = config.providers; } - let cacheClient = await getCacheClient(); + let cacheClient = await getContext().cacheClient; const key = id + JSON.stringify(extra); - if (cacheClient != null && !cacheClient.isOpen) { - console.log("Cache is not open, opening it again"); - cacheClient = await getCacheClient(); - } try { // Check if the response is cached const cachedResponse = await cacheClient?.get(key); @@ -81,7 +77,5 @@ export async function handleCatalogRequest({ id, type, extra, config }: any) { } catch (error) { console.error("Error in CatalogHandler:", error); return { metas: [] }; - } finally { - closeCacheClient(); } } \ No newline at end of file diff --git a/src/handlers/metaHandler.ts b/src/handlers/metaHandler.ts index c744d6f..9511bac 100644 --- a/src/handlers/metaHandler.ts +++ b/src/handlers/metaHandler.ts @@ -1,7 +1,8 @@ import { DEFAULT_PROVIDERS } from '../constants/costants'; import { scrapeRatings } from '../utils/ratingScrapers'; +import { MetaDetail } from 'stremio-addon-sdk'; -export async function handleMetaRequest({ id, type, extra, config }: any) { +export async function handleMetaRequest({ id, type, extra, config }: any): Promise { let providers = DEFAULT_PROVIDERS; if (config && config.providers) { providers = config.providers; diff --git a/src/index.ts b/src/index.ts index fab4dd8..14a951a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -3,42 +3,51 @@ import { handleMetaRequest } from "./handlers/metaHandler"; import { handleCatalogRequest } from "./handlers/catalogHandler"; import manifest from "./manifest"; import dotenv from "dotenv"; -import { closeClient, getClient } from "./repository"; -import { get } from "http"; +import { closeDBClient, getDBClient } from "./repository"; +import { closeCacheClient, getCacheClient } from "./cache"; +import pg from 'pg'; +import { initializeContext } from "./context"; dotenv.config(); -const builder = new addonBuilder(manifest); +initializeContext().then(() => { + const builder = new addonBuilder(manifest); -// Catalog Handlers -builder.defineCatalogHandler(async (args: Args) => { - console.log("CatalogHandler args:", args); - await getClient(); - try { - return await handleCatalogRequest(args); - } catch (error) { - console.error("Error in CatalogHandler:", error); - return { metas: [] }; - } finally{ - console.log("CatalogHandler finally"); - closeClient(); - } -}); + // Catalog Handlers + builder.defineCatalogHandler(async (args: Args) => { + console.log("CatalogHandler args:", args); + await getDBClient(); + try { + return await handleCatalogRequest(args); + } catch (error) { + console.error("Error in CatalogHandler:", error); + return { metas: [] }; + } + }); -// Meta Handlers -builder.defineMetaHandler(async (args: { type: ContentType, id: string }) => { - await getClient(); - try { - return { meta: await handleMetaRequest(args) }; - } catch (error) { - console.error("Error in MetaHandler:", error); - return { meta: {} as any }; - } finally { - closeClient(); - } -}); + // Meta Handlers + builder.defineMetaHandler(async (args: { type: ContentType, id: string }) => { + await getDBClient(); + try { + return { meta: await handleMetaRequest(args) }; + } catch (error) { + console.error("Error in MetaHandler:", error); + return { meta: {} as any }; + } + }); -// Additional handlers (stream, subtitle, etc.) can be added similarly -const port = Number(process.env.PORT) || 3000; -serveHTTP(builder.getInterface(), { port: port }); -console.log(`🚀 Link for addon http://localhost:${port}`); + // Graceful shutdown + process.on('SIGINT', async () => { + await closeDBClient(); // Close database connection + await closeCacheClient(); // Close Redis client + process.exit(0); + }); + + // Additional handlers (stream, subtitle, etc.) can be added similarly + const port = Number(process.env.PORT) || 3000; + serveHTTP(builder.getInterface(), { port: port }); + console.log(`🚀 Link for addon http://localhost:${port}`); +}).catch((error) => { + console.error('Failed to initialize context:', error); + process.exit(1); +}); diff --git a/src/repository/index.ts b/src/repository/index.ts index 16d7107..5285d6b 100644 --- a/src/repository/index.ts +++ b/src/repository/index.ts @@ -1,57 +1,62 @@ -import pg from 'pg' -const { Client } = pg +import pg from 'pg'; +const { Pool } = pg; -let client: pg.Client | null = null +let pool: pg.Pool | null = null; - ; - -export async function getClient(): Promise { - const connectionStr = process.env.DATABASE_URL +export async function getDBClient(): Promise { + const connectionStr = process.env.DATABASE_URL; if (!connectionStr) { - console.error('DATABASE_URL so not using database') + console.error('DATABASE_URL is not defined'); return null; } - if (client) { - console.error('Already connected to database') - return client + + if (pool) { + return pool; } - client = new Client({ connectionString: connectionStr, ssl: { rejectUnauthorized: false } }) - client.on('error', (error) => { - console.error('Database error:', error) - }); - client.on('end', () => { - console.log('Database connection closed') + + pool = new Pool({ connectionString: connectionStr, ssl: { rejectUnauthorized: false } }); + pool.on('error', (error) => { + console.error('Database pool error:', error); }); - client.connect() + + return pool; } -export async function closeClient() { - if (client) { - await client.end() - client = null +export async function closeDBClient(): Promise { + if (pool) { + try { + await pool.end(); + } catch (error) { + console.error('Error closing database pool:', error); + } finally { + pool = null; + } } } export async function getRatingsfromTTIDs(ttids: string[]): Promise>> { - if (!client) { - throw new Error('Not connected to database') + const pool = await getDBClient(); + if (!pool) { + throw new Error('Not connected to database'); } try { - const query = `SELECT ttid, ratings.provider, rating FROM ratings WHERE ttid = ANY($1)` - const { rows } = await client.query(query, [ttids]) + const client = await pool.connect(); + const query = `SELECT ttid, ratings.provider, rating FROM ratings WHERE ttid = ANY($1)`; + const { rows } = await client.query(query, [ttids]); + client.release(); // Release client back to pool return rows.reduce((acc: Record>, row: any) => { if (!acc[row.ttid]) { - acc[row.ttid] = {} + acc[row.ttid] = {}; } - acc[row.ttid][row.provider] = row.rating - return acc + acc[row.ttid][row.provider] = row.rating; + return acc; }, {}); } catch (error) { - console.error('Error fetching ratings from database:', error) - return {} + console.error('Error fetching ratings from database:', error); + return {}; } } export function isDatabaseConnected(): boolean { - return client != null + return pool != null; } \ No newline at end of file diff --git a/src/utils/ratingScrapers.ts b/src/utils/ratingScrapers.ts index 3acfb8e..243105b 100644 --- a/src/utils/ratingScrapers.ts +++ b/src/utils/ratingScrapers.ts @@ -1,10 +1,10 @@ import { MetaDetail } from 'stremio-addon-sdk'; -import { getCacheClient } from '../cache'; import { fetchBingRatings, fetchGoogleRatings, fetchYahooRatings, getMetadata } from './api'; import { RedisClientType } from 'redis'; import { addRatingToImage } from './image'; import axios from 'axios'; import { getRatingsfromTTIDs } from '../repository'; +import { getContext } from '../context'; export async function getRatingsFromGoogle(query: string, imdbId: string, cacheClient: RedisClientType | null): Promise> { try { @@ -73,7 +73,8 @@ export async function getRatingsFromYahoo(query: string, imdbId: string, cacheCl } export async function scrapeRatings(imdbId: string, type: string, providers: string[]): Promise { - const cacheClient = await getCacheClient(); + + const cacheClient = getContext().cacheClient; const metadata = await getMetadata(imdbId, type); let ratingMap: Record = {}; try {