mirror of
https://github.com/mue/mue.git
synced 2026-07-21 16:04:22 +02:00
feat: integrate Wikidata API for fetching author information, including images and occupations
This commit is contained in:
@@ -20,17 +20,22 @@ export default function AuthorInfo({
|
||||
const t = useT();
|
||||
const showAuthorImg = localStorage.getItem('authorImg') !== 'false';
|
||||
const hasLink = authorOccupation !== 'Unknown' && authorlink !== null;
|
||||
const trimmedLicense = authorimglicense?.substring(0, 40) +
|
||||
(authorimglicense?.length > 40 ? '…' : '');
|
||||
|
||||
return (
|
||||
<div className="author-holder">
|
||||
<div className="author">
|
||||
{showAuthorImg && (
|
||||
<div className="author-img" style={{ backgroundImage: `url(${authorimg})` }}>
|
||||
{!authorimg && authorimg !== undefined && <MdPerson />}
|
||||
</div>
|
||||
)}
|
||||
{showAuthorImg &&
|
||||
(authorimglicense && authorimg ? (
|
||||
<Tooltip title={authorimglicense}>
|
||||
<div className="author-img" style={{ backgroundImage: `url(${authorimg})` }}>
|
||||
{!authorimg && authorimg !== undefined && <MdPerson />}
|
||||
</div>
|
||||
</Tooltip>
|
||||
) : (
|
||||
<div className="author-img" style={{ backgroundImage: `url(${authorimg})` }}>
|
||||
{!authorimg && authorimg !== undefined && <MdPerson />}
|
||||
</div>
|
||||
))}
|
||||
|
||||
{author ? (
|
||||
<div className="author-content">
|
||||
@@ -38,11 +43,6 @@ export default function AuthorInfo({
|
||||
{authorOccupation && authorOccupation !== 'Unknown' && (
|
||||
<span className="subtitle">{authorOccupation}</span>
|
||||
)}
|
||||
{authorimglicense && (
|
||||
<span className="author-license" title={authorimglicense}>
|
||||
{trimmedLicense}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="author-content whileLoading">
|
||||
|
||||
@@ -2,21 +2,13 @@ import { useCallback } from 'react';
|
||||
import variables from 'config/variables';
|
||||
import offline_quotes from '../offline_quotes.json';
|
||||
import { shouldUpdateByFrequency, resetStartTime } from 'utils/frequencyManager';
|
||||
import { safeParseJSON } from 'utils/jsonStorage';
|
||||
import { useCachedFetch } from 'hooks/useCachedFetch';
|
||||
import { QueueManager } from 'utils/backgroundQueue';
|
||||
import { fetchAuthorFromWikidata } from 'utils/wikidata/wikidataAuthorFetcher';
|
||||
|
||||
/**
|
||||
* Custom hook for loading quote data from various sources
|
||||
*/
|
||||
export function useQuoteLoader(updateQuote) {
|
||||
// Initialize cache hook for author images
|
||||
const { fetchWithCache: fetchAuthorImage } = useCachedFetch({
|
||||
cacheKey: 'authorImageCache',
|
||||
timestampKey: 'authorImageCacheTimestamp',
|
||||
expiryDays: 7,
|
||||
});
|
||||
|
||||
const getAuthorLink = useCallback((author) => {
|
||||
return localStorage.getItem('authorLink') === 'false' || author === 'Unknown'
|
||||
? null
|
||||
@@ -25,78 +17,75 @@ export function useQuoteLoader(updateQuote) {
|
||||
.join('_')}`;
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* Fetch author data from Wikidata (image, occupation, description, etc.)
|
||||
* @param {string} author - Author name
|
||||
* @returns {Promise<object>} Author data including image, occupation, and license info
|
||||
*/
|
||||
const getAuthorDataFromWikidata = useCallback(
|
||||
async (author) => {
|
||||
if (localStorage.getItem('authorImg') === 'false' || author === 'Unknown') {
|
||||
return {
|
||||
authorimg: null,
|
||||
authorimglicense: null,
|
||||
authorOccupation: null,
|
||||
authorlink: null,
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const lang = variables.languagecode.split('_')[0];
|
||||
const wikidataResult = await fetchAuthorFromWikidata(author, lang);
|
||||
|
||||
if (!wikidataResult) {
|
||||
return {
|
||||
authorimg: null,
|
||||
authorimglicense: null,
|
||||
authorOccupation: null,
|
||||
authorlink: getAuthorLink(author), // Fallback to Wikipedia link
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
authorimg: wikidataResult.imageUrl || null,
|
||||
authorimglicense: wikidataResult.imageLicense || null,
|
||||
authorOccupation: wikidataResult.occupation || null,
|
||||
authorlink: wikidataResult.wikipediaLink || getAuthorLink(author),
|
||||
};
|
||||
} catch (e) {
|
||||
console.error('Error fetching author data from Wikidata:', e);
|
||||
return {
|
||||
authorimg: null,
|
||||
authorimglicense: null,
|
||||
authorOccupation: null,
|
||||
authorlink: getAuthorLink(author),
|
||||
};
|
||||
}
|
||||
},
|
||||
[getAuthorLink],
|
||||
);
|
||||
|
||||
const stripHTML = useCallback((html) => {
|
||||
const tmpdoc = new DOMParser().parseFromString(html, 'text/html');
|
||||
return tmpdoc.body.textContent || '';
|
||||
}, []);
|
||||
|
||||
const getAuthorImg = useCallback(
|
||||
// Get cached author data or fetch from Wikidata and cache it
|
||||
const getCachedAuthorData = useCallback(
|
||||
async (author) => {
|
||||
if (localStorage.getItem('authorImg') === 'false' || author === 'Unknown') {
|
||||
return { authorimg: null, authorimglicense: null };
|
||||
return {
|
||||
authorimg: null,
|
||||
authorimglicense: null,
|
||||
authorOccupation: null,
|
||||
authorlink: null,
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const lang = variables.languagecode.split('_')[0];
|
||||
const pageData = await fetch(
|
||||
`https://${lang}.wikipedia.org/w/api.php?action=query&titles=${author}&origin=*&prop=pageimages&format=json&pithumbsize=100`,
|
||||
).then((res) => res.json());
|
||||
|
||||
const authorPage = Object.values(pageData.query.pages)[0];
|
||||
const authorimg = authorPage?.thumbnail?.source;
|
||||
|
||||
if (!authorimg) {
|
||||
return { authorimg: null, authorimglicense: null };
|
||||
}
|
||||
|
||||
const licenseData = await fetch(
|
||||
`https://${lang}.wikipedia.org/w/api.php?action=query&prop=imageinfo&iiprop=extmetadata&titles=File:${authorPage.pageimage}&origin=*&format=json`,
|
||||
).then((res) => res.json());
|
||||
|
||||
const licensePage = Object.values(licenseData.query.pages)[0];
|
||||
const metadata = licensePage?.imageinfo?.[0]?.extmetadata;
|
||||
const license = metadata?.LicenseShortName;
|
||||
const photographer = stripHTML(
|
||||
metadata?.Attribution?.value || metadata?.Artist?.value || '',
|
||||
)
|
||||
.replace(/©\s/, '')
|
||||
.replace(/ \(talk\)/, '');
|
||||
|
||||
if (!photographer) {
|
||||
return { authorimg: null, authorimglicense: null };
|
||||
}
|
||||
|
||||
if (license?.value === 'Public domain') {
|
||||
return { authorimg, authorimglicense: null };
|
||||
}
|
||||
|
||||
const authorimglicense = photographer
|
||||
? `© ${photographer}${license ? `. ${license.value}` : ''}`.replace(/copyright\s/i, '')
|
||||
: license?.value || null;
|
||||
|
||||
return { authorimg, authorimglicense };
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
return { authorimg: null, authorimglicense: null };
|
||||
}
|
||||
// Wikidata fetcher has its own caching mechanism
|
||||
return await getAuthorDataFromWikidata(author);
|
||||
},
|
||||
[stripHTML],
|
||||
);
|
||||
|
||||
// Get cached author image or fetch and cache it
|
||||
const getCachedAuthorImg = useCallback(
|
||||
async (author) => {
|
||||
if (localStorage.getItem('authorImg') === 'false' || author === 'Unknown') {
|
||||
return { authorimg: null, authorimglicense: null };
|
||||
}
|
||||
|
||||
return await fetchAuthorImage(author, async () => {
|
||||
const result = await getAuthorImg(author);
|
||||
// Return only if we have actual data
|
||||
return result.authorimg || result.authorimglicense ? result : null;
|
||||
});
|
||||
},
|
||||
[fetchAuthorImage, getAuthorImg],
|
||||
[getAuthorDataFromWikidata],
|
||||
);
|
||||
|
||||
// Select raw quote data without author images (non-blocking)
|
||||
@@ -133,7 +122,7 @@ export function useQuoteLoader(updateQuote) {
|
||||
quote: `"${selected.quote}"`,
|
||||
author: selected.author,
|
||||
authorlink: getAuthorLink(selected.author),
|
||||
needsAuthorImg: true,
|
||||
needsAuthorData: true,
|
||||
noQuote: false,
|
||||
};
|
||||
}
|
||||
@@ -145,7 +134,7 @@ export function useQuoteLoader(updateQuote) {
|
||||
quote: '"' + quote.quote + '"',
|
||||
author: quote.author,
|
||||
authorlink: getAuthorLink(quote.author),
|
||||
needsAuthorImg: false, // Offline quotes don't get author images
|
||||
needsAuthorData: false, // Offline quotes don't get author data
|
||||
};
|
||||
}
|
||||
|
||||
@@ -167,7 +156,7 @@ export function useQuoteLoader(updateQuote) {
|
||||
quote: '"' + quote.quote + '"',
|
||||
author: quote.author,
|
||||
authorlink: getAuthorLink(quote.author),
|
||||
needsAuthorImg: false,
|
||||
needsAuthorData: false,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -178,51 +167,54 @@ export function useQuoteLoader(updateQuote) {
|
||||
return {
|
||||
quote: `"${data.quote}"`,
|
||||
author: displayAuthor,
|
||||
realAuthor: hasAuthor ? data.author : null, // For Wikipedia lookup
|
||||
realAuthor: hasAuthor ? data.author : null, // For Wikidata lookup
|
||||
authorlink: hasAuthor ? getAuthorLink(data.author) : null,
|
||||
fallbackauthorimg: data.fallbackauthorimg,
|
||||
needsAuthorImg: hasAuthor && !data.noAuthorImg,
|
||||
needsAuthorData: hasAuthor && !data.noAuthorImg,
|
||||
};
|
||||
}, [getAuthorLink]);
|
||||
|
||||
// Fetch complete quote data including author image (for prefetching)
|
||||
// Fetch complete quote data including author data (for prefetching)
|
||||
const fetchCompleteQuote = useCallback(
|
||||
async (quoteData) => {
|
||||
if (!quoteData || quoteData.noQuote) return quoteData;
|
||||
|
||||
// If author image is needed, fetch it
|
||||
if (quoteData.needsAuthorImg) {
|
||||
// If author data is needed, fetch it
|
||||
if (quoteData.needsAuthorData) {
|
||||
const authorToLookup = quoteData.realAuthor || quoteData.author;
|
||||
try {
|
||||
const authorImgData = await getCachedAuthorImg(authorToLookup);
|
||||
const authorData = await getCachedAuthorData(authorToLookup);
|
||||
|
||||
// For quote packs, use fallback if Wikipedia fails
|
||||
if (!authorImgData.authorimg && quoteData.fallbackauthorimg) {
|
||||
// For quote packs, use fallback if Wikidata fails
|
||||
if (!authorData.authorimg && quoteData.fallbackauthorimg) {
|
||||
return {
|
||||
...quoteData,
|
||||
authorimg: quoteData.fallbackauthorimg,
|
||||
authorimglicense: null,
|
||||
authorOccupation: authorData.authorOccupation || null,
|
||||
authorlink: authorData.authorlink || quoteData.authorlink,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
...quoteData,
|
||||
...authorImgData,
|
||||
...authorData,
|
||||
};
|
||||
} catch (e) {
|
||||
console.error('Failed to fetch author image:', e);
|
||||
// Return quote data with fallback or no image
|
||||
console.error('Failed to fetch author data:', e);
|
||||
// Return quote data with fallback or no data
|
||||
return {
|
||||
...quoteData,
|
||||
authorimg: quoteData.fallbackauthorimg || null,
|
||||
authorimglicense: null,
|
||||
authorOccupation: null,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return quoteData;
|
||||
},
|
||||
[getCachedAuthorImg],
|
||||
[getCachedAuthorData],
|
||||
);
|
||||
|
||||
const doOffline = useCallback(() => {
|
||||
@@ -233,6 +225,7 @@ export function useQuoteLoader(updateQuote) {
|
||||
author: quote.author,
|
||||
authorlink: getAuthorLink(quote.author),
|
||||
authorimg: '',
|
||||
authorOccupation: null,
|
||||
});
|
||||
}, [updateQuote, getAuthorLink]);
|
||||
|
||||
@@ -243,10 +236,6 @@ export function useQuoteLoader(updateQuote) {
|
||||
if (localStorage.getItem('quoteQueue') === null) {
|
||||
localStorage.setItem('quoteQueue', JSON.stringify([]));
|
||||
}
|
||||
if (localStorage.getItem('authorImageCache') === null) {
|
||||
localStorage.setItem('authorImageCache', JSON.stringify({}));
|
||||
localStorage.setItem('authorImageCacheTimestamp', Date.now().toString());
|
||||
}
|
||||
if (localStorage.getItem('quotePrefetchEnabled') === null) {
|
||||
localStorage.setItem('quotePrefetchEnabled', 'true');
|
||||
}
|
||||
@@ -278,15 +267,16 @@ export function useQuoteLoader(updateQuote) {
|
||||
authorlink: getAuthorLink(author),
|
||||
authorimg: null, // Will be updated asynchronously
|
||||
authorimglicense: null,
|
||||
authorOccupation: null,
|
||||
});
|
||||
|
||||
// Fetch author image asynchronously (non-blocking)
|
||||
getCachedAuthorImg(author).then((authorimgdata) => {
|
||||
// Fetch author data asynchronously (non-blocking)
|
||||
getCachedAuthorData(author).then((authorData) => {
|
||||
updateQuote({
|
||||
quote,
|
||||
author,
|
||||
authorlink: getAuthorLink(author),
|
||||
...authorimgdata,
|
||||
authorlink: authorData.authorlink || getAuthorLink(author),
|
||||
...authorData,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -308,28 +298,33 @@ export function useQuoteLoader(updateQuote) {
|
||||
return updateQuote({ noQuote: true });
|
||||
}
|
||||
|
||||
// For non-queue fetch, display immediately then enhance with author image
|
||||
if (rawQuote.needsAuthorImg) {
|
||||
// For non-queue fetch, display immediately then enhance with author data
|
||||
if (rawQuote.needsAuthorData) {
|
||||
// Display quote text immediately
|
||||
updateQuote({
|
||||
...rawQuote,
|
||||
authorimg: rawQuote.fallbackauthorimg || null,
|
||||
authorimglicense: null,
|
||||
authorOccupation: null,
|
||||
});
|
||||
|
||||
// Fetch author image asynchronously
|
||||
const authorToLookup = rawQuote.realAuthor || rawQuote.author;
|
||||
getCachedAuthorImg(authorToLookup).then((authorImgData) => {
|
||||
updateQuote({
|
||||
...rawQuote,
|
||||
...(authorImgData.authorimg
|
||||
? authorImgData
|
||||
: {
|
||||
authorimg: rawQuote.fallbackauthorimg || null,
|
||||
authorimglicense: null,
|
||||
}),
|
||||
// Fetch author data asynchronously (after a brief delay to ensure snappy initial render)
|
||||
setTimeout(() => {
|
||||
const authorToLookup = rawQuote.realAuthor || rawQuote.author;
|
||||
getCachedAuthorData(authorToLookup).then((authorData) => {
|
||||
updateQuote({
|
||||
...rawQuote,
|
||||
...(authorData.authorimg
|
||||
? authorData
|
||||
: {
|
||||
authorimg: rawQuote.fallbackauthorimg || null,
|
||||
authorimglicense: null,
|
||||
authorOccupation: authorData.authorOccupation || null,
|
||||
authorlink: authorData.authorlink || rawQuote.authorlink,
|
||||
}),
|
||||
});
|
||||
});
|
||||
});
|
||||
}, 0);
|
||||
|
||||
quoteData = rawQuote; // Use for prefetch reference
|
||||
} else {
|
||||
@@ -339,7 +334,7 @@ export function useQuoteLoader(updateQuote) {
|
||||
}
|
||||
|
||||
// Step 3: Display current quote (if from queue, it's already complete)
|
||||
if (cachedQueue.length > 0 || !quoteData.needsAuthorImg) {
|
||||
if (cachedQueue.length > 0 || !quoteData.needsAuthorData) {
|
||||
updateQuote(quoteData);
|
||||
}
|
||||
|
||||
@@ -351,28 +346,31 @@ export function useQuoteLoader(updateQuote) {
|
||||
console.warn('Could not save currentQuote to localStorage:', e);
|
||||
}
|
||||
|
||||
// Step 5: Prefetch next 3 quotes asynchronously (non-blocking)
|
||||
// Step 5: Prefetch next 3 quotes asynchronously (non-blocking, deferred)
|
||||
if (queueManager.needsPrefetch() && !offline) {
|
||||
const spaceNeeded = queueManager.getSpaceNeeded();
|
||||
|
||||
Promise.all(
|
||||
Array.from({ length: spaceNeeded }, async () => {
|
||||
const rawQuote = selectQuoteData();
|
||||
if (rawQuote.noQuote) return null;
|
||||
return await fetchCompleteQuote(rawQuote);
|
||||
}),
|
||||
)
|
||||
.then((newQuotes) => {
|
||||
const validQuotes = newQuotes.filter(Boolean);
|
||||
if (validQuotes.length > 0) {
|
||||
queueManager.push(validQuotes);
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to prefetch quotes:', error);
|
||||
});
|
||||
// Defer prefetch to avoid blocking initial quote display
|
||||
setTimeout(() => {
|
||||
Promise.all(
|
||||
Array.from({ length: spaceNeeded }, async () => {
|
||||
const rawQuote = selectQuoteData();
|
||||
if (rawQuote.noQuote) return null;
|
||||
return await fetchCompleteQuote(rawQuote);
|
||||
}),
|
||||
)
|
||||
.then((newQuotes) => {
|
||||
const validQuotes = newQuotes.filter(Boolean);
|
||||
if (validQuotes.length > 0) {
|
||||
queueManager.push(validQuotes);
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to prefetch quotes:', error);
|
||||
});
|
||||
}, 100); // Small delay to ensure current quote renders first
|
||||
}
|
||||
}, [updateQuote, getAuthorLink, getCachedAuthorImg, selectQuoteData, fetchCompleteQuote]);
|
||||
}, [updateQuote, getAuthorLink, getCachedAuthorData, selectQuoteData, fetchCompleteQuote]);
|
||||
|
||||
return {
|
||||
getQuote,
|
||||
|
||||
Reference in New Issue
Block a user