cc8d0f6969
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
166 lines
4.9 KiB
TypeScript
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();
|