108 lines
4.1 KiB
TypeScript
108 lines
4.1 KiB
TypeScript
import { FastifyInstance } from 'fastify';
|
|
import { DbService } from '../services/db.service.js';
|
|
import { DiscoveryService } from '../services/discovery.service.js';
|
|
import { ImageEnrichmentService } from '../services/image-enrichment.service.js';
|
|
import { JobService } from '../services/job.service.js';
|
|
|
|
export default async function discoveryRoutes(fastify: FastifyInstance, options: { dbService: DbService; jobService: JobService }) {
|
|
const { dbService } = options;
|
|
const { jobService } = options;
|
|
const discovery = new DiscoveryService(dbService);
|
|
const images = new ImageEnrichmentService(dbService);
|
|
|
|
/**
|
|
* POST /api/discovery/walk — trigger graph walk for discovery candidates
|
|
*/
|
|
fastify.post('/discovery/walk', async (request, reply) => {
|
|
const userId = (request.headers['x-user-id'] as string) || '00000000-0000-0000-0000-000000000000';
|
|
const count = await discovery.walkGraphForDiscovery(userId);
|
|
return reply.send({ newCandidates: count });
|
|
});
|
|
|
|
/**
|
|
* GET /api/discovery/candidates — list discovery candidates
|
|
* Query: ?status=candidate&limit=50
|
|
*/
|
|
fastify.get('/discovery/candidates', async (request, reply) => {
|
|
const query = request.query as { status?: string; limit?: string };
|
|
const status = query.status || 'candidate';
|
|
const limit = parseInt(query.limit || '50', 10);
|
|
|
|
const res = await dbService.pgClient.query(
|
|
`SELECT * FROM discovery_candidates WHERE status = $1 ORDER BY first_seen_at DESC LIMIT $2`,
|
|
[status, limit]
|
|
);
|
|
return reply.send({ candidates: res.rows });
|
|
});
|
|
|
|
/**
|
|
* POST /api/discovery/eval — evaluate pending candidates for acquisition
|
|
*/
|
|
fastify.post('/discovery/eval', async (request, reply) => {
|
|
const userId = (request.headers['x-user-id'] as string) || '00000000-0000-0000-0000-000000000000';
|
|
const results = await discovery.evalCandidates(userId);
|
|
for (const result of results) {
|
|
if (!result.shouldAcquire) continue;
|
|
try {
|
|
await jobService.enqueueDiscoveryAcquisition(result.candidateId);
|
|
} catch (err) {
|
|
const reason = err instanceof Error ? err.message : 'failed to enqueue acquisition';
|
|
await discovery.markEnqueueFailed(result.candidateId, reason);
|
|
result.shouldAcquire = false;
|
|
result.reason = `queue unavailable: ${reason}`;
|
|
}
|
|
}
|
|
return reply.send({ evaluated: results.length, results });
|
|
});
|
|
|
|
/**
|
|
* POST /api/discovery/sweep-probation — evaluate probation tracks
|
|
*/
|
|
fastify.post('/discovery/sweep-probation', async (_request, reply) => {
|
|
const result = await discovery.sweepProbation();
|
|
return reply.send(result);
|
|
});
|
|
|
|
/**
|
|
* POST /api/discovery/meta-learn — run meta-learning
|
|
*/
|
|
fastify.post('/discovery/meta-learn', async (_request, reply) => {
|
|
await discovery.runMetaLearning();
|
|
return reply.send({ status: 'ok' });
|
|
});
|
|
|
|
/**
|
|
* POST /api/images/fetch — mark image candidates for an entity
|
|
* Body: { entity_type, entity_id }
|
|
*/
|
|
fastify.post('/images/fetch', async (request, reply) => {
|
|
const body = request.body as { entity_type: string; entity_id: string };
|
|
if (!body.entity_type || !body.entity_id) {
|
|
return reply.code(400).send({ error: 'entity_type and entity_id required' });
|
|
}
|
|
|
|
let count = 0;
|
|
if (body.entity_type === 'artist') {
|
|
count = await images.fetchImagesForArtist(body.entity_id);
|
|
} else if (body.entity_type === 'album') {
|
|
count = await images.fetchImagesForAlbum(body.entity_id);
|
|
} else {
|
|
return reply.code(400).send({ error: 'entity_type must be "artist" or "album"' });
|
|
}
|
|
return reply.send({ candidateRows: count });
|
|
});
|
|
|
|
/**
|
|
* POST /api/images/select — select best image for an entity
|
|
* Body: { entity_type, entity_id }
|
|
*/
|
|
fastify.post('/images/select', async (request, reply) => {
|
|
const body = request.body as { entity_type: string; entity_id: string };
|
|
if (!body.entity_type || !body.entity_id) {
|
|
return reply.code(400).send({ error: 'entity_type and entity_id required' });
|
|
}
|
|
const url = await images.selectBestImage(body.entity_type, body.entity_id);
|
|
return reply.send({ url });
|
|
});
|
|
}
|