Files
muzick/backend/src/routes/discovery.routes.ts
T
kami 4c48d11e9d
Typecheck / typecheck (backend) (push) Has been cancelled
Typecheck / typecheck (workers) (push) Has been cancelled
feat: enhance discovery, vibe sessions, and library enrichment
2026-08-01 14:40:48 +04:00

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 });
});
}