Spaces:
Sleeping
Sleeping
| import express from 'express'; | |
| import swaggerUi from 'swagger-ui-express'; | |
| import YAML from 'yamljs'; | |
| import path from 'path'; | |
| import { fileURLToPath } from 'url'; | |
| import fs from 'fs/promises'; | |
| import crypto from 'crypto'; | |
| import { getAuthorData, getAuthorFilters } from 'stihirus-reader'; | |
| const __filename = fileURLToPath(import.meta.url); | |
| const __dirname = path.dirname(__filename); | |
| const app = express(); | |
| const port = process.env.PORT || 7860; | |
| const CACHE_DIR = path.join(__dirname, 'cache'); | |
| const CACHE_DURATION_MS = 60 * 60 * 1000; | |
| const swaggerDocument = YAML.load(path.join(__dirname, 'openapi.yaml')); | |
| app.use('/docs', swaggerUi.serve, swaggerUi.setup(swaggerDocument)); | |
| async function ensureCacheDir() { | |
| try { | |
| await fs.mkdir(CACHE_DIR, { recursive: true }); | |
| } catch (err) { | |
| console.error('Error creating cache directory:', err); | |
| } | |
| } | |
| function generateCacheKey(prefix, identifier, queryParams = {}) { | |
| const identifierPart = String(identifier).replace(/[^a-zA-Z0-9_-]/g, '_'); | |
| let queryPart = ''; | |
| const sortedKeys = Object.keys(queryParams).sort(); | |
| sortedKeys.forEach(key => { | |
| if (queryParams[key] !== undefined && queryParams[key] !== null) { | |
| queryPart += `_${key}_${String(queryParams[key])}`; | |
| } else if (queryParams[key] === null) { | |
| queryPart += `_${key}_null`; | |
| } | |
| }); | |
| return `${prefix}_${identifierPart}${queryPart}.json`; | |
| } | |
| async function readCache(key) { | |
| const filePath = path.join(CACHE_DIR, key); | |
| try { | |
| const stats = await fs.stat(filePath); | |
| const now = Date.now(); | |
| const mtime = stats.mtime.getTime(); | |
| const isStale = (now - mtime) > CACHE_DURATION_MS; | |
| const content = await fs.readFile(filePath, 'utf-8'); | |
| const data = JSON.parse(content); | |
| return { data, isStale, exists: true, mtime }; | |
| } catch (err) { | |
| if (err.code === 'ENOENT') { | |
| return { exists: false }; | |
| } | |
| console.error(`Error reading cache file ${key}:`, err); | |
| return { exists: false, error: true }; | |
| } | |
| } | |
| async function writeCache(key, data) { | |
| const filePath = path.join(CACHE_DIR, key); | |
| try { | |
| const content = JSON.stringify(data); | |
| await fs.writeFile(filePath, content, 'utf-8'); | |
| } catch (err) { | |
| console.error(`Error writing cache file ${key}:`, err); | |
| } | |
| } | |
| app.get('/author/:identifier', async (req, res) => { | |
| const identifier = req.params.identifier; | |
| let page = req.query.page; | |
| let delay = req.query.delay; | |
| let pageNum = null; | |
| if (page !== undefined) { | |
| const parsedPage = parseInt(page, 10); | |
| if (!isNaN(parsedPage) && parsedPage >= 0) { | |
| pageNum = parsedPage; | |
| } else if (page === 'null' || page === '') { | |
| pageNum = null; | |
| } else { | |
| return res.status(400).json({ status: 'error', error: { code: 400, message: 'Invalid page parameter. Use null, 0, or a positive integer.' } }); | |
| } | |
| } | |
| let delayMs = undefined; | |
| if (delay !== undefined) { | |
| const parsedDelay = parseInt(delay, 10); | |
| if (!isNaN(parsedDelay) && parsedDelay >= 0) { | |
| delayMs = parsedDelay; | |
| } else { | |
| return res.status(400).json({ status: 'error', error: { code: 400, message: 'Invalid delay parameter. Use a non-negative integer.' } }); | |
| } | |
| } | |
| const cacheKey = generateCacheKey('author', identifier, { page: pageNum }); | |
| let cacheEntry = null; | |
| try { | |
| cacheEntry = await readCache(cacheKey); | |
| if (cacheEntry.exists && !cacheEntry.isStale) { | |
| return res.json(cacheEntry.data); | |
| } | |
| const freshResult = await getAuthorData(identifier, pageNum, delayMs); | |
| if (freshResult.status === 'success') { | |
| await writeCache(cacheKey, freshResult); | |
| return res.json(freshResult); | |
| } else { | |
| if (cacheEntry.exists) { | |
| return res.json(cacheEntry.data); | |
| } else { | |
| return res.status(freshResult.error.code >= 400 && freshResult.error.code < 600 ? freshResult.error.code : 500).json(freshResult); | |
| } | |
| } | |
| } catch (err) { | |
| if (cacheEntry && cacheEntry.exists) { | |
| return res.json(cacheEntry.data); | |
| } else { | |
| console.error(`Error processing /author/${identifier} (CacheKey: ${cacheKey}):`, err); | |
| return res.status(500).json({ status: 'error', error: { code: 500, message: 'Internal Server Error', originalMessage: err.message } }); | |
| } | |
| } | |
| }); | |
| app.get('/author/:identifier/filters', async (req, res) => { | |
| const identifier = req.params.identifier; | |
| const cacheKey = generateCacheKey('filters', identifier); | |
| let cacheEntry = null; | |
| try { | |
| cacheEntry = await readCache(cacheKey); | |
| if (cacheEntry.exists && !cacheEntry.isStale) { | |
| return res.json(cacheEntry.data); | |
| } | |
| const freshResult = await getAuthorFilters(identifier); | |
| if (freshResult.status === 'success') { | |
| await writeCache(cacheKey, freshResult); | |
| return res.json(freshResult); | |
| } else { | |
| if (cacheEntry.exists) { | |
| return res.json(cacheEntry.data); | |
| } else { | |
| return res.status(freshResult.error.code >= 400 && freshResult.error.code < 600 ? freshResult.error.code : 500).json(freshResult); | |
| } | |
| } | |
| } catch (err) { | |
| if (cacheEntry && cacheEntry.exists) { | |
| return res.json(cacheEntry.data); | |
| } else { | |
| console.error(`Error processing /author/${identifier}/filters (CacheKey: ${cacheKey}):`, err); | |
| return res.status(500).json({ status: 'error', error: { code: 500, message: 'Internal Server Error', originalMessage: err.message } }); | |
| } | |
| } | |
| }); | |
| app.get('/', (req, res) => { | |
| res.redirect('/docs'); | |
| }); | |
| ensureCacheDir().then(() => { | |
| app.listen(port, () => { | |
| console.log(`StihiRus API wrapper listening on port ${port}`); | |
| console.log(`Cache directory: ${CACHE_DIR}`); | |
| console.log(`API Docs available at /docs`); | |
| }); | |
| }).catch(err => { | |
| console.error("Failed to initialize cache directory. Exiting.", err); | |
| process.exit(1); | |
| }); |