Files
lidify/backend/src/services/wikidata.ts
T
Your Name cc8d0f6969 Release v1.3.0: Multi-source downloads, audio analyzer resilience, mobile improvements
Major Features:
- Multi-source download system (Soulseek/Lidarr with fallback)
- Configurable enrichment speed control (1-5x)
- Mobile touch drag support for seek sliders
- iOS PWA media controls (Control Center, Lock Screen)
- Artist name alias resolution via Last.fm
- Circuit breaker pattern for audio analysis

Critical Fixes:
- Audio analyzer stability (non-ASCII, BrokenProcessPool, OOM)
- Discovery system race conditions and import failures
- Radio decade categorization using originalYear
- LastFM API response normalization
- Mood bucket infinite loop prevention

Security:
- Bull Board admin authentication
- Lidarr webhook signature verification
- JWT token expiration and refresh
- Encryption key validation on startup

Closes #2, #6, #9, #13, #21, #26, #31, #34, #35, #37, #40, #43
2026-01-06 20:07:33 -06:00

166 lines
4.9 KiB
TypeScript

import axios, { AxiosInstance } from "axios";
import { logger } from "../utils/logger";
import { redisClient } from "../utils/redis";
interface WikidataResult {
summary?: string;
heroUrl?: string;
}
class WikidataService {
private client: AxiosInstance;
constructor() {
this.client = axios.create({
timeout: 10000,
headers: {
"User-Agent":
"Lidify/1.0.0 (https://github.com/Chevron7Locked/lidify)",
},
});
}
async getArtistInfo(
artistName: string,
mbid: string
): Promise<WikidataResult> {
const cacheKey = `wikidata:${mbid}`;
try {
const cached = await redisClient.get(cacheKey);
if (cached) {
return JSON.parse(cached);
}
} catch (err) {
logger.warn("Redis get error:", err);
}
try {
// Step 1: Query Wikidata for the MusicBrainz ID
const wikidataId = await this.getWikidataIdFromMBID(mbid);
if (!wikidataId) {
logger.debug(`No Wikidata entry found for ${artistName}`);
return {};
}
// Step 2: Get Wikipedia article and image
const [summary, heroUrl] = await Promise.all([
this.getWikipediaSummary(wikidataId),
this.getWikidataImage(wikidataId),
]);
const result: WikidataResult = { summary, heroUrl };
// Cache for 30 days
try {
await redisClient.setEx(
cacheKey,
2592000,
JSON.stringify(result)
);
} catch (err) {
logger.warn("Redis set error:", err);
}
return result;
} catch (error) {
logger.error(`Wikidata fetch failed for ${artistName}:`, error);
return {};
}
}
private async getWikidataIdFromMBID(mbid: string): Promise<string | null> {
const sparqlQuery = `
SELECT ?item WHERE {
?item wdt:P434 "${mbid}" .
}
LIMIT 1
`;
const response = await axios.get("https://query.wikidata.org/sparql", {
params: {
query: sparqlQuery,
format: "json",
},
headers: {
"User-Agent": "Lidify/1.0.0",
},
});
const bindings = response.data.results?.bindings || [];
if (bindings.length === 0) return null;
const itemUrl = bindings[0].item.value;
return itemUrl.split("/").pop() || null;
}
private async getWikipediaSummary(
wikidataId: string
): Promise<string | undefined> {
try {
// Get English Wikipedia article title
const response = await axios.get(
`https://www.wikidata.org/wiki/Special:EntityData/${wikidataId}.json`
);
const entity = response.data.entities?.[wikidataId];
const enWikiTitle = entity?.sitelinks?.enwiki?.title;
if (!enWikiTitle) return undefined;
// Get article summary from Wikipedia API
const summaryResponse = await axios.get(
"https://en.wikipedia.org/api/rest_v1/page/summary/" +
encodeURIComponent(enWikiTitle)
);
return summaryResponse.data.extract;
} catch (error) {
logger.error(
`Failed to get Wikipedia summary for ${wikidataId}:`,
error
);
return undefined;
}
}
private async getWikidataImage(
wikidataId: string
): Promise<string | undefined> {
try {
const response = await axios.get(
`https://www.wikidata.org/wiki/Special:EntityData/${wikidataId}.json`
);
const entity = response.data.entities?.[wikidataId];
const imageProperty = entity?.claims?.P18; // P18 is "image"
if (!imageProperty || imageProperty.length === 0) return undefined;
const imageName = imageProperty[0].mainsnak?.datavalue?.value;
if (!imageName) return undefined;
// Convert to Wikimedia Commons URL
const fileName = imageName.replace(/ /g, "_");
const md5 = require("crypto")
.createHash("md5")
.update(fileName)
.digest("hex");
const url = `https://upload.wikimedia.org/wikipedia/commons/${
md5[0]
}/${md5[0]}${md5[1]}/${encodeURIComponent(fileName)}`;
return url;
} catch (error) {
logger.error(
`Failed to get Wikidata image for ${wikidataId}:`,
error
);
return undefined;
}
}
}
export const wikidataService = new WikidataService();