@prose-reader/metadata-fetcher 1.337.0 → 1.339.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -1,2 +1,2 @@
1
- Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});let e=require("@prose-reader/archive-reader"),t=require("@prose-reader/shared");var n=e=>{let t=e.contributors??[],n=t.filter(e=>e.roles.includes(`author`)),r=(n.length>0?n:t).map(e=>e.name.trim()).filter(e=>e.length>0);return[...new Set(r)]},r=/^\d{13}$/,i=/^\d{9}[\dX]$/,a=t=>{let n=(0,e.normalizeIsbn)(t);if(n===void 0)return;if(r.test(n))return n;if(!i.test(n))return;let a=`978${n.slice(0,9)}`,o=0;for(let e=0;e<a.length;e++){let t=a.charCodeAt(e)-48;o+=e%2==0?t:t*3}return`${a}${(10-o%10)%10}`},o=/[\u0300-\u036f]/g,s=/[^\p{L}\p{N}]+/gu,c=e=>e.normalize(`NFD`).replace(o,``).toLowerCase().replace(s,` `).trim(),l=e=>{let t=new Map;for(let n=0;n<e.length-1;n++){let r=e.slice(n,n+2);t.set(r,(t.get(r)??0)+1)}return t},u=(e,t)=>{let n=c(e).replaceAll(` `,``),r=c(t).replaceAll(` `,``);if(n.length===0||r.length===0)return 0;if(n===r)return 1;if(n.length<2||r.length<2)return 0;let i=l(n),a=0;for(let[e,t]of l(r))a+=Math.min(t,i.get(e)??0);return 2*a/(n.length-1+(r.length-1))},d=/\s*[:;]\s*|\s+[-–—]\s+/,f=e=>{let[t=``,...n]=e.split(d),r=t.trim(),i=n.join(` `).trim();return r.length===0?{main:e,hasSubtitle:!1}:{main:r,hasSubtitle:i.length>0}},p=.9,m=(e,t)=>{let n=u(e,t),r=f(e),i=f(t);return r.hasSubtitle===i.hasSubtitle?n:Math.max(n,u(r.main,i.main)*p)},h=e=>c(e).split(` `).filter(e=>e.length>0).sort().join(` `),g=(e,t)=>u(h(e),h(t)),_={isbn:1,gtin:1,identifiers:.6,title:.8,contributors:.5,series:.3,published:.2,publisher:.15,languages:.15,numberOfPages:.15},v=new Set([`isbn`,`gtin`]),y=e=>{let t=e?.trim();return t!==void 0&&t.length>0?t:void 0},b=e=>e.value.trim().toLowerCase(),x=e=>e.scheme===void 0?e.value:`${e.scheme}:${e.value}`,S=(e,t)=>b(e)===b(t)?e.scheme===void 0||t.scheme===void 0||e.scheme.trim().toLowerCase()===t.scheme.trim().toLowerCase():!1,C=e=>e.trim().toLowerCase().split(`-`)[0]??``,w=(e,t)=>{let n=Math.abs(e-t);return n===0?1:n===1?.6:n<=3?.3:0},T=(e,t)=>{let n=Math.max(e,t);return n<=0?0:Math.max(0,1-Math.abs(e-t)/n*2)},E=(e,t,n)=>e.reduce((e,r)=>t.reduce((e,t)=>Math.max(e,n(r,t)),e),0),D=(e,t)=>{let r=[],i=(e,t)=>{t!==void 0&&r.push({field:e,weight:_[e],score:Math.min(1,Math.max(0,t.score)),query:t.query,candidate:t.candidate})},o=(e,t,n,r=u)=>{let a=y(t),o=y(n);a===void 0||o===void 0||i(e,{score:r(a,o),query:a,candidate:o})},s=y(e.isbn),c=y(t.isbn),l=s===void 0?void 0:a(s),d=c===void 0?void 0:a(c);s!==void 0&&c!==void 0&&l!==void 0&&d!==void 0&&i(`isbn`,{score:+(l===d),query:s,candidate:c});let f=y(e.gtin),p=y(t.gtin);f!==void 0&&p!==void 0&&i(`gtin`,{score:+(f===p),query:f,candidate:p});let h=e.identifiers??[],b=t.identifiers??[];h.length>0&&b.length>0&&i(`identifiers`,{score:+!!h.some(e=>b.some(t=>S(e,t))),query:h.map(x).join(`, `),candidate:b.map(x).join(`, `)}),o(`title`,e.title,t.title,m),o(`publisher`,e.publisher,t.publisher),o(`series`,e.belongsTo?.series?.[0]?.name,t.belongsTo?.series?.[0]?.name);let D=n(t),O=n(e);O.length>0&&D.length>0&&i(`contributors`,{score:E(O,D,g),query:O.join(`, `),candidate:D.join(`, `)});let k=e.published?.year,A=t.published?.year;k!==void 0&&A!==void 0&&i(`published`,{score:w(k,A),query:String(k),candidate:String(A)});let j=e.languages??[],M=t.languages??[];if(j.length>0&&M.length>0){let e=new Set(M.map(C));i(`languages`,{score:+!!j.some(t=>e.has(C(t))),query:j.join(`, `),candidate:M.join(`, `)})}e.numberOfPages!==void 0&&t.numberOfPages!==void 0&&i(`numberOfPages`,{score:T(e.numberOfPages,t.numberOfPages),query:String(e.numberOfPages),candidate:String(t.numberOfPages)});let N=r.filter(e=>v.has(e.field));if(N.some(e=>e.score===0))return{score:0,signals:r};if(N.some(e=>e.score===1))return{score:1,signals:r};let P=r.reduce((e,t)=>e+t.weight,0);return P===0?{score:0,signals:r}:{score:r.reduce((e,t)=>e+t.score*t.weight,0)/P,signals:r}},O=e=>{let t=Object.entries(e).filter(([,e])=>e!==void 0);return Object.fromEntries(t)},k=e=>{let t=new Set;return e.filter(e=>{let n=`${e.scheme?.trim().toLowerCase()??``}:${e.value.trim().toLowerCase()}`;return t.has(n)?!1:(t.add(n),!0)})},A=(...e)=>{let t=e.filter(e=>e!==void 0),n=e=>{for(let n of t){let t=n[e];if(t!==void 0)return t}},r=k(t.flatMap(e=>e.identifiers??[])),i=O({series:t.find(e=>e.belongsTo?.series!==void 0)?.belongsTo?.series,collection:t.find(e=>e.belongsTo?.collection!==void 0)?.belongsTo?.collection});return O({title:n(`title`),cover:n(`cover`),description:n(`description`),publisher:n(`publisher`),imprint:n(`imprint`),rights:n(`rights`),languages:n(`languages`),subjects:n(`subjects`),contributors:n(`contributors`),published:n(`published`),readingDirection:n(`readingDirection`),renditionLayout:n(`renditionLayout`),renditionFlow:n(`renditionFlow`),renditionSpread:n(`renditionSpread`),numberOfPages:n(`numberOfPages`),gtin:n(`gtin`),isbn:n(`isbn`),identifiers:r.length>0?r:void 0,belongsTo:Object.keys(i).length>0?i:void 0,properties:n(`properties`),comic:n(`comic`),apple:n(`apple`),kobo:n(`kobo`)})},j=class extends Error{status;constructor(e,t){super(t??`Provider responded with status ${e}`),this.name=`MetadataProviderResponseError`,this.status=e}},M=e=>{if(typeof e!=`object`||!e||!(`status`in e))return;let{status:t}=e;if(!(typeof t!=`number`||!Number.isInteger(t)))return t>=100&&t<=599?t:void 0},N=t.Report.namespace(`@prose-reader/metadata-fetcher`,void 0,{color:`#b06642`}),P=5,F=.5,I=1,L=({candidate:e,providerId:t,score:n,signals:r,accepted:i,includeRaw:a})=>O({providerId:t,score:n,signals:r,accepted:i,metadata:e.metadata,id:e.id,url:e.url,raw:a?e.raw:void 0}),R=async(e,t)=>{let n=`metadata`in e?e.metadata:e,r=t.limit??P,i=t.minScore??F,a=t.includeRaw===!0,o=await Promise.all(t.providers.map(async e=>{try{return{provider:e,candidates:await e.search(n,{limit:r,signal:t.signal}),failure:void 0}}catch(n){if(t.signal?.aborted===!0)throw n;return N.error(`fetchMetadata: provider "${e.id}" failed`,n),{provider:e,candidates:void 0,failure:O({id:e.id,status:M(n)})}}})),s=o.flatMap(e=>e.failure===void 0?[]:[e.failure]),c={},l=[];for(let{provider:e,candidates:t}of o){if(t===void 0)continue;let o=t.map(t=>{let{score:r,signals:o}=D(n,t.metadata);return L({candidate:t,providerId:e.id,score:r,signals:o,accepted:r>=i,includeRaw:a})}).sort((e,t)=>t.score-e.score).slice(0,r);c[e.id]!==void 0&&N.warn(`fetchMetadata: several providers share the id "${e.id}"`),c[e.id]={provider:{id:e.id,name:e.name},matches:o},l.push(...o)}let u=[...l].sort((e,t)=>t.score-e.score);return{version:I,metadata:A(...u.filter(e=>e.accepted).map(e=>e.metadata)),matches:u,sources:c,failedProviders:s}},z=e=>typeof e==`object`&&!!e&&!Array.isArray(e),B=(e,t)=>{let n=e[t];if(typeof n!=`string`)return;let r=n.trim();return r.length>0?r:void 0},V=(e,t)=>{let n=e[t];return typeof n==`number`&&Number.isFinite(n)?n:void 0},H=(e,t)=>{let n=e[t];if(!Array.isArray(n))return;let r=n.filter(e=>typeof e==`string`).map(e=>e.trim()).filter(e=>e.length>0);return r.length>0?r:void 0},U=(e,t)=>{let n=e[t];return Array.isArray(n)?n.filter(z):[]},W=e=>({key:B(e,`key`),title:B(e,`title`),subtitle:B(e,`subtitle`),author_name:H(e,`author_name`),first_publish_year:V(e,`first_publish_year`),publisher:H(e,`publisher`),language:H(e,`language`),subject:H(e,`subject`),number_of_pages_median:V(e,`number_of_pages_median`),cover_i:V(e,`cover_i`)}),G=e=>z(e)?U(e,`docs`).map(W):[],K={afr:`af`,alb:`sq`,amh:`am`,ara:`ar`,arm:`hy`,asm:`as`,aze:`az`,baq:`eu`,bel:`be`,ben:`bn`,bos:`bs`,bul:`bg`,bur:`my`,cat:`ca`,ces:`cs`,chi:`zh`,cym:`cy`,cze:`cs`,dan:`da`,deu:`de`,dut:`nl`,ell:`el`,eng:`en`,epo:`eo`,est:`et`,eus:`eu`,fao:`fo`,fas:`fa`,fin:`fi`,fra:`fr`,fre:`fr`,geo:`ka`,ger:`de`,gle:`ga`,glg:`gl`,gre:`el`,guj:`gu`,heb:`he`,hin:`hi`,hrv:`hr`,hun:`hu`,hye:`hy`,ice:`is`,ind:`id`,isl:`is`,ita:`it`,jpn:`ja`,kan:`kn`,kat:`ka`,kaz:`kk`,khm:`km`,kor:`ko`,lao:`lo`,lat:`la`,lav:`lv`,lit:`lt`,ltz:`lb`,mac:`mk`,mal:`ml`,mar:`mr`,may:`ms`,mkd:`mk`,mlt:`mt`,mon:`mn`,msa:`ms`,mya:`my`,nep:`ne`,nld:`nl`,nno:`nn`,nob:`nb`,nor:`no`,ori:`or`,pan:`pa`,per:`fa`,pol:`pl`,por:`pt`,rom:`ro`,ron:`ro`,rum:`ro`,rus:`ru`,scc:`sr`,scr:`hr`,sin:`si`,slk:`sk`,slo:`sk`,slv:`sl`,snd:`sd`,spa:`es`,sqi:`sq`,srp:`sr`,swa:`sw`,swe:`sv`,tam:`ta`,tel:`te`,tgl:`tl`,tha:`th`,tur:`tr`,ukr:`uk`,urd:`ur`,uzb:`uz`,vie:`vi`,wel:`cy`,yid:`yi`,zho:`zh`},q=new Set([`mul`,`und`,`zxx`,`sgn`]),J=e=>{let t=e.trim().toLowerCase();if(t.length!==0&&!q.has(t))return K[t]??t},Y=25,ee={key:`identifiers`,title:`title`,subtitle:`title`,author_name:`contributors`,first_publish_year:`published`,publisher:`publisher`,language:`languages`,subject:`subjects`,number_of_pages_median:`numberOfPages`,cover_i:`cover`},X=`OpenLibrary`,Z=e=>e.length>0?e:void 0,Q=(e,t)=>{let n=e.title!==void 0&&e.subtitle!==void 0?`${e.title}: ${e.subtitle}`:e.title,r=Z([...new Set((e.language??[]).map(J).filter(e=>e!==void 0))]),i=[...t.isbn===void 0?[]:[{value:t.isbn,scheme:`ISBN`}],...e.key===void 0?[]:[{value:e.key,scheme:X}]];return O({title:n,cover:e.cover_i===void 0?void 0:{uri:`${t.coversBaseUrl}/b/id/${e.cover_i}-L.jpg`,mediaType:`image/jpeg`,confidence:`derived`},publisher:e.publisher?.[0],languages:r,subjects:Z((e.subject??[]).slice(0,25)),contributors:Z((e.author_name??[]).map(e=>({name:e,roles:[`author`]}))),published:e.first_publish_year===void 0?void 0:{year:e.first_publish_year},numberOfPages:e.number_of_pages_median,isbn:t.isbn,identifiers:Z(i)})},$=`openLibrary`,te=`https://openlibrary.org`,ne=`https://covers.openlibrary.org`,re=[`key`,`title`,`subtitle`,`author_name`,`first_publish_year`,`publisher`,`language`,`subject`,`number_of_pages_median`,`cover_i`].join(`,`),ie=e=>{let t=e.title?.trim();if(t===void 0||t.length===0)return;let r=n(e)[0];return{title:t,...r===void 0?{}:{author:r}}},ae=(e={})=>{let t=e.baseUrl??te,n=e.coversBaseUrl??ne,r=async(n,r)=>{let i=new URL(`/search.json`,t);for(let[e,t]of Object.entries(n))i.searchParams.set(e,t);i.searchParams.set(`fields`,re),i.searchParams.set(`limit`,String(r.limit));let a=await(e.fetch??globalThis.fetch)(i.toString(),{signal:r.signal,headers:{Accept:`application/json`,...e.userAgent===void 0?{}:{"User-Agent":e.userAgent}}});if(!a.ok)throw new j(a.status,`Open Library search failed with status ${a.status}`);return G(await a.json())},i=(e,r)=>e.map(e=>({metadata:Q(e,{coversBaseUrl:n,isbn:r}),id:e.key,url:e.key===void 0?void 0:`${t}${e.key}`,raw:e}));return{id:$,name:`Open Library`,search:async(e,t)=>{let n=e.isbn?.trim()||void 0;if(n!==void 0){let e=await r({isbn:n},t);if(e.length>0)return i(e,n)}let a=ie(e);return a===void 0?[]:i(await r(a,t),void 0)}}},oe=e=>[e.title,e.isbn,e.gtin,e.belongsTo?.series?.[0]?.name].some(e=>e!==void 0&&e.trim().length>0)||(e.identifiers??[]).length>0||n(e).length>0;exports.METADATA_MATCH_WEIGHTS=_,exports.MetadataProviderResponseError=j,exports.OPEN_LIBRARY_IDENTIFIER_SCHEME=X,exports.OPEN_LIBRARY_MAX_SUBJECTS=Y,exports.OPEN_LIBRARY_PROVIDER_ID=$,exports.createOpenLibraryProvider=ae,exports.fetchMetadata=R,exports.hasSearchTerms=oe,exports.marcLanguageToBcp47=J,exports.mergeResolvedMetadata=A,exports.metadataAuthors=n,exports.normalizeForComparison=c,exports.openLibraryMetadataHomes=ee,exports.parseOpenLibrarySearchResponse=G,exports.personNameSimilarity=g,exports.resolveOpenLibraryDoc=Q,exports.responseErrorStatus=M,exports.scoreMetadataCandidate=D,exports.textSimilarity=u,exports.titleSimilarity=m,exports.toIsbn13=a;
1
+ Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});let e=require("@prose-reader/archive-reader"),t=require("@prose-reader/shared");var n=e=>{let t=e.contributors??[],n=t.filter(e=>e.roles.includes(`author`)),r=(n.length>0?n:t).map(e=>e.name.trim()).filter(e=>e.length>0);return[...new Set(r)]},r=/^\d{13}$/,i=/^\d{9}[\dX]$/,a=t=>{let n=(0,e.normalizeIsbn)(t);if(n===void 0)return;if(r.test(n))return n;if(!i.test(n))return;let a=`978${n.slice(0,9)}`,o=0;for(let e=0;e<a.length;e++){let t=a.charCodeAt(e)-48;o+=e%2==0?t:t*3}return`${a}${(10-o%10)%10}`},o=/[\u0300-\u036f]/g,s=/[^\p{L}\p{N}]+/gu,c=e=>e.normalize(`NFD`).replace(o,``).toLowerCase().replace(s,` `).trim(),l=e=>{let t=new Map;for(let n=0;n<e.length-1;n++){let r=e.slice(n,n+2);t.set(r,(t.get(r)??0)+1)}return t},u=(e,t)=>{let n=c(e).replaceAll(` `,``),r=c(t).replaceAll(` `,``);if(n.length===0||r.length===0)return 0;if(n===r)return 1;if(n.length<2||r.length<2)return 0;let i=l(n),a=0;for(let[e,t]of l(r))a+=Math.min(t,i.get(e)??0);return 2*a/(n.length-1+(r.length-1))},d=/\s*[:;]\s*|\s+[-–—]\s+/,f=e=>{let[t=``,...n]=e.split(d),r=t.trim(),i=n.join(` `).trim();return r.length===0?{main:e,hasSubtitle:!1}:{main:r,hasSubtitle:i.length>0}},p=.9,m=[`volume`,`part`,`book`],h=/\b(volumes?|vols?|parts?|books?)\s+([\p{L}\p{N}]+)\b/gu,g={one:1,first:1,two:2,second:2,three:3,third:3,four:4,fourth:4,five:5,fifth:5,six:6,sixth:6,seven:7,seventh:7,eight:8,eighth:8,nine:9,ninth:9,ten:10,tenth:10,eleven:11,eleventh:11,twelve:12,twelfth:12},_={i:1,v:5,x:10,l:50,c:100,d:500,m:1e3},v=e=>{if(!/^m{0,3}(cm|cd|d?c{0,3})(xc|xl|l?x{0,3})(ix|iv|v?i{0,3})$/i.test(e))return;let t=0,n=0;for(let r of[...e.toLowerCase()].reverse()){let e=_[r];if(e===void 0)return;e<n?t-=e:(t+=e,n=e)}return t>0?t:void 0},y=e=>/^\d+$/.test(e)?Number.parseInt(e,10):g[e]??v(e),b=e=>{if(e.startsWith(`vol`))return`volume`;if(e.startsWith(`part`))return`part`;if(e.startsWith(`book`))return`book`},x=e=>{let t=new Map;for(let n of c(e).matchAll(h)){let e=n[1]===void 0?void 0:b(n[1]),r=n[2]===void 0?void 0:y(n[2]);if(e===void 0||r===void 0)continue;let i=t.get(e)??new Set;i.add(r),t.set(e,i)}return t},S=(e,t)=>{let n=x(e),r=x(t);for(let e of m){let t=n.get(e),i=r.get(e);if(!(t===void 0||i===void 0)&&[...t].every(e=>!i.has(e)))return!0}return!1},C=(e,t)=>{if(S(e,t))return 0;let n=u(e,t),r=f(e),i=f(t);return r.hasSubtitle===i.hasSubtitle?n:Math.max(n,u(r.main,i.main)*p)},w=e=>c(e).split(` `).filter(e=>e.length>0).sort().join(` `),T=(e,t)=>u(w(e),w(t)),E={isbn:1,gtin:1,identifiers:.6,title:.8,contributors:.5,series:.3,published:.2,publisher:.15,languages:.15,numberOfPages:.15},D=new Set([`isbn`,`gtin`]),O=e=>{let t=e?.trim();return t!==void 0&&t.length>0?t:void 0},k=e=>e.value.trim().toLowerCase(),A=e=>e.scheme===void 0?e.value:`${e.scheme}:${e.value}`,j=(e,t)=>k(e)===k(t)?e.scheme===void 0||t.scheme===void 0||e.scheme.trim().toLowerCase()===t.scheme.trim().toLowerCase():!1,M=e=>e.trim().toLowerCase().split(`-`)[0]??``,N=(e,t)=>{let n=Math.abs(e-t);return n===0?1:n===1?.6:n<=3?.3:0},ee=(e,t)=>{let n=Math.max(e,t);return n<=0?0:Math.max(0,1-Math.abs(e-t)/n*2)},P=(e,t,n)=>e.reduce((e,r)=>t.reduce((e,t)=>Math.max(e,n(r,t)),e),0),F=(e,t)=>{let r=[],i=(e,t)=>{t!==void 0&&r.push({field:e,weight:E[e],score:Math.min(1,Math.max(0,t.score)),query:t.query,candidate:t.candidate})},o=(e,t,n,r=u)=>{let a=O(t),o=O(n);a===void 0||o===void 0||i(e,{score:r(a,o),query:a,candidate:o})},s=O(e.isbn),c=O(t.isbn),l=s===void 0?void 0:a(s),d=c===void 0?void 0:a(c);s!==void 0&&c!==void 0&&l!==void 0&&d!==void 0&&i(`isbn`,{score:+(l===d),query:s,candidate:c});let f=O(e.gtin),p=O(t.gtin);f!==void 0&&p!==void 0&&i(`gtin`,{score:+(f===p),query:f,candidate:p});let m=e.identifiers??[],h=t.identifiers??[];m.length>0&&h.length>0&&i(`identifiers`,{score:+!!m.some(e=>h.some(t=>j(e,t))),query:m.map(A).join(`, `),candidate:h.map(A).join(`, `)}),o(`title`,e.title,t.title,C),o(`publisher`,e.publisher,t.publisher),o(`series`,e.belongsTo?.series?.[0]?.name,t.belongsTo?.series?.[0]?.name);let g=n(t),_=n(e);_.length>0&&g.length>0&&i(`contributors`,{score:P(_,g,T),query:_.join(`, `),candidate:g.join(`, `)});let v=e.published?.year,y=t.published?.year;v!==void 0&&y!==void 0&&i(`published`,{score:N(v,y),query:String(v),candidate:String(y)});let b=e.languages??[],x=t.languages??[];if(b.length>0&&x.length>0){let e=new Set(x.map(M));i(`languages`,{score:+!!b.some(t=>e.has(M(t))),query:b.join(`, `),candidate:x.join(`, `)})}e.numberOfPages!==void 0&&t.numberOfPages!==void 0&&i(`numberOfPages`,{score:ee(e.numberOfPages,t.numberOfPages),query:String(e.numberOfPages),candidate:String(t.numberOfPages)});let S=r.filter(e=>D.has(e.field));if(S.some(e=>e.score===0))return{score:0,signals:r};if(S.some(e=>e.score===1))return{score:1,signals:r};let w=r.reduce((e,t)=>e+t.weight,0);return w===0?{score:0,signals:r}:{score:r.reduce((e,t)=>e+t.score*t.weight,0)/w,signals:r}},I=e=>{let t=Object.entries(e).filter(([,e])=>e!==void 0);return Object.fromEntries(t)},te=e=>{let t=new Map;for(let n of e){let e=`${n.scheme?.trim().toLowerCase()??``}:${n.value.trim().toLowerCase()}`,r=t.get(e);if(r===void 0){t.set(e,n);continue}n.unique===!0&&r.unique!==!0&&t.set(e,{...r,unique:!0})}return[...t.values()]},L=(...e)=>{let t=e.filter(e=>e!==void 0),n=e=>{for(let n of t){let t=n[e];if(t!==void 0)return t}},r=te(t.flatMap(e=>e.identifiers??[])),i=I({series:t.find(e=>e.belongsTo?.series!==void 0)?.belongsTo?.series,collection:t.find(e=>e.belongsTo?.collection!==void 0)?.belongsTo?.collection});return I({title:n(`title`),cover:n(`cover`),description:n(`description`),publisher:n(`publisher`),imprint:n(`imprint`),rights:n(`rights`),languages:n(`languages`),subjects:n(`subjects`),contributors:n(`contributors`),published:n(`published`),readingDirection:n(`readingDirection`),renditionLayout:n(`renditionLayout`),renditionFlow:n(`renditionFlow`),renditionSpread:n(`renditionSpread`),numberOfPages:n(`numberOfPages`),gtin:n(`gtin`),isbn:n(`isbn`),identifiers:r.length>0?r:void 0,belongsTo:Object.keys(i).length>0?i:void 0,properties:n(`properties`),comic:n(`comic`),apple:n(`apple`),kobo:n(`kobo`)})},R=class extends Error{status;constructor(e,t){super(t??`Provider responded with status ${e}`),this.name=`MetadataProviderResponseError`,this.status=e}},z=e=>{if(typeof e!=`object`||!e||!(`status`in e))return;let{status:t}=e;if(!(typeof t!=`number`||!Number.isInteger(t)))return t>=100&&t<=599?t:void 0},B=t.Report.namespace(`@prose-reader/metadata-fetcher`,void 0,{color:`#b06642`}),ne=5,re=.5,ie=1,ae=({candidate:e,providerId:t,score:n,signals:r,accepted:i,includeRaw:a})=>I({providerId:t,score:n,signals:r,accepted:i,metadata:e.metadata,id:e.id,url:e.url,raw:a?e.raw:void 0}),V=async(e,t)=>{let n=`metadata`in e?e.metadata:e,r=t.limit??ne,i=t.minScore??re,a=t.includeRaw===!0,o=await Promise.all(t.providers.map(async e=>{try{return{provider:e,candidates:await e.search(n,{limit:r,signal:t.signal}),failure:void 0}}catch(n){if(t.signal?.aborted===!0)throw n;return B.error(`fetchMetadata: provider "${e.id}" failed`,n),{provider:e,candidates:void 0,failure:I({id:e.id,status:z(n)})}}})),s=o.flatMap(e=>e.failure===void 0?[]:[e.failure]),c={},l=[];for(let{provider:e,candidates:t}of o){if(t===void 0)continue;let o=t.map(t=>{let{score:r,signals:o}=F(n,t.metadata);return ae({candidate:t,providerId:e.id,score:r,signals:o,accepted:r>=i,includeRaw:a})}).sort((e,t)=>t.score-e.score).slice(0,r);c[e.id]!==void 0&&B.warn(`fetchMetadata: several providers share the id "${e.id}"`),c[e.id]={provider:{id:e.id,name:e.name},matches:o},l.push(...o)}let u=[...l].sort((e,t)=>t.score-e.score);return{version:ie,metadata:L(...u.filter(e=>e.accepted).map(e=>e.metadata)),matches:u,sources:c,failedProviders:s}},H=e=>typeof e==`object`&&!!e&&!Array.isArray(e),U=(e,t)=>{let n=e[t];if(typeof n!=`string`)return;let r=n.trim();return r.length>0?r:void 0},W=(e,t)=>{let n=e[t];return typeof n==`number`&&Number.isFinite(n)?n:void 0},G=(e,t)=>{let n=e[t];if(!Array.isArray(n))return;let r=n.filter(e=>typeof e==`string`).map(e=>e.trim()).filter(e=>e.length>0);return r.length>0?r:void 0},K=(e,t)=>{let n=e[t];return Array.isArray(n)?n.filter(H):[]},q=e=>({key:U(e,`key`),title:U(e,`title`),subtitle:U(e,`subtitle`),author_name:G(e,`author_name`),first_publish_year:W(e,`first_publish_year`),publisher:G(e,`publisher`),language:G(e,`language`),subject:G(e,`subject`),number_of_pages_median:W(e,`number_of_pages_median`),cover_i:W(e,`cover_i`),id_project_gutenberg:G(e,`id_project_gutenberg`)}),J=e=>H(e)?K(e,`docs`).map(q):[],oe=new Set([`gutenberg.org`,`www.gutenberg.org`]),se=[/^\/ebooks\/(\d+)(?:[./]|$)/i,/^\/files\/(\d+)(?:\/|$)/i,/^\/cache\/epub\/(\d+)(?:\/|$)/i,/^\/(\d+)(?:\/|$)/],ce=e=>{try{let t=new URL(e.trim());if(t.protocol!==`http:`&&t.protocol!==`https:`||!oe.has(t.hostname.toLowerCase()))return;for(let e of se){let n=e.exec(t.pathname)?.[1];if(n!==void 0)return n}}catch{return}},le=e=>{for(let t of e.identifiers??[]){let e=t.scheme?.trim().toLowerCase();if(e!==void 0&&e!==`url`&&e!==`uri`)continue;let n=ce(t.value);if(n!==void 0)return{id:n,identifier:{value:t.value,...t.scheme===void 0?{}:{scheme:t.scheme}}}}},ue={afr:`af`,alb:`sq`,amh:`am`,ara:`ar`,arm:`hy`,asm:`as`,aze:`az`,baq:`eu`,bel:`be`,ben:`bn`,bos:`bs`,bul:`bg`,bur:`my`,cat:`ca`,ces:`cs`,chi:`zh`,cym:`cy`,cze:`cs`,dan:`da`,deu:`de`,dut:`nl`,ell:`el`,eng:`en`,epo:`eo`,est:`et`,eus:`eu`,fao:`fo`,fas:`fa`,fin:`fi`,fra:`fr`,fre:`fr`,geo:`ka`,ger:`de`,gle:`ga`,glg:`gl`,gre:`el`,guj:`gu`,heb:`he`,hin:`hi`,hrv:`hr`,hun:`hu`,hye:`hy`,ice:`is`,ind:`id`,isl:`is`,ita:`it`,jpn:`ja`,kan:`kn`,kat:`ka`,kaz:`kk`,khm:`km`,kor:`ko`,lao:`lo`,lat:`la`,lav:`lv`,lit:`lt`,ltz:`lb`,mac:`mk`,mal:`ml`,mar:`mr`,may:`ms`,mkd:`mk`,mlt:`mt`,mon:`mn`,msa:`ms`,mya:`my`,nep:`ne`,nld:`nl`,nno:`nn`,nob:`nb`,nor:`no`,ori:`or`,pan:`pa`,per:`fa`,pol:`pl`,por:`pt`,rom:`ro`,ron:`ro`,rum:`ro`,rus:`ru`,scc:`sr`,scr:`hr`,sin:`si`,slk:`sk`,slo:`sk`,slv:`sl`,snd:`sd`,spa:`es`,sqi:`sq`,srp:`sr`,swa:`sw`,swe:`sv`,tam:`ta`,tel:`te`,tgl:`tl`,tha:`th`,tur:`tr`,ukr:`uk`,urd:`ur`,uzb:`uz`,vie:`vi`,wel:`cy`,yid:`yi`,zho:`zh`},de=new Set([`mul`,`und`,`zxx`,`sgn`]),Y=e=>{let t=e.trim().toLowerCase();if(t.length!==0&&!de.has(t))return ue[t]??t},fe=25,pe={key:`identifiers`,title:`title`,subtitle:`title`,author_name:`contributors`,first_publish_year:`published`,publisher:`publisher`,language:`languages`,subject:`subjects`,number_of_pages_median:`numberOfPages`,cover_i:`cover`,id_project_gutenberg:`identifiers`},X=`OpenLibrary`,me=`ProjectGutenberg`,Z=e=>e.length>0?e:void 0,Q=(e,t)=>{let n=e.title!==void 0&&e.subtitle!==void 0?`${e.title}: ${e.subtitle}`:e.title,r=Z([...new Set((e.language??[]).map(Y).filter(e=>e!==void 0))]),i=[...t.isbn===void 0?[]:[{value:t.isbn,scheme:`ISBN`}],...t.matchedProjectGutenbergIdentifier===void 0?[]:[t.matchedProjectGutenbergIdentifier],...(e.id_project_gutenberg??[]).map(e=>({value:e,scheme:me})),...e.key===void 0?[]:[{value:e.key,scheme:X}]];return I({title:n,cover:e.cover_i===void 0?void 0:{uri:`${t.coversBaseUrl}/b/id/${e.cover_i}-L.jpg`,mediaType:`image/jpeg`,confidence:`derived`},publisher:e.publisher?.[0],languages:r,subjects:Z((e.subject??[]).slice(0,25)),contributors:Z((e.author_name??[]).map(e=>({name:e,roles:[`author`]}))),published:e.first_publish_year===void 0?void 0:{year:e.first_publish_year},numberOfPages:e.number_of_pages_median,isbn:t.isbn,identifiers:Z(i)})},$=`openLibrary`,he=`https://openlibrary.org`,ge=`https://covers.openlibrary.org`,_e=[`key`,`title`,`subtitle`,`author_name`,`first_publish_year`,`publisher`,`language`,`subject`,`number_of_pages_median`,`cover_i`,`id_project_gutenberg`].join(`,`),ve=e=>{let t=e.title?.trim();if(t===void 0||t.length===0)return;let r=n(e)[0];return{title:t,...r===void 0?{}:{author:r}}},ye=(e={})=>{let t=e.baseUrl??he,n=e.coversBaseUrl??ge,r=async(n,r)=>{let i=new URL(`/search.json`,t);for(let[e,t]of Object.entries(n))i.searchParams.set(e,t);i.searchParams.set(`fields`,_e),i.searchParams.set(`limit`,String(r.limit));let a=await(e.fetch??globalThis.fetch)(i.toString(),{signal:r.signal,headers:{Accept:`application/json`,...e.userAgent===void 0?{}:{"User-Agent":e.userAgent}}});if(!a.ok)throw new R(a.status,`Open Library search failed with status ${a.status}`);return J(await a.json())},i=(e,r={})=>e.map(e=>({metadata:Q(e,{coversBaseUrl:n,isbn:r.isbn,matchedProjectGutenbergIdentifier:r.confirmedProjectGutenberg?.identifier}),id:e.key,url:e.key===void 0?void 0:`${t}${e.key}`,raw:e}));return{id:$,name:`Open Library`,search:async(e,t)=>{let n=e.isbn?.trim()||void 0,a=le(e);if(n!==void 0){let e=await r({isbn:n},t);if(e.length>0)return i(e,{isbn:n})}if(a!==void 0){let e=await r({q:`id_project_gutenberg:${a.id}`},t);if(e.length>0)return i(e,{confirmedProjectGutenberg:a})}let o=ve(e);return o===void 0?[]:i(await r(o,t))}}},be=e=>[e.title,e.isbn,e.gtin,e.belongsTo?.series?.[0]?.name].some(e=>e!==void 0&&e.trim().length>0)||(e.identifiers??[]).length>0||n(e).length>0;exports.METADATA_MATCH_WEIGHTS=E,exports.MetadataProviderResponseError=R,exports.OPEN_LIBRARY_IDENTIFIER_SCHEME=X,exports.OPEN_LIBRARY_MAX_SUBJECTS=fe,exports.OPEN_LIBRARY_PROVIDER_ID=$,exports.createOpenLibraryProvider=ye,exports.fetchMetadata=V,exports.hasSearchTerms=be,exports.marcLanguageToBcp47=Y,exports.mergeResolvedMetadata=L,exports.metadataAuthors=n,exports.normalizeForComparison=c,exports.openLibraryMetadataHomes=pe,exports.parseOpenLibrarySearchResponse=J,exports.personNameSimilarity=T,exports.resolveOpenLibraryDoc=Q,exports.responseErrorStatus=z,exports.scoreMetadataCandidate=F,exports.textSimilarity=u,exports.titleSimilarity=C,exports.toIsbn13=a;
2
2
  //# sourceMappingURL=index.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.cjs","names":[],"sources":["../src/utils/metadataAuthors.ts","../src/utils/toIsbn13.ts","../src/match/similarity.ts","../src/match/scoreMetadataCandidate.ts","../src/utils/omitUndefined.ts","../src/merge/mergeResolvedMetadata.ts","../src/providers/responseError.ts","../src/report.ts","../src/fetchMetadata.ts","../src/utils/json.ts","../src/providers/openLibrary/parse.ts","../src/providers/openLibrary/marcLanguage.ts","../src/providers/openLibrary/resolve.ts","../src/providers/openLibrary/createOpenLibraryProvider.ts","../src/utils/hasSearchTerms.ts"],"sourcesContent":["import type { ResolvedMetadata } from \"@prose-reader/archive-reader\"\n\n/**\n * The names to match a publication on: those credited as `author`, else — for\n * a publication crediting nobody as one, like a comic archive listing only a\n * penciler — every contributor. Order is preserved (sources list the lead\n * creator first) and duplicates are dropped.\n */\nexport const metadataAuthors = (\n metadata: ResolvedMetadata,\n): ReadonlyArray<string> => {\n const contributors = metadata.contributors ?? []\n const authors = contributors.filter((contributor) =>\n contributor.roles.includes(\"author\"),\n )\n const names = (authors.length > 0 ? authors : contributors)\n .map((contributor) => contributor.name.trim())\n .filter((name) => name.length > 0)\n\n return [...new Set(names)]\n}\n","import { normalizeIsbn } from \"@prose-reader/archive-reader\"\n\nconst ISBN_13 = /^\\d{13}$/\nconst ISBN_10 = /^\\d{9}[\\dX]$/\n\n/**\n * Put an ISBN in its 13-digit form so the two printed on the same book compare\n * equal. A book carries an ISBN-10 in its OPF and an ISBN-13 in a catalog\n * record all the time; comparing them verbatim would read as \"different\n * books\", which — ISBN being a decisive match signal — is the most damaging\n * false negative the matcher can produce.\n *\n * Input is put through `normalizeIsbn`, the same function that produces\n * {@link ResolvedMetadata.isbn} in the first place, so the hyphenated and\n * prefixed forms a human types (`978-0-441-01359-3`, `ISBN 0-441-01359-7`)\n * are recognized rather than silently dropped.\n *\n * Returns `undefined` when nothing ISBN-shaped can be recovered (check digits\n * are not verified: a wrong one still identifies the intended book).\n */\nexport const toIsbn13 = (isbn: string): string | undefined => {\n const normalized = normalizeIsbn(isbn)\n\n if (normalized === undefined) return undefined\n if (ISBN_13.test(normalized)) return normalized\n if (!ISBN_10.test(normalized)) return undefined\n\n // ISBN-10 → ISBN-13: prefix the 978 Bookland code, drop the ISBN-10 check\n // digit, recompute the EAN-13 one (weights 1,3,1,3…).\n const core = `978${normalized.slice(0, 9)}`\n let sum = 0\n\n for (let index = 0; index < core.length; index++) {\n // charCodeAt over indexing: `noUncheckedIndexedAccess` types `core[i]` as\n // possibly undefined even though the length is known here\n const digit = core.charCodeAt(index) - 48\n\n sum += index % 2 === 0 ? digit : digit * 3\n }\n\n return `${core}${(10 - (sum % 10)) % 10}`\n}\n","const DIACRITICS = /[\\u0300-\\u036f]/g\nconst NON_ALPHANUMERIC = /[^\\p{L}\\p{N}]+/gu\n\n/** `Les Misérables` ≡ `les miserables`, `Dune: Messiah` ≡ `dune messiah`. */\nexport const normalizeForComparison = (value: string): string =>\n value\n .normalize(\"NFD\")\n .replace(DIACRITICS, \"\")\n .toLowerCase()\n .replace(NON_ALPHANUMERIC, \" \")\n .trim()\n\nconst bigramCounts = (value: string): Map<string, number> => {\n const counts = new Map<string, number>()\n\n for (let index = 0; index < value.length - 1; index++) {\n const bigram = value.slice(index, index + 2)\n\n counts.set(bigram, (counts.get(bigram) ?? 0) + 1)\n }\n\n return counts\n}\n\n/**\n * Sørensen–Dice coefficient over character bigrams, `0` to `1`.\n *\n * Bigrams rather than an edit distance: titles disagree by whole words far\n * more often than by typos. Spaces are stripped so `Spider-Man` and\n * `Spider Man` stay exact.\n */\nexport const textSimilarity = (a: string, b: string): number => {\n const left = normalizeForComparison(a).replaceAll(\" \", \"\")\n const right = normalizeForComparison(b).replaceAll(\" \", \"\")\n\n if (left.length === 0 || right.length === 0) return 0\n if (left === right) return 1\n // nothing to compare: a single character has no bigram\n if (left.length < 2 || right.length < 2) return 0\n\n const leftBigrams = bigramCounts(left)\n let shared = 0\n\n for (const [bigram, count] of bigramCounts(right)) {\n shared += Math.min(count, leftBigrams.get(bigram) ?? 0)\n }\n\n return (2 * shared) / (left.length - 1 + (right.length - 1))\n}\n\n/** Spaced dashes only, so `Spider-Man` stays one word. */\nconst SUBTITLE_SEPARATOR = /\\s*[:;]\\s*|\\s+[-–—]\\s+/\n\nconst splitTitle = (value: string): { main: string; hasSubtitle: boolean } => {\n const [head = \"\", ...rest] = value.split(SUBTITLE_SEPARATOR)\n const main = head.trim()\n const subtitle = rest.join(\" \").trim()\n\n if (main.length === 0) return { main: value, hasSubtitle: false }\n\n return { main, hasSubtitle: subtitle.length > 0 }\n}\n\n/** Below an exact match: the omitted half could have been a volume title. */\nconst SUBTITLE_REPAIR_CEILING = 0.9\n\n/**\n * {@link textSimilarity}, with `Dune` matched against `Dune: a novel`.\n *\n * Only when **one** side states a subtitle: that asymmetry is the same title\n * catalogued at a different depth. When both state one, both catalogers meant\n * it — `Dune: Book One` and `Dune: Messiah` are two books.\n */\nexport const titleSimilarity = (a: string, b: string): number => {\n const full = textSimilarity(a, b)\n const left = splitTitle(a)\n const right = splitTitle(b)\n\n if (left.hasSubtitle === right.hasSubtitle) return full\n\n return Math.max(\n full,\n textSimilarity(left.main, right.main) * SUBTITLE_REPAIR_CEILING,\n )\n}\n\nconst sortedTokens = (value: string): string =>\n normalizeForComparison(value)\n .split(\" \")\n .filter((token) => token.length > 0)\n .sort()\n .join(\" \")\n\n/** {@link textSimilarity} on sorted tokens: `Herbert, Frank` ≡ `Frank Herbert`. */\nexport const personNameSimilarity = (a: string, b: string): number =>\n textSimilarity(sortedTokens(a), sortedTokens(b))\n","import type { ResolvedMetadata } from \"@prose-reader/archive-reader\"\nimport type { MetadataMatchField, MetadataMatchSignal } from \"../types/match.ts\"\nimport type { MetadataIdentifier } from \"../types/provider.ts\"\nimport { metadataAuthors } from \"../utils/metadataAuthors.ts\"\nimport { toIsbn13 } from \"../utils/toIsbn13.ts\"\nimport {\n personNameSimilarity,\n textSimilarity,\n titleSimilarity,\n} from \"./similarity.ts\"\n\n/**\n * Intent, not statistics: identifiers *are* the book, a title is what a human\n * recognizes it by, everything else corroborates. An edition detail that\n * differs must never sink an otherwise convincing match on its own.\n */\nexport const METADATA_MATCH_WEIGHTS = {\n isbn: 1,\n gtin: 1,\n identifiers: 0.6,\n title: 0.8,\n contributors: 0.5,\n series: 0.3,\n published: 0.2,\n publisher: 0.15,\n languages: 0.15,\n numberOfPages: 0.15,\n} as const satisfies Record<MetadataMatchField, number>\n\n/**\n * Identity rather than description, so these settle the score outright in both\n * directions. Catalogs and books disagree on titles and page counts\n * constantly; they do not agree or disagree on an ISBN by accident.\n */\nconst DECISIVE_FIELDS: ReadonlySet<MetadataMatchField> = new Set([\n \"isbn\",\n \"gtin\",\n])\n\n/** Blank is absent — the vocabulary says so, a hand-built query may not. */\nconst stated = (value: string | undefined): string | undefined => {\n const trimmed = value?.trim()\n\n return trimmed !== undefined && trimmed.length > 0 ? trimmed : undefined\n}\n\nconst identifierKey = (identifier: MetadataIdentifier): string =>\n identifier.value.trim().toLowerCase()\n\nconst identifierLabel = (identifier: MetadataIdentifier): string =>\n identifier.scheme !== undefined\n ? `${identifier.scheme}:${identifier.value}`\n : identifier.value\n\n/**\n * An unannounced scheme (the OPF `dc:identifier` case) is not a contradiction;\n * a `DOI` and an `ISBN` carrying the same digits are.\n */\nconst identifiersAgree = (\n a: MetadataIdentifier,\n b: MetadataIdentifier,\n): boolean => {\n if (identifierKey(a) !== identifierKey(b)) return false\n if (a.scheme === undefined || b.scheme === undefined) return true\n\n return a.scheme.trim().toLowerCase() === b.scheme.trim().toLowerCase()\n}\n\nconst primaryLanguageSubtag = (language: string): string =>\n language.trim().toLowerCase().split(\"-\")[0] ?? \"\"\n\n/** Reprints shift the year around, so a near one still corroborates. */\nconst publishedYearScore = (\n queryYear: number,\n candidateYear: number,\n): number => {\n const distance = Math.abs(queryYear - candidateYear)\n\n if (distance === 0) return 1\n if (distance === 1) return 0.6\n if (distance <= 3) return 0.3\n\n return 0\n}\n\n/**\n * Proximity, not equality — front matter and ads move the count: identical is\n * `1`, 25% apart `0.5`, half as long `0`.\n */\nconst numberOfPagesScore = (query: number, candidate: number): number => {\n const largest = Math.max(query, candidate)\n\n if (largest <= 0) return 0\n\n return Math.max(0, 1 - (Math.abs(query - candidate) / largest) * 2)\n}\n\nconst bestSimilarity = (\n values: ReadonlyArray<string>,\n candidates: ReadonlyArray<string>,\n compare: (a: string, b: string) => number,\n): number =>\n values.reduce(\n (best, value) =>\n candidates.reduce(\n (innerBest, candidate) =>\n Math.max(innerBest, compare(value, candidate)),\n best,\n ),\n 0,\n )\n\nexport type ScoredMetadataCandidate = {\n readonly score: number\n readonly signals: ReadonlyArray<MetadataMatchSignal>\n}\n\n/**\n * How well a candidate agrees with the query, field by field, and why. Every\n * provider is scored by these rules, so their scores stay comparable:\n *\n * - **Only comparable fields count** — a field is compared when both sides\n * state it, so what the book doesn't know can neither raise nor lower a\n * candidate, and nothing in common scores `0`.\n * - **The aggregate is a weighted average** ({@link METADATA_MATCH_WEIGHTS}),\n * putting a rich query and a sparse one on the same scale.\n * - **A stated identifier settles it, both ways** — agreeing pins the score to\n * `1`, contradicting to `0`. The latter is what sinks the wrong edition that\n * agrees on everything a weighted average can see.\n */\nexport const scoreMetadataCandidate = (\n query: ResolvedMetadata,\n candidate: ResolvedMetadata,\n): ScoredMetadataCandidate => {\n const signals: MetadataMatchSignal[] = []\n\n const addSignal = (\n field: MetadataMatchField,\n values:\n | {\n score: number\n query: string\n candidate: string\n }\n | undefined,\n ) => {\n if (values === undefined) return\n\n signals.push({\n field,\n weight: METADATA_MATCH_WEIGHTS[field],\n score: Math.min(1, Math.max(0, values.score)),\n query: values.query,\n candidate: values.candidate,\n })\n }\n\n const compareStrings = (\n field: MetadataMatchField,\n rawQueryValue: string | undefined,\n rawCandidateValue: string | undefined,\n compare: (a: string, b: string) => number = textSimilarity,\n ) => {\n const queryValue = stated(rawQueryValue)\n const candidateValue = stated(rawCandidateValue)\n\n if (queryValue === undefined || candidateValue === undefined) return\n\n addSignal(field, {\n score: compare(queryValue, candidateValue),\n query: queryValue,\n candidate: candidateValue,\n })\n }\n\n const queryIsbn = stated(query.isbn)\n const candidateIsbn = stated(candidate.isbn)\n const queryIsbn13 = queryIsbn !== undefined ? toIsbn13(queryIsbn) : undefined\n const candidateIsbn13 =\n candidateIsbn !== undefined ? toIsbn13(candidateIsbn) : undefined\n\n if (\n queryIsbn !== undefined &&\n candidateIsbn !== undefined &&\n queryIsbn13 !== undefined &&\n candidateIsbn13 !== undefined\n ) {\n addSignal(\"isbn\", {\n score: queryIsbn13 === candidateIsbn13 ? 1 : 0,\n query: queryIsbn,\n candidate: candidateIsbn,\n })\n }\n\n const queryGtin = stated(query.gtin)\n const candidateGtin = stated(candidate.gtin)\n\n if (queryGtin !== undefined && candidateGtin !== undefined) {\n addSignal(\"gtin\", {\n score: queryGtin === candidateGtin ? 1 : 0,\n query: queryGtin,\n candidate: candidateGtin,\n })\n }\n\n const queryIdentifiers = query.identifiers ?? []\n const candidateIdentifiers = candidate.identifiers ?? []\n\n if (queryIdentifiers.length > 0 && candidateIdentifiers.length > 0) {\n const shared = queryIdentifiers.some((identifier) =>\n candidateIdentifiers.some((other) => identifiersAgree(identifier, other)),\n )\n\n addSignal(\"identifiers\", {\n score: shared ? 1 : 0,\n query: queryIdentifiers.map(identifierLabel).join(\", \"),\n candidate: candidateIdentifiers.map(identifierLabel).join(\", \"),\n })\n }\n\n compareStrings(\"title\", query.title, candidate.title, titleSimilarity)\n compareStrings(\"publisher\", query.publisher, candidate.publisher)\n compareStrings(\n \"series\",\n query.belongsTo?.series?.[0]?.name,\n candidate.belongsTo?.series?.[0]?.name,\n )\n\n const candidateAuthors = metadataAuthors(candidate)\n const queryAuthors = metadataAuthors(query)\n\n if (queryAuthors.length > 0 && candidateAuthors.length > 0) {\n addSignal(\"contributors\", {\n score: bestSimilarity(\n queryAuthors,\n candidateAuthors,\n personNameSimilarity,\n ),\n query: queryAuthors.join(\", \"),\n candidate: candidateAuthors.join(\", \"),\n })\n }\n\n const queryYear = query.published?.year\n const candidateYear = candidate.published?.year\n\n if (queryYear !== undefined && candidateYear !== undefined) {\n addSignal(\"published\", {\n score: publishedYearScore(queryYear, candidateYear),\n query: String(queryYear),\n candidate: String(candidateYear),\n })\n }\n\n const queryLanguages = query.languages ?? []\n const candidateLanguages = candidate.languages ?? []\n\n if (queryLanguages.length > 0 && candidateLanguages.length > 0) {\n const candidateSubtags = new Set(\n candidateLanguages.map(primaryLanguageSubtag),\n )\n\n addSignal(\"languages\", {\n score: queryLanguages.some((language) =>\n candidateSubtags.has(primaryLanguageSubtag(language)),\n )\n ? 1\n : 0,\n query: queryLanguages.join(\", \"),\n candidate: candidateLanguages.join(\", \"),\n })\n }\n\n if (\n query.numberOfPages !== undefined &&\n candidate.numberOfPages !== undefined\n ) {\n addSignal(\"numberOfPages\", {\n score: numberOfPagesScore(query.numberOfPages, candidate.numberOfPages),\n query: String(query.numberOfPages),\n candidate: String(candidate.numberOfPages),\n })\n }\n\n const decisive = signals.filter((signal) => DECISIVE_FIELDS.has(signal.field))\n\n // Contradiction wins over agreement: refusing a publication whose stated\n // identity disagrees is the recoverable mistake. Without this the average is\n // merciful to exactly the wrong candidate — a different edition agreeing on\n // title and author scores (0.8 + 0.5) / 2.3 ≈ 0.57, accepted.\n if (decisive.some((signal) => signal.score === 0))\n return { score: 0, signals }\n if (decisive.some((signal) => signal.score === 1))\n return { score: 1, signals }\n\n const totalWeight = signals.reduce(\n (total, signal) => total + signal.weight,\n 0,\n )\n\n if (totalWeight === 0) return { score: 0, signals }\n\n const weighted = signals.reduce(\n (total, signal) => total + signal.score * signal.weight,\n 0,\n )\n\n return { score: weighted / totalWeight, signals }\n}\n","/**\n * Returns a copy of `obj` without the keys whose value is `undefined`, so\n * results stay sparse (an absent key and an `undefined` value are the same\n * statement: \"nothing was found for this field\").\n */\nexport const omitUndefined = <T extends object>(obj: T): T => {\n const entries = Object.entries(obj).filter(([, value]) => value !== undefined)\n\n // `as T`: Object.entries/fromEntries erase the key–value pairing; filtering\n // only drops `undefined` values, which `T`'s optional fields already allow,\n // so the runtime shape is a subset of `T` with no way to express it inline.\n return Object.fromEntries(entries) as T\n}\n","import type { ResolvedMetadata } from \"@prose-reader/archive-reader\"\nimport type { MetadataIdentifier } from \"../types/provider.ts\"\nimport { omitUndefined } from \"../utils/omitUndefined.ts\"\n\nconst dedupeIdentifiers = (\n identifiers: ReadonlyArray<MetadataIdentifier>,\n): ReadonlyArray<MetadataIdentifier> => {\n const seen = new Set<string>()\n\n return identifiers.filter((identifier) => {\n const key = `${identifier.scheme?.trim().toLowerCase() ?? \"\"}:${identifier.value.trim().toLowerCase()}`\n\n if (seen.has(key)) return false\n\n seen.add(key)\n\n return true\n })\n}\n\n/**\n * Merges several {@link ResolvedMetadata} into one, **first defined wins**.\n * Precedence is the caller's — pass the sources in the order you trust them.\n *\n * ```ts\n * // the book over the catalogs, catalogs filling the gaps\n * const metadata = mergeResolvedMetadata(resolved.metadata, fetched.metadata)\n * ```\n *\n * Field-wise rather than object-wise, so a source knowing only a cover\n * contributes it without hiding another's title. Two fields are additive\n * instead: `identifiers` concatenate (deduped on scheme + value, mirroring\n * `resolveMetadata`), and `belongsTo` merges `series` and `collection`\n * independently.\n *\n * Everything else takes the first stated value whole. Unioning keyword lists\n * across catalogs is a judgement call for the consumer, not for a merge that\n * has to stay predictable.\n */\nexport const mergeResolvedMetadata = (\n ...entries: ReadonlyArray<ResolvedMetadata | undefined>\n): ResolvedMetadata => {\n const defined = entries.filter(\n (entry): entry is ResolvedMetadata => entry !== undefined,\n )\n\n const first = <K extends keyof ResolvedMetadata>(\n key: K,\n ): ResolvedMetadata[K] => {\n for (const entry of defined) {\n const value = entry[key]\n\n if (value !== undefined) return value\n }\n\n return undefined\n }\n\n const identifiers = dedupeIdentifiers(\n defined.flatMap((entry) => entry.identifiers ?? []),\n )\n\n const belongsTo = omitUndefined({\n series: defined.find((entry) => entry.belongsTo?.series !== undefined)\n ?.belongsTo?.series,\n collection: defined.find(\n (entry) => entry.belongsTo?.collection !== undefined,\n )?.belongsTo?.collection,\n })\n\n // `Required` makes every key mandatory in this literal (values still accept\n // `undefined`), so a field added to the vocabulary is a compile error here\n // until its merge rule is chosen, rather than silently dropped.\n const merged: {\n [K in keyof Required<ResolvedMetadata>]: ResolvedMetadata[K]\n } = {\n title: first(\"title\"),\n cover: first(\"cover\"),\n description: first(\"description\"),\n publisher: first(\"publisher\"),\n imprint: first(\"imprint\"),\n rights: first(\"rights\"),\n languages: first(\"languages\"),\n subjects: first(\"subjects\"),\n contributors: first(\"contributors\"),\n published: first(\"published\"),\n readingDirection: first(\"readingDirection\"),\n renditionLayout: first(\"renditionLayout\"),\n renditionFlow: first(\"renditionFlow\"),\n renditionSpread: first(\"renditionSpread\"),\n numberOfPages: first(\"numberOfPages\"),\n gtin: first(\"gtin\"),\n isbn: first(\"isbn\"),\n identifiers: identifiers.length > 0 ? identifiers : undefined,\n belongsTo: Object.keys(belongsTo).length > 0 ? belongsTo : undefined,\n properties: first(\"properties\"),\n comic: first(\"comic\"),\n apple: first(\"apple\"),\n kobo: first(\"kobo\"),\n }\n\n return omitUndefined(merged)\n}\n","/**\n * Thrown by a provider when a catalog answered with a failing status. Carrying\n * it is what lets `fetchMetadata` report *why* a provider dropped out: `429`\n * is \"ask again later\", `503` is \"the catalog is down\", and a consumer decides\n * differently for each.\n *\n * ```ts\n * if (!response.ok) {\n * throw new MetadataProviderResponseError(response.status, \"My Catalog search failed\")\n * }\n * ```\n */\nexport class MetadataProviderResponseError extends Error {\n readonly status: number\n\n constructor(status: number, message?: string) {\n super(message ?? `Provider responded with status ${status}`)\n\n this.name = \"MetadataProviderResponseError\"\n this.status = status\n }\n}\n\n/**\n * The HTTP status behind a thrown error, when there is one.\n *\n * Structural rather than `instanceof`, so a provider built on another HTTP\n * client works unadapted. No status means it was never a response — network,\n * parse, bug — which is itself the answer.\n */\nexport const responseErrorStatus = (error: unknown): number | undefined => {\n if (typeof error !== \"object\" || error === null) return undefined\n if (!(\"status\" in error)) return undefined\n\n const { status } = error\n\n if (typeof status !== \"number\" || !Number.isInteger(status)) return undefined\n\n // an unrelated `status` field means something else entirely\n return status >= 100 && status <= 599 ? status : undefined\n}\n","import { Report as SharedReport } from \"@prose-reader/shared\"\n\n// Spelled out rather than read from package.json: node runs this source\n// directly in development, where a JSON import needs an import attribute.\nconst NAME = \"@prose-reader/metadata-fetcher\"\n\n// No explicit `enabled`: inherit the shared root, which auto-configures from\n// the `globalThis.__PROSE_READER_DEBUG` flag.\nexport const Report = SharedReport.namespace(NAME, undefined, {\n color: \"#b06642\",\n})\n","import type { ResolvedMetadata } from \"@prose-reader/archive-reader\"\nimport { scoreMetadataCandidate } from \"./match/scoreMetadataCandidate.ts\"\nimport { mergeResolvedMetadata } from \"./merge/mergeResolvedMetadata.ts\"\nimport { responseErrorStatus } from \"./providers/responseError.ts\"\nimport { Report } from \"./report.ts\"\nimport type {\n FailedMetadataProvider,\n FetchedMetadata,\n FetchedMetadataSource,\n} from \"./types/fetchedMetadata.ts\"\nimport type { MetadataMatch } from \"./types/match.ts\"\nimport type { MetadataCandidate, MetadataProvider } from \"./types/provider.ts\"\nimport { omitUndefined } from \"./utils/omitUndefined.ts\"\n\n/**\n * A {@link ResolvedMetadata}, or anything carrying one — the `ResolvedArchive`\n * shape, so the output of `resolveArchive` goes straight in. Hand-built terms\n * (`{ title: \"Dune\" }`) work too, when there is no container to resolve.\n */\nexport type FetchMetadataInput =\n | ResolvedMetadata\n | { readonly metadata: ResolvedMetadata }\n\nexport type FetchMetadataOptions = {\n /**\n * The catalogs to ask, queried concurrently. Their order is the tie-break\n * precedence: equally-scored matches rank in the order declared here, and\n * that ranking drives the merged `metadata`.\n */\n readonly providers: ReadonlyArray<MetadataProvider>\n /**\n * Hard cap on the matches kept **per provider**, best first; also the\n * page-size hint each provider receives. Defaults to `5`.\n */\n readonly limit?: number\n /**\n * Score a match must reach to be `accepted`, and so contribute to the merged\n * `metadata`. Defaults to `0.5`. Below it, matches are still returned and\n * ranked, for a consumer to offer as \"did you mean?\".\n */\n readonly minScore?: number\n /**\n * Keep each provider's own record on `match.raw`. Defaults to `false`: it is\n * provenance most consumers don't need, and can dwarf what they persist.\n */\n readonly includeRaw?: boolean\n readonly signal?: AbortSignal\n}\n\nconst DEFAULT_LIMIT = 5\nconst DEFAULT_MIN_SCORE = 0.5\nconst FETCHED_METADATA_VERSION = 1\n\nconst toMatch = ({\n candidate,\n providerId,\n score,\n signals,\n accepted,\n includeRaw,\n}: {\n candidate: MetadataCandidate\n providerId: string\n score: number\n signals: MetadataMatch[\"signals\"]\n accepted: boolean\n includeRaw: boolean\n}): MetadataMatch =>\n omitUndefined({\n providerId,\n score,\n signals,\n accepted,\n metadata: candidate.metadata,\n id: candidate.id,\n url: candidate.url,\n raw: includeRaw ? candidate.raw : undefined,\n })\n\n/**\n * Asks every provider what it knows about a book, scores the answers against\n * what you gave it, and merges the convincing ones into a single\n * {@link ResolvedMetadata} — the vocabulary `resolveArchive` produces, so\n * remote and embedded metadata stay interchangeable.\n *\n * ```ts\n * import { resolveArchive } from \"@prose-reader/archive-reader\"\n * import {\n * createOpenLibraryProvider,\n * fetchMetadata,\n * mergeResolvedMetadata,\n * } from \"@prose-reader/metadata-fetcher\"\n *\n * const resolved = await resolveArchive(archive)\n * const fetched = await fetchMetadata(resolved, {\n * providers: [createOpenLibraryProvider()],\n * })\n *\n * // what the catalogs found, and why we believe it\n * fetched.metadata.cover?.uri\n * fetched.matches[0]?.signals // [{ field: \"isbn\", score: 1, … }, …]\n *\n * // the book wins, the catalogs fill its gaps\n * const metadata = mergeResolvedMetadata(resolved.metadata, fetched.metadata)\n * ```\n *\n * Error policy mirrors `resolveArchive`: a provider that throws never fails\n * the fetch — it is logged through the debug `Report` and listed in\n * `failedProviders`. An abort is the exception: cancelling through `signal`\n * rejects, since the caller asked for it.\n */\nexport const fetchMetadata = async (\n input: FetchMetadataInput,\n options: FetchMetadataOptions,\n): Promise<FetchedMetadata> => {\n const metadata = \"metadata\" in input ? input.metadata : input\n const limit = options.limit ?? DEFAULT_LIMIT\n const minScore = options.minScore ?? DEFAULT_MIN_SCORE\n const includeRaw = options.includeRaw === true\n\n const results = await Promise.all(\n options.providers.map(async (provider) => {\n try {\n const candidates = await provider.search(metadata, {\n limit,\n signal: options.signal,\n })\n\n return { provider, candidates, failure: undefined }\n } catch (error) {\n // the caller pulled the plug: their cancellation, their rejection\n if (options.signal?.aborted === true) throw error\n\n // catalogs in the wild are flaky: a failing provider never fails the\n // fetch, it just doesn't contribute. The status, when the catalog\n // answered with one, is what tells a rate limit from an outage.\n Report.error(`fetchMetadata: provider \"${provider.id}\" failed`, error)\n\n return {\n provider,\n candidates: undefined,\n failure: omitUndefined({\n id: provider.id,\n status: responseErrorStatus(error),\n }),\n }\n }\n }),\n )\n\n const failedProviders: FailedMetadataProvider[] = results.flatMap((result) =>\n result.failure !== undefined ? [result.failure] : [],\n )\n\n const sources: Record<string, FetchedMetadataSource> = {}\n const matches: MetadataMatch[] = []\n\n for (const { provider, candidates } of results) {\n if (candidates === undefined) continue\n\n const providerMatches = candidates\n .map((candidate) => {\n const { score, signals } = scoreMetadataCandidate(\n metadata,\n candidate.metadata,\n )\n\n return toMatch({\n candidate,\n providerId: provider.id,\n score,\n signals,\n accepted: score >= minScore,\n includeRaw,\n })\n })\n // `sort` is stable, so equally-scored candidates keep the order the\n // provider ranked them in\n .sort((a, b) => b.score - a.score)\n .slice(0, limit)\n\n // ids key `sources`, so a duplicate would silently drop a provider's\n // detail — the kind of thing that costs an hour to notice\n if (sources[provider.id] !== undefined) {\n Report.warn(\n `fetchMetadata: several providers share the id \"${provider.id}\"`,\n )\n }\n\n sources[provider.id] = {\n provider: { id: provider.id, name: provider.name },\n matches: providerMatches,\n }\n\n matches.push(...providerMatches)\n }\n\n // again stable: providers were walked in declaration order, so that order\n // breaks ties between equally convincing catalogs\n const rankedMatches = [...matches].sort((a, b) => b.score - a.score)\n\n return {\n version: FETCHED_METADATA_VERSION,\n metadata: mergeResolvedMetadata(\n ...rankedMatches\n .filter((match) => match.accepted)\n .map((match) => match.metadata),\n ),\n matches: rankedMatches,\n sources,\n failedProviders,\n }\n}\n","/**\n * Readers for `JSON.parse` output. A catalog's payload is third-party data, so\n * it enters as `unknown` and every field goes through a guard: a renamed or\n * nulled field reads as absent instead of crashing the fetch, or producing a\n * candidate with `undefined` where a string was promised.\n */\nexport const isJsonRecord = (\n value: unknown,\n): value is Record<string, unknown> =>\n typeof value === \"object\" && value !== null && !Array.isArray(value)\n\nexport const readString = (\n record: Record<string, unknown>,\n key: string,\n): string | undefined => {\n const value = record[key]\n\n if (typeof value !== \"string\") return undefined\n\n const trimmed = value.trim()\n\n return trimmed.length > 0 ? trimmed : undefined\n}\n\nexport const readNumber = (\n record: Record<string, unknown>,\n key: string,\n): number | undefined => {\n const value = record[key]\n\n return typeof value === \"number\" && Number.isFinite(value) ? value : undefined\n}\n\nexport const readStringArray = (\n record: Record<string, unknown>,\n key: string,\n): ReadonlyArray<string> | undefined => {\n const value = record[key]\n\n if (!Array.isArray(value)) return undefined\n\n const strings = value\n .filter((entry): entry is string => typeof entry === \"string\")\n .map((entry) => entry.trim())\n .filter((entry) => entry.length > 0)\n\n return strings.length > 0 ? strings : undefined\n}\n\nexport const readRecordArray = (\n record: Record<string, unknown>,\n key: string,\n): ReadonlyArray<Record<string, unknown>> => {\n const value = record[key]\n\n if (!Array.isArray(value)) return []\n\n return value.filter(isJsonRecord)\n}\n","import {\n isJsonRecord,\n readNumber,\n readRecordArray,\n readString,\n readStringArray,\n} from \"../../utils/json.ts\"\n\n/**\n * One entry of an Open Library `search.json` response, field names mirroring\n * the API (same rule as the archive parsers: a parsed source looks like its\n * source document). Every field is optional — the API omits what it doesn't\n * have, and the `fields` parameter decides what it may return at all.\n *\n * @see https://openlibrary.org/dev/docs/api/search\n */\nexport type OpenLibraryDoc = {\n /** Work key, e.g. `/works/OL45883W`. */\n readonly key?: string\n readonly title?: string\n readonly subtitle?: string\n readonly author_name?: ReadonlyArray<string>\n readonly first_publish_year?: number\n readonly publisher?: ReadonlyArray<string>\n /** MARC 21 language codes (`eng`, `fre`), not BCP 47. */\n readonly language?: ReadonlyArray<string>\n readonly subject?: ReadonlyArray<string>\n readonly number_of_pages_median?: number\n /** Cover id, addressing `covers.openlibrary.org`. */\n readonly cover_i?: number\n}\n\nconst parseDoc = (record: Record<string, unknown>): OpenLibraryDoc => ({\n key: readString(record, \"key\"),\n title: readString(record, \"title\"),\n subtitle: readString(record, \"subtitle\"),\n author_name: readStringArray(record, \"author_name\"),\n first_publish_year: readNumber(record, \"first_publish_year\"),\n publisher: readStringArray(record, \"publisher\"),\n language: readStringArray(record, \"language\"),\n subject: readStringArray(record, \"subject\"),\n number_of_pages_median: readNumber(record, \"number_of_pages_median\"),\n cover_i: readNumber(record, \"cover_i\"),\n})\n\n/**\n * Reads the `docs` of a `search.json` payload. A payload that isn't a record,\n * or carries no `docs` array, yields an empty list: an unexpected response\n * shape is \"this catalog has nothing for us\", not a crash.\n */\nexport const parseOpenLibrarySearchResponse = (\n payload: unknown,\n): ReadonlyArray<OpenLibraryDoc> => {\n if (!isJsonRecord(payload)) return []\n\n return readRecordArray(payload, \"docs\").map(parseDoc)\n}\n","/**\n * MARC 21 language codes → BCP 47 primary subtags, for the codes a general\n * book catalog actually returns. Open Library reports `language` as MARC\n * (`eng`, `fre`), while {@link ResolvedMetadata.languages} is BCP 47 by\n * contract, so the mapping is part of normalizing — not a nicety.\n *\n * Both MARC variants are listed where they differ (bibliographic `fre` and\n * terminology `fra`), since records in the wild carry either.\n */\nconst MARC_TO_BCP47: Readonly<Record<string, string>> = {\n afr: \"af\",\n alb: \"sq\",\n amh: \"am\",\n ara: \"ar\",\n arm: \"hy\",\n asm: \"as\",\n aze: \"az\",\n baq: \"eu\",\n bel: \"be\",\n ben: \"bn\",\n bos: \"bs\",\n bul: \"bg\",\n bur: \"my\",\n cat: \"ca\",\n ces: \"cs\",\n chi: \"zh\",\n cym: \"cy\",\n cze: \"cs\",\n dan: \"da\",\n deu: \"de\",\n dut: \"nl\",\n ell: \"el\",\n eng: \"en\",\n epo: \"eo\",\n est: \"et\",\n eus: \"eu\",\n fao: \"fo\",\n fas: \"fa\",\n fin: \"fi\",\n fra: \"fr\",\n fre: \"fr\",\n geo: \"ka\",\n ger: \"de\",\n gle: \"ga\",\n glg: \"gl\",\n gre: \"el\",\n guj: \"gu\",\n heb: \"he\",\n hin: \"hi\",\n hrv: \"hr\",\n hun: \"hu\",\n hye: \"hy\",\n ice: \"is\",\n ind: \"id\",\n isl: \"is\",\n ita: \"it\",\n jpn: \"ja\",\n kan: \"kn\",\n kat: \"ka\",\n kaz: \"kk\",\n khm: \"km\",\n kor: \"ko\",\n lao: \"lo\",\n lat: \"la\",\n lav: \"lv\",\n lit: \"lt\",\n ltz: \"lb\",\n mac: \"mk\",\n mal: \"ml\",\n mar: \"mr\",\n may: \"ms\",\n mkd: \"mk\",\n mlt: \"mt\",\n mon: \"mn\",\n msa: \"ms\",\n mya: \"my\",\n nep: \"ne\",\n nld: \"nl\",\n nno: \"nn\",\n nob: \"nb\",\n nor: \"no\",\n ori: \"or\",\n pan: \"pa\",\n per: \"fa\",\n pol: \"pl\",\n por: \"pt\",\n rom: \"ro\",\n ron: \"ro\",\n rum: \"ro\",\n rus: \"ru\",\n scc: \"sr\",\n scr: \"hr\",\n sin: \"si\",\n slk: \"sk\",\n slo: \"sk\",\n slv: \"sl\",\n snd: \"sd\",\n spa: \"es\",\n sqi: \"sq\",\n srp: \"sr\",\n swa: \"sw\",\n swe: \"sv\",\n tam: \"ta\",\n tel: \"te\",\n tgl: \"tl\",\n tha: \"th\",\n tur: \"tr\",\n ukr: \"uk\",\n urd: \"ur\",\n uzb: \"uz\",\n vie: \"vi\",\n wel: \"cy\",\n yid: \"yi\",\n zho: \"zh\",\n}\n\n/**\n * MARC placeholders that say \"no single language\": keeping them would put a\n * non-language in a BCP 47 field, so they resolve to nothing.\n */\nconst NON_LANGUAGE_CODES = new Set([\"mul\", \"und\", \"zxx\", \"sgn\"])\n\n/**\n * Normalizes one MARC language code to its BCP 47 primary subtag. Unknown\n * codes (the long tail of MARC) pass through lowercased rather than being\n * dropped — a language we can't name is still a language the record stated.\n */\nexport const marcLanguageToBcp47 = (code: string): string | undefined => {\n const normalized = code.trim().toLowerCase()\n\n if (normalized.length === 0) return undefined\n if (NON_LANGUAGE_CODES.has(normalized)) return undefined\n\n return MARC_TO_BCP47[normalized] ?? normalized\n}\n","import type { ResolvedMetadata } from \"@prose-reader/archive-reader\"\nimport { omitUndefined } from \"../../utils/omitUndefined.ts\"\nimport { marcLanguageToBcp47 } from \"./marcLanguage.ts\"\nimport type { OpenLibraryDoc } from \"./parse.ts\"\n\n/**\n * A popular work carries hundreds of subject headings, most of them long-tail\n * noise (\"Accessible book\", \"Protected DAISY\"), which would dwarf the entity a\n * consumer persists. The API puts the meaningful ones first.\n */\nexport const OPEN_LIBRARY_MAX_SUBJECTS = 25\n\n/**\n * Where every {@link OpenLibraryDoc} field lands in {@link ResolvedMetadata},\n * compile-enforced: adding a field to the parser without declaring its home is\n * a type error. The archive parsers' losslessness contract, and the mapping\n * documentation.\n */\nexport const openLibraryMetadataHomes = {\n key: \"identifiers\",\n title: \"title\",\n subtitle: \"title\",\n author_name: \"contributors\",\n first_publish_year: \"published\",\n publisher: \"publisher\",\n language: \"languages\",\n subject: \"subjects\",\n number_of_pages_median: \"numberOfPages\",\n cover_i: \"cover\",\n} as const satisfies Record<\n keyof OpenLibraryDoc,\n keyof ResolvedMetadata | \"identifiers\"\n>\n\nexport const OPEN_LIBRARY_IDENTIFIER_SCHEME = \"OpenLibrary\"\n\nconst emptyToUndefined = <T>(\n values: ReadonlyArray<T>,\n): ReadonlyArray<T> | undefined => (values.length > 0 ? values : undefined)\n\n/**\n * Normalizes one search hit into the cross-format vocabulary. Two choices\n * worth stating:\n *\n * - **`title` folds in `subtitle`** (`Dune: Messiah`): the vocabulary has one\n * title field, and an OPF `dc:title` normally carries the subtitle too, so\n * comparing a bare title against a full one would cost match score.\n * - **`isbn` is set only for an ISBN lookup**, to the queried one — the API\n * answered \"this work has that ISBN\", a fact about the record. A hit\n * describes a *work*, whose editions each have their own ISBN, so picking\n * one out of a title search would be fabrication.\n */\nexport const resolveOpenLibraryDoc = (\n doc: OpenLibraryDoc,\n options: {\n readonly coversBaseUrl: string\n /** The ISBN this record was looked up by, when it was. */\n readonly isbn?: string\n },\n): ResolvedMetadata => {\n const title =\n doc.title !== undefined && doc.subtitle !== undefined\n ? `${doc.title}: ${doc.subtitle}`\n : doc.title\n\n const languages = emptyToUndefined([\n ...new Set(\n (doc.language ?? [])\n .map(marcLanguageToBcp47)\n .filter((language): language is string => language !== undefined),\n ),\n ])\n\n const identifiers = [\n ...(options.isbn !== undefined\n ? [{ value: options.isbn, scheme: \"ISBN\" }]\n : []),\n ...(doc.key !== undefined\n ? [{ value: doc.key, scheme: OPEN_LIBRARY_IDENTIFIER_SCHEME }]\n : []),\n ]\n\n return omitUndefined({\n title,\n cover:\n doc.cover_i !== undefined\n ? {\n // absolute, not container-relative: a catalog addresses its\n // covers in its own space, with nothing to rebase onto\n uri: `${options.coversBaseUrl}/b/id/${doc.cover_i}-L.jpg`,\n mediaType: \"image/jpeg\",\n confidence: \"derived\" as const,\n }\n : undefined,\n publisher: doc.publisher?.[0],\n languages,\n subjects: emptyToUndefined(\n (doc.subject ?? []).slice(0, OPEN_LIBRARY_MAX_SUBJECTS),\n ),\n contributors: emptyToUndefined(\n (doc.author_name ?? []).map((name) => ({\n name,\n roles: [\"author\" as const],\n })),\n ),\n published:\n doc.first_publish_year !== undefined\n ? { year: doc.first_publish_year }\n : undefined,\n numberOfPages: doc.number_of_pages_median,\n isbn: options.isbn,\n identifiers: emptyToUndefined(identifiers),\n })\n}\n","import type { ResolvedMetadata } from \"@prose-reader/archive-reader\"\nimport type {\n MetadataCandidate,\n MetadataProvider,\n MetadataProviderContext,\n} from \"../../types/provider.ts\"\nimport { metadataAuthors } from \"../../utils/metadataAuthors.ts\"\nimport { MetadataProviderResponseError } from \"../responseError.ts\"\nimport { type OpenLibraryDoc, parseOpenLibrarySearchResponse } from \"./parse.ts\"\nimport { resolveOpenLibraryDoc } from \"./resolve.ts\"\n\nexport const OPEN_LIBRARY_PROVIDER_ID = \"openLibrary\"\n\nconst DEFAULT_BASE_URL = \"https://openlibrary.org\"\nconst DEFAULT_COVERS_BASE_URL = \"https://covers.openlibrary.org\"\n\n/**\n * Only what {@link resolveOpenLibraryDoc} maps: `search.json` returns every\n * edition key and IA identifier by default, so `fields` is the difference\n * between a few hundred bytes per hit and a few hundred kilobytes.\n */\nconst SEARCH_FIELDS = [\n \"key\",\n \"title\",\n \"subtitle\",\n \"author_name\",\n \"first_publish_year\",\n \"publisher\",\n \"language\",\n \"subject\",\n \"number_of_pages_median\",\n \"cover_i\",\n].join(\",\")\n\nexport type OpenLibraryProviderOptions = {\n /** API origin. Defaults to `https://openlibrary.org`. */\n readonly baseUrl?: string\n /** Cover service origin. Defaults to `https://covers.openlibrary.org`. */\n readonly coversBaseUrl?: string\n /** For tests, a custom agent, or a caching layer. Defaults to the global one. */\n readonly fetch?: typeof globalThis.fetch\n /**\n * Open Library's API etiquette asks for an identifying `User-Agent` (app\n * name + contact) and throttles anonymous traffic harder.\n */\n readonly userAgent?: string\n}\n\nconst searchTerms = (\n metadata: ResolvedMetadata,\n): Record<string, string> | undefined => {\n const title = metadata.title?.trim()\n\n if (title === undefined || title.length === 0) return undefined\n\n const author = metadataAuthors(metadata)[0]\n\n return {\n title,\n ...(author !== undefined ? { author } : {}),\n }\n}\n\n/**\n * Open Library — the Internet Archive's open bibliographic catalog. Free, no\n * key, covers books broadly (much less so comics and manga).\n *\n * ```ts\n * const provider = createOpenLibraryProvider({\n * userAgent: \"MyReader/1.0 (contact@example.com)\",\n * })\n * ```\n *\n * Lookup strategy, at most two requests: an **ISBN search** when the book\n * states one — the catalog then verifies the identity for us — falling back\n * to a **title (+ first author) search** when the ISBN is unknown to it or\n * absent. A query with neither an ISBN nor a title yields no candidates\n * rather than a fishing expedition.\n */\nexport const createOpenLibraryProvider = (\n options: OpenLibraryProviderOptions = {},\n): MetadataProvider => {\n const baseUrl = options.baseUrl ?? DEFAULT_BASE_URL\n const coversBaseUrl = options.coversBaseUrl ?? DEFAULT_COVERS_BASE_URL\n\n const searchDocs = async (\n terms: Record<string, string>,\n context: MetadataProviderContext,\n ): Promise<ReadonlyArray<OpenLibraryDoc>> => {\n const url = new URL(\"/search.json\", baseUrl)\n\n for (const [key, value] of Object.entries(terms)) {\n url.searchParams.set(key, value)\n }\n\n url.searchParams.set(\"fields\", SEARCH_FIELDS)\n url.searchParams.set(\"limit\", String(context.limit))\n\n // read lazily so a consumer stubbing the global afterwards still wins\n const doFetch = options.fetch ?? globalThis.fetch\n const response = await doFetch(url.toString(), {\n signal: context.signal,\n headers: {\n Accept: \"application/json\",\n ...(options.userAgent !== undefined\n ? { \"User-Agent\": options.userAgent }\n : {}),\n },\n })\n\n if (!response.ok) {\n throw new MetadataProviderResponseError(\n response.status,\n `Open Library search failed with status ${response.status}`,\n )\n }\n\n return parseOpenLibrarySearchResponse(await response.json())\n }\n\n const toCandidates = (\n docs: ReadonlyArray<OpenLibraryDoc>,\n isbn: string | undefined,\n ): ReadonlyArray<MetadataCandidate> =>\n docs.map((doc) => ({\n metadata: resolveOpenLibraryDoc(doc, { coversBaseUrl, isbn }),\n id: doc.key,\n url: doc.key !== undefined ? `${baseUrl}${doc.key}` : undefined,\n raw: doc,\n }))\n\n return {\n id: OPEN_LIBRARY_PROVIDER_ID,\n name: \"Open Library\",\n search: async (metadata, context) => {\n const isbn = metadata.isbn?.trim() || undefined\n\n if (isbn !== undefined) {\n const docs = await searchDocs({ isbn }, context)\n\n if (docs.length > 0) return toCandidates(docs, isbn)\n }\n\n const terms = searchTerms(metadata)\n\n if (terms === undefined) return []\n\n return toCandidates(await searchDocs(terms, context), undefined)\n },\n }\n}\n","import type { ResolvedMetadata } from \"@prose-reader/archive-reader\"\nimport { metadataAuthors } from \"./metadataAuthors.ts\"\n\n/**\n * Whether a catalog has anything to go on, so a lookup with nothing to ask\n * about costs no round trip. Descriptive fields alone — a publisher, a\n * language, a page count — narrow a search but cannot start one.\n */\nexport const hasSearchTerms = (metadata: ResolvedMetadata): boolean =>\n [\n metadata.title,\n metadata.isbn,\n metadata.gtin,\n metadata.belongsTo?.series?.[0]?.name,\n ].some((value) => value !== undefined && value.trim().length > 0) ||\n (metadata.identifiers ?? []).length > 0 ||\n metadataAuthors(metadata).length > 0\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.cjs","names":[],"sources":["../src/utils/metadataAuthors.ts","../src/utils/toIsbn13.ts","../src/match/similarity.ts","../src/match/scoreMetadataCandidate.ts","../src/utils/omitUndefined.ts","../src/merge/mergeResolvedMetadata.ts","../src/providers/responseError.ts","../src/report.ts","../src/fetchMetadata.ts","../src/utils/json.ts","../src/providers/openLibrary/parse.ts","../src/providers/openLibrary/projectGutenbergIdentifier.ts","../src/providers/openLibrary/marcLanguage.ts","../src/providers/openLibrary/resolve.ts","../src/providers/openLibrary/createOpenLibraryProvider.ts","../src/utils/hasSearchTerms.ts"],"sourcesContent":["import type { ResolvedMetadata } from \"@prose-reader/archive-reader\"\n\n/**\n * The names to match a publication on: those credited as `author`, else — for\n * a publication crediting nobody as one, like a comic archive listing only a\n * penciler — every contributor. Order is preserved (sources list the lead\n * creator first) and duplicates are dropped.\n */\nexport const metadataAuthors = (\n metadata: ResolvedMetadata,\n): ReadonlyArray<string> => {\n const contributors = metadata.contributors ?? []\n const authors = contributors.filter((contributor) =>\n contributor.roles.includes(\"author\"),\n )\n const names = (authors.length > 0 ? authors : contributors)\n .map((contributor) => contributor.name.trim())\n .filter((name) => name.length > 0)\n\n return [...new Set(names)]\n}\n","import { normalizeIsbn } from \"@prose-reader/archive-reader\"\n\nconst ISBN_13 = /^\\d{13}$/\nconst ISBN_10 = /^\\d{9}[\\dX]$/\n\n/**\n * Put an ISBN in its 13-digit form so the two printed on the same book compare\n * equal. A book carries an ISBN-10 in its OPF and an ISBN-13 in a catalog\n * record all the time; comparing them verbatim would read as \"different\n * books\", which — ISBN being a decisive match signal — is the most damaging\n * false negative the matcher can produce.\n *\n * Input is put through `normalizeIsbn`, the same function that produces\n * {@link ResolvedMetadata.isbn} in the first place, so the hyphenated and\n * prefixed forms a human types (`978-0-441-01359-3`, `ISBN 0-441-01359-7`)\n * are recognized rather than silently dropped.\n *\n * Returns `undefined` when nothing ISBN-shaped can be recovered (check digits\n * are not verified: a wrong one still identifies the intended book).\n */\nexport const toIsbn13 = (isbn: string): string | undefined => {\n const normalized = normalizeIsbn(isbn)\n\n if (normalized === undefined) return undefined\n if (ISBN_13.test(normalized)) return normalized\n if (!ISBN_10.test(normalized)) return undefined\n\n // ISBN-10 → ISBN-13: prefix the 978 Bookland code, drop the ISBN-10 check\n // digit, recompute the EAN-13 one (weights 1,3,1,3…).\n const core = `978${normalized.slice(0, 9)}`\n let sum = 0\n\n for (let index = 0; index < core.length; index++) {\n // charCodeAt over indexing: `noUncheckedIndexedAccess` types `core[i]` as\n // possibly undefined even though the length is known here\n const digit = core.charCodeAt(index) - 48\n\n sum += index % 2 === 0 ? digit : digit * 3\n }\n\n return `${core}${(10 - (sum % 10)) % 10}`\n}\n","const DIACRITICS = /[\\u0300-\\u036f]/g\nconst NON_ALPHANUMERIC = /[^\\p{L}\\p{N}]+/gu\n\n/** `Les Misérables` ≡ `les miserables`, `Dune: Messiah` ≡ `dune messiah`. */\nexport const normalizeForComparison = (value: string): string =>\n value\n .normalize(\"NFD\")\n .replace(DIACRITICS, \"\")\n .toLowerCase()\n .replace(NON_ALPHANUMERIC, \" \")\n .trim()\n\nconst bigramCounts = (value: string): Map<string, number> => {\n const counts = new Map<string, number>()\n\n for (let index = 0; index < value.length - 1; index++) {\n const bigram = value.slice(index, index + 2)\n\n counts.set(bigram, (counts.get(bigram) ?? 0) + 1)\n }\n\n return counts\n}\n\n/**\n * Sørensen–Dice coefficient over character bigrams, `0` to `1`.\n *\n * Bigrams rather than an edit distance: titles disagree by whole words far\n * more often than by typos. Spaces are stripped so `Spider-Man` and\n * `Spider Man` stay exact.\n */\nexport const textSimilarity = (a: string, b: string): number => {\n const left = normalizeForComparison(a).replaceAll(\" \", \"\")\n const right = normalizeForComparison(b).replaceAll(\" \", \"\")\n\n if (left.length === 0 || right.length === 0) return 0\n if (left === right) return 1\n // nothing to compare: a single character has no bigram\n if (left.length < 2 || right.length < 2) return 0\n\n const leftBigrams = bigramCounts(left)\n let shared = 0\n\n for (const [bigram, count] of bigramCounts(right)) {\n shared += Math.min(count, leftBigrams.get(bigram) ?? 0)\n }\n\n return (2 * shared) / (left.length - 1 + (right.length - 1))\n}\n\n/** Spaced dashes only, so `Spider-Man` stays one word. */\nconst SUBTITLE_SEPARATOR = /\\s*[:;]\\s*|\\s+[-–—]\\s+/\n\nconst splitTitle = (value: string): { main: string; hasSubtitle: boolean } => {\n const [head = \"\", ...rest] = value.split(SUBTITLE_SEPARATOR)\n const main = head.trim()\n const subtitle = rest.join(\" \").trim()\n\n if (main.length === 0) return { main: value, hasSubtitle: false }\n\n return { main, hasSubtitle: subtitle.length > 0 }\n}\n\n/** Below an exact match: the omitted half could have been a volume title. */\nconst SUBTITLE_REPAIR_CEILING = 0.9\n\ntype TitleDivision = \"volume\" | \"part\" | \"book\"\nconst TITLE_DIVISIONS: ReadonlyArray<TitleDivision> = [\"volume\", \"part\", \"book\"]\n\nconst TITLE_DIVISION_PATTERN =\n /\\b(volumes?|vols?|parts?|books?)\\s+([\\p{L}\\p{N}]+)\\b/gu\n\nconst TITLE_NUMBER_WORDS: Readonly<Record<string, number>> = {\n one: 1,\n first: 1,\n two: 2,\n second: 2,\n three: 3,\n third: 3,\n four: 4,\n fourth: 4,\n five: 5,\n fifth: 5,\n six: 6,\n sixth: 6,\n seven: 7,\n seventh: 7,\n eight: 8,\n eighth: 8,\n nine: 9,\n ninth: 9,\n ten: 10,\n tenth: 10,\n eleven: 11,\n eleventh: 11,\n twelve: 12,\n twelfth: 12,\n}\n\nconst ROMAN_DIGIT_VALUES: Readonly<Record<string, number>> = {\n i: 1,\n v: 5,\n x: 10,\n l: 50,\n c: 100,\n d: 500,\n m: 1000,\n}\n\nconst parseRomanNumeral = (value: string): number | undefined => {\n if (!/^m{0,3}(cm|cd|d?c{0,3})(xc|xl|l?x{0,3})(ix|iv|v?i{0,3})$/i.test(value))\n return undefined\n\n let total = 0\n let previous = 0\n\n for (const symbol of [...value.toLowerCase()].reverse()) {\n const current = ROMAN_DIGIT_VALUES[symbol]\n\n if (current === undefined) return undefined\n\n if (current < previous) total -= current\n else {\n total += current\n previous = current\n }\n }\n\n return total > 0 ? total : undefined\n}\n\nconst parseTitleDivisionNumber = (value: string): number | undefined => {\n if (/^\\d+$/.test(value)) return Number.parseInt(value, 10)\n\n return TITLE_NUMBER_WORDS[value] ?? parseRomanNumeral(value)\n}\n\nconst titleDivision = (value: string): TitleDivision | undefined => {\n if (value.startsWith(\"vol\")) return \"volume\"\n if (value.startsWith(\"part\")) return \"part\"\n if (value.startsWith(\"book\")) return \"book\"\n\n return undefined\n}\n\nconst explicitTitleDivisions = (\n value: string,\n): ReadonlyMap<TitleDivision, ReadonlySet<number>> => {\n const divisions = new Map<TitleDivision, Set<number>>()\n\n for (const match of normalizeForComparison(value).matchAll(\n TITLE_DIVISION_PATTERN,\n )) {\n const division =\n match[1] !== undefined ? titleDivision(match[1]) : undefined\n const number =\n match[2] !== undefined ? parseTitleDivisionNumber(match[2]) : undefined\n\n if (division === undefined || number === undefined) continue\n\n const numbers = divisions.get(division) ?? new Set<number>()\n numbers.add(number)\n divisions.set(division, numbers)\n }\n\n return divisions\n}\n\nconst titleDivisionsContradict = (a: string, b: string): boolean => {\n const left = explicitTitleDivisions(a)\n const right = explicitTitleDivisions(b)\n\n for (const division of TITLE_DIVISIONS) {\n const leftNumbers = left.get(division)\n const rightNumbers = right.get(division)\n\n if (leftNumbers === undefined || rightNumbers === undefined) continue\n if ([...leftNumbers].every((number) => !rightNumbers.has(number)))\n return true\n }\n\n return false\n}\n\n/**\n * {@link textSimilarity}, with `Dune` matched against `Dune: a novel`.\n *\n * Only when **one** side states a subtitle: that asymmetry is the same title\n * catalogued at a different depth. When both state one, both catalogers meant\n * it — `Dune: Book One` and `Dune: Messiah` are two books.\n * Explicit volume, part, or book numbers are semantic identity: when both\n * sides state different ones, the title score is `0` however similar the rest.\n */\nexport const titleSimilarity = (a: string, b: string): number => {\n if (titleDivisionsContradict(a, b)) return 0\n\n const full = textSimilarity(a, b)\n const left = splitTitle(a)\n const right = splitTitle(b)\n\n if (left.hasSubtitle === right.hasSubtitle) return full\n\n return Math.max(\n full,\n textSimilarity(left.main, right.main) * SUBTITLE_REPAIR_CEILING,\n )\n}\n\nconst sortedTokens = (value: string): string =>\n normalizeForComparison(value)\n .split(\" \")\n .filter((token) => token.length > 0)\n .sort()\n .join(\" \")\n\n/** {@link textSimilarity} on sorted tokens: `Herbert, Frank` ≡ `Frank Herbert`. */\nexport const personNameSimilarity = (a: string, b: string): number =>\n textSimilarity(sortedTokens(a), sortedTokens(b))\n","import type { ResolvedMetadata } from \"@prose-reader/archive-reader\"\nimport type { MetadataMatchField, MetadataMatchSignal } from \"../types/match.ts\"\nimport type { MetadataIdentifier } from \"../types/provider.ts\"\nimport { metadataAuthors } from \"../utils/metadataAuthors.ts\"\nimport { toIsbn13 } from \"../utils/toIsbn13.ts\"\nimport {\n personNameSimilarity,\n textSimilarity,\n titleSimilarity,\n} from \"./similarity.ts\"\n\n/**\n * Intent, not statistics: identifiers *are* the book, a title is what a human\n * recognizes it by, everything else corroborates. An edition detail that\n * differs must never sink an otherwise convincing match on its own.\n */\nexport const METADATA_MATCH_WEIGHTS = {\n isbn: 1,\n gtin: 1,\n identifiers: 0.6,\n title: 0.8,\n contributors: 0.5,\n series: 0.3,\n published: 0.2,\n publisher: 0.15,\n languages: 0.15,\n numberOfPages: 0.15,\n} as const satisfies Record<MetadataMatchField, number>\n\n/**\n * Identity rather than description, so these settle the score outright in both\n * directions. Catalogs and books disagree on titles and page counts\n * constantly; they do not agree or disagree on an ISBN by accident.\n */\nconst DECISIVE_FIELDS: ReadonlySet<MetadataMatchField> = new Set([\n \"isbn\",\n \"gtin\",\n])\n\n/** Blank is absent — the vocabulary says so, a hand-built query may not. */\nconst stated = (value: string | undefined): string | undefined => {\n const trimmed = value?.trim()\n\n return trimmed !== undefined && trimmed.length > 0 ? trimmed : undefined\n}\n\nconst identifierKey = (identifier: MetadataIdentifier): string =>\n identifier.value.trim().toLowerCase()\n\nconst identifierLabel = (identifier: MetadataIdentifier): string =>\n identifier.scheme !== undefined\n ? `${identifier.scheme}:${identifier.value}`\n : identifier.value\n\n/**\n * An unannounced scheme (the OPF `dc:identifier` case) is not a contradiction;\n * a `DOI` and an `ISBN` carrying the same digits are.\n */\nconst identifiersAgree = (\n a: MetadataIdentifier,\n b: MetadataIdentifier,\n): boolean => {\n if (identifierKey(a) !== identifierKey(b)) return false\n if (a.scheme === undefined || b.scheme === undefined) return true\n\n return a.scheme.trim().toLowerCase() === b.scheme.trim().toLowerCase()\n}\n\nconst primaryLanguageSubtag = (language: string): string =>\n language.trim().toLowerCase().split(\"-\")[0] ?? \"\"\n\n/** Reprints shift the year around, so a near one still corroborates. */\nconst publishedYearScore = (\n queryYear: number,\n candidateYear: number,\n): number => {\n const distance = Math.abs(queryYear - candidateYear)\n\n if (distance === 0) return 1\n if (distance === 1) return 0.6\n if (distance <= 3) return 0.3\n\n return 0\n}\n\n/**\n * Proximity, not equality — front matter and ads move the count: identical is\n * `1`, 25% apart `0.5`, half as long `0`.\n */\nconst numberOfPagesScore = (query: number, candidate: number): number => {\n const largest = Math.max(query, candidate)\n\n if (largest <= 0) return 0\n\n return Math.max(0, 1 - (Math.abs(query - candidate) / largest) * 2)\n}\n\nconst bestSimilarity = (\n values: ReadonlyArray<string>,\n candidates: ReadonlyArray<string>,\n compare: (a: string, b: string) => number,\n): number =>\n values.reduce(\n (best, value) =>\n candidates.reduce(\n (innerBest, candidate) =>\n Math.max(innerBest, compare(value, candidate)),\n best,\n ),\n 0,\n )\n\nexport type ScoredMetadataCandidate = {\n readonly score: number\n readonly signals: ReadonlyArray<MetadataMatchSignal>\n}\n\n/**\n * How well a candidate agrees with the query, field by field, and why. Every\n * provider is scored by these rules, so their scores stay comparable:\n *\n * - **Only comparable fields count** — a field is compared when both sides\n * state it, so what the book doesn't know can neither raise nor lower a\n * candidate, and nothing in common scores `0`.\n * - **The aggregate is a weighted average** ({@link METADATA_MATCH_WEIGHTS}),\n * putting a rich query and a sparse one on the same scale.\n * - **A stated identifier settles it, both ways** — agreeing pins the score to\n * `1`, contradicting to `0`. The latter is what sinks the wrong edition that\n * agrees on everything a weighted average can see.\n */\nexport const scoreMetadataCandidate = (\n query: ResolvedMetadata,\n candidate: ResolvedMetadata,\n): ScoredMetadataCandidate => {\n const signals: MetadataMatchSignal[] = []\n\n const addSignal = (\n field: MetadataMatchField,\n values:\n | {\n score: number\n query: string\n candidate: string\n }\n | undefined,\n ) => {\n if (values === undefined) return\n\n signals.push({\n field,\n weight: METADATA_MATCH_WEIGHTS[field],\n score: Math.min(1, Math.max(0, values.score)),\n query: values.query,\n candidate: values.candidate,\n })\n }\n\n const compareStrings = (\n field: MetadataMatchField,\n rawQueryValue: string | undefined,\n rawCandidateValue: string | undefined,\n compare: (a: string, b: string) => number = textSimilarity,\n ) => {\n const queryValue = stated(rawQueryValue)\n const candidateValue = stated(rawCandidateValue)\n\n if (queryValue === undefined || candidateValue === undefined) return\n\n addSignal(field, {\n score: compare(queryValue, candidateValue),\n query: queryValue,\n candidate: candidateValue,\n })\n }\n\n const queryIsbn = stated(query.isbn)\n const candidateIsbn = stated(candidate.isbn)\n const queryIsbn13 = queryIsbn !== undefined ? toIsbn13(queryIsbn) : undefined\n const candidateIsbn13 =\n candidateIsbn !== undefined ? toIsbn13(candidateIsbn) : undefined\n\n if (\n queryIsbn !== undefined &&\n candidateIsbn !== undefined &&\n queryIsbn13 !== undefined &&\n candidateIsbn13 !== undefined\n ) {\n addSignal(\"isbn\", {\n score: queryIsbn13 === candidateIsbn13 ? 1 : 0,\n query: queryIsbn,\n candidate: candidateIsbn,\n })\n }\n\n const queryGtin = stated(query.gtin)\n const candidateGtin = stated(candidate.gtin)\n\n if (queryGtin !== undefined && candidateGtin !== undefined) {\n addSignal(\"gtin\", {\n score: queryGtin === candidateGtin ? 1 : 0,\n query: queryGtin,\n candidate: candidateGtin,\n })\n }\n\n const queryIdentifiers = query.identifiers ?? []\n const candidateIdentifiers = candidate.identifiers ?? []\n\n if (queryIdentifiers.length > 0 && candidateIdentifiers.length > 0) {\n const shared = queryIdentifiers.some((identifier) =>\n candidateIdentifiers.some((other) => identifiersAgree(identifier, other)),\n )\n\n addSignal(\"identifiers\", {\n score: shared ? 1 : 0,\n query: queryIdentifiers.map(identifierLabel).join(\", \"),\n candidate: candidateIdentifiers.map(identifierLabel).join(\", \"),\n })\n }\n\n compareStrings(\"title\", query.title, candidate.title, titleSimilarity)\n compareStrings(\"publisher\", query.publisher, candidate.publisher)\n compareStrings(\n \"series\",\n query.belongsTo?.series?.[0]?.name,\n candidate.belongsTo?.series?.[0]?.name,\n )\n\n const candidateAuthors = metadataAuthors(candidate)\n const queryAuthors = metadataAuthors(query)\n\n if (queryAuthors.length > 0 && candidateAuthors.length > 0) {\n addSignal(\"contributors\", {\n score: bestSimilarity(\n queryAuthors,\n candidateAuthors,\n personNameSimilarity,\n ),\n query: queryAuthors.join(\", \"),\n candidate: candidateAuthors.join(\", \"),\n })\n }\n\n const queryYear = query.published?.year\n const candidateYear = candidate.published?.year\n\n if (queryYear !== undefined && candidateYear !== undefined) {\n addSignal(\"published\", {\n score: publishedYearScore(queryYear, candidateYear),\n query: String(queryYear),\n candidate: String(candidateYear),\n })\n }\n\n const queryLanguages = query.languages ?? []\n const candidateLanguages = candidate.languages ?? []\n\n if (queryLanguages.length > 0 && candidateLanguages.length > 0) {\n const candidateSubtags = new Set(\n candidateLanguages.map(primaryLanguageSubtag),\n )\n\n addSignal(\"languages\", {\n score: queryLanguages.some((language) =>\n candidateSubtags.has(primaryLanguageSubtag(language)),\n )\n ? 1\n : 0,\n query: queryLanguages.join(\", \"),\n candidate: candidateLanguages.join(\", \"),\n })\n }\n\n if (\n query.numberOfPages !== undefined &&\n candidate.numberOfPages !== undefined\n ) {\n addSignal(\"numberOfPages\", {\n score: numberOfPagesScore(query.numberOfPages, candidate.numberOfPages),\n query: String(query.numberOfPages),\n candidate: String(candidate.numberOfPages),\n })\n }\n\n const decisive = signals.filter((signal) => DECISIVE_FIELDS.has(signal.field))\n\n // Contradiction wins over agreement: refusing a publication whose stated\n // identity disagrees is the recoverable mistake. Without this the average is\n // merciful to exactly the wrong candidate — a different edition agreeing on\n // title and author scores (0.8 + 0.5) / 2.3 ≈ 0.57, accepted.\n if (decisive.some((signal) => signal.score === 0))\n return { score: 0, signals }\n if (decisive.some((signal) => signal.score === 1))\n return { score: 1, signals }\n\n const totalWeight = signals.reduce(\n (total, signal) => total + signal.weight,\n 0,\n )\n\n if (totalWeight === 0) return { score: 0, signals }\n\n const weighted = signals.reduce(\n (total, signal) => total + signal.score * signal.weight,\n 0,\n )\n\n return { score: weighted / totalWeight, signals }\n}\n","/**\n * Returns a copy of `obj` without the keys whose value is `undefined`, so\n * results stay sparse (an absent key and an `undefined` value are the same\n * statement: \"nothing was found for this field\").\n */\nexport const omitUndefined = <T extends object>(obj: T): T => {\n const entries = Object.entries(obj).filter(([, value]) => value !== undefined)\n\n // `as T`: Object.entries/fromEntries erase the key–value pairing; filtering\n // only drops `undefined` values, which `T`'s optional fields already allow,\n // so the runtime shape is a subset of `T` with no way to express it inline.\n return Object.fromEntries(entries) as T\n}\n","import type { ResolvedMetadata } from \"@prose-reader/archive-reader\"\nimport type { MetadataIdentifier } from \"../types/provider.ts\"\nimport { omitUndefined } from \"../utils/omitUndefined.ts\"\n\nconst dedupeIdentifiers = (\n identifiers: ReadonlyArray<MetadataIdentifier>,\n): ReadonlyArray<MetadataIdentifier> => {\n const deduped = new Map<string, MetadataIdentifier>()\n\n for (const identifier of identifiers) {\n const key = `${identifier.scheme?.trim().toLowerCase() ?? \"\"}:${identifier.value.trim().toLowerCase()}`\n const existing = deduped.get(key)\n\n if (existing === undefined) {\n deduped.set(key, identifier)\n continue\n }\n\n if (identifier.unique === true && existing.unique !== true) {\n deduped.set(key, { ...existing, unique: true })\n }\n }\n\n return [...deduped.values()]\n}\n\n/**\n * Merges several {@link ResolvedMetadata} into one, **first defined wins**.\n * Precedence is the caller's — pass the sources in the order you trust them.\n *\n * ```ts\n * // the book over the catalogs, catalogs filling the gaps\n * const metadata = mergeResolvedMetadata(resolved.metadata, fetched.metadata)\n * ```\n *\n * Field-wise rather than object-wise, so a source knowing only a cover\n * contributes it without hiding another's title. Two fields are additive\n * instead: `identifiers` concatenate (deduped on scheme + value, mirroring\n * `resolveMetadata`), and `belongsTo` merges `series` and `collection`\n * independently.\n *\n * Everything else takes the first stated value whole. Unioning keyword lists\n * across catalogs is a judgement call for the consumer, not for a merge that\n * has to stay predictable.\n */\nexport const mergeResolvedMetadata = (\n ...entries: ReadonlyArray<ResolvedMetadata | undefined>\n): ResolvedMetadata => {\n const defined = entries.filter(\n (entry): entry is ResolvedMetadata => entry !== undefined,\n )\n\n const first = <K extends keyof ResolvedMetadata>(\n key: K,\n ): ResolvedMetadata[K] => {\n for (const entry of defined) {\n const value = entry[key]\n\n if (value !== undefined) return value\n }\n\n return undefined\n }\n\n const identifiers = dedupeIdentifiers(\n defined.flatMap((entry) => entry.identifiers ?? []),\n )\n\n const belongsTo = omitUndefined({\n series: defined.find((entry) => entry.belongsTo?.series !== undefined)\n ?.belongsTo?.series,\n collection: defined.find(\n (entry) => entry.belongsTo?.collection !== undefined,\n )?.belongsTo?.collection,\n })\n\n // `Required` makes every key mandatory in this literal (values still accept\n // `undefined`), so a field added to the vocabulary is a compile error here\n // until its merge rule is chosen, rather than silently dropped.\n const merged: {\n [K in keyof Required<ResolvedMetadata>]: ResolvedMetadata[K]\n } = {\n title: first(\"title\"),\n cover: first(\"cover\"),\n description: first(\"description\"),\n publisher: first(\"publisher\"),\n imprint: first(\"imprint\"),\n rights: first(\"rights\"),\n languages: first(\"languages\"),\n subjects: first(\"subjects\"),\n contributors: first(\"contributors\"),\n published: first(\"published\"),\n readingDirection: first(\"readingDirection\"),\n renditionLayout: first(\"renditionLayout\"),\n renditionFlow: first(\"renditionFlow\"),\n renditionSpread: first(\"renditionSpread\"),\n numberOfPages: first(\"numberOfPages\"),\n gtin: first(\"gtin\"),\n isbn: first(\"isbn\"),\n identifiers: identifiers.length > 0 ? identifiers : undefined,\n belongsTo: Object.keys(belongsTo).length > 0 ? belongsTo : undefined,\n properties: first(\"properties\"),\n comic: first(\"comic\"),\n apple: first(\"apple\"),\n kobo: first(\"kobo\"),\n }\n\n return omitUndefined(merged)\n}\n","/**\n * Thrown by a provider when a catalog answered with a failing status. Carrying\n * it is what lets `fetchMetadata` report *why* a provider dropped out: `429`\n * is \"ask again later\", `503` is \"the catalog is down\", and a consumer decides\n * differently for each.\n *\n * ```ts\n * if (!response.ok) {\n * throw new MetadataProviderResponseError(response.status, \"My Catalog search failed\")\n * }\n * ```\n */\nexport class MetadataProviderResponseError extends Error {\n readonly status: number\n\n constructor(status: number, message?: string) {\n super(message ?? `Provider responded with status ${status}`)\n\n this.name = \"MetadataProviderResponseError\"\n this.status = status\n }\n}\n\n/**\n * The HTTP status behind a thrown error, when there is one.\n *\n * Structural rather than `instanceof`, so a provider built on another HTTP\n * client works unadapted. No status means it was never a response — network,\n * parse, bug — which is itself the answer.\n */\nexport const responseErrorStatus = (error: unknown): number | undefined => {\n if (typeof error !== \"object\" || error === null) return undefined\n if (!(\"status\" in error)) return undefined\n\n const { status } = error\n\n if (typeof status !== \"number\" || !Number.isInteger(status)) return undefined\n\n // an unrelated `status` field means something else entirely\n return status >= 100 && status <= 599 ? status : undefined\n}\n","import { Report as SharedReport } from \"@prose-reader/shared\"\n\n// Spelled out rather than read from package.json: node runs this source\n// directly in development, where a JSON import needs an import attribute.\nconst NAME = \"@prose-reader/metadata-fetcher\"\n\n// No explicit `enabled`: inherit the shared root, which auto-configures from\n// the `globalThis.__PROSE_READER_DEBUG` flag.\nexport const Report = SharedReport.namespace(NAME, undefined, {\n color: \"#b06642\",\n})\n","import type { ResolvedMetadata } from \"@prose-reader/archive-reader\"\nimport { scoreMetadataCandidate } from \"./match/scoreMetadataCandidate.ts\"\nimport { mergeResolvedMetadata } from \"./merge/mergeResolvedMetadata.ts\"\nimport { responseErrorStatus } from \"./providers/responseError.ts\"\nimport { Report } from \"./report.ts\"\nimport type {\n FailedMetadataProvider,\n FetchedMetadata,\n FetchedMetadataSource,\n} from \"./types/fetchedMetadata.ts\"\nimport type { MetadataMatch } from \"./types/match.ts\"\nimport type { MetadataCandidate, MetadataProvider } from \"./types/provider.ts\"\nimport { omitUndefined } from \"./utils/omitUndefined.ts\"\n\n/**\n * A {@link ResolvedMetadata}, or anything carrying one — the `ResolvedArchive`\n * shape, so the output of `resolveArchive` goes straight in. Hand-built terms\n * (`{ title: \"Dune\" }`) work too, when there is no container to resolve.\n */\nexport type FetchMetadataInput =\n | ResolvedMetadata\n | { readonly metadata: ResolvedMetadata }\n\nexport type FetchMetadataOptions = {\n /**\n * The catalogs to ask, queried concurrently. Their order is the tie-break\n * precedence: equally-scored matches rank in the order declared here, and\n * that ranking drives the merged `metadata`.\n */\n readonly providers: ReadonlyArray<MetadataProvider>\n /**\n * Hard cap on the matches kept **per provider**, best first; also the\n * page-size hint each provider receives. Defaults to `5`.\n */\n readonly limit?: number\n /**\n * Score a match must reach to be `accepted`, and so contribute to the merged\n * `metadata`. Defaults to `0.5`. Below it, matches are still returned and\n * ranked, for a consumer to offer as \"did you mean?\".\n */\n readonly minScore?: number\n /**\n * Keep each provider's own record on `match.raw`. Defaults to `false`: it is\n * provenance most consumers don't need, and can dwarf what they persist.\n */\n readonly includeRaw?: boolean\n readonly signal?: AbortSignal\n}\n\nconst DEFAULT_LIMIT = 5\nconst DEFAULT_MIN_SCORE = 0.5\nconst FETCHED_METADATA_VERSION = 1\n\nconst toMatch = ({\n candidate,\n providerId,\n score,\n signals,\n accepted,\n includeRaw,\n}: {\n candidate: MetadataCandidate\n providerId: string\n score: number\n signals: MetadataMatch[\"signals\"]\n accepted: boolean\n includeRaw: boolean\n}): MetadataMatch =>\n omitUndefined({\n providerId,\n score,\n signals,\n accepted,\n metadata: candidate.metadata,\n id: candidate.id,\n url: candidate.url,\n raw: includeRaw ? candidate.raw : undefined,\n })\n\n/**\n * Asks every provider what it knows about a book, scores the answers against\n * what you gave it, and merges the convincing ones into a single\n * {@link ResolvedMetadata} — the vocabulary `resolveArchive` produces, so\n * remote and embedded metadata stay interchangeable.\n *\n * ```ts\n * import { resolveArchive } from \"@prose-reader/archive-reader\"\n * import {\n * createOpenLibraryProvider,\n * fetchMetadata,\n * mergeResolvedMetadata,\n * } from \"@prose-reader/metadata-fetcher\"\n *\n * const resolved = await resolveArchive(archive)\n * const fetched = await fetchMetadata(resolved, {\n * providers: [createOpenLibraryProvider()],\n * })\n *\n * // what the catalogs found, and why we believe it\n * fetched.metadata.cover?.uri\n * fetched.matches[0]?.signals // [{ field: \"isbn\", score: 1, … }, …]\n *\n * // the book wins, the catalogs fill its gaps\n * const metadata = mergeResolvedMetadata(resolved.metadata, fetched.metadata)\n * ```\n *\n * Error policy mirrors `resolveArchive`: a provider that throws never fails\n * the fetch — it is logged through the debug `Report` and listed in\n * `failedProviders`. An abort is the exception: cancelling through `signal`\n * rejects, since the caller asked for it.\n */\nexport const fetchMetadata = async (\n input: FetchMetadataInput,\n options: FetchMetadataOptions,\n): Promise<FetchedMetadata> => {\n const metadata = \"metadata\" in input ? input.metadata : input\n const limit = options.limit ?? DEFAULT_LIMIT\n const minScore = options.minScore ?? DEFAULT_MIN_SCORE\n const includeRaw = options.includeRaw === true\n\n const results = await Promise.all(\n options.providers.map(async (provider) => {\n try {\n const candidates = await provider.search(metadata, {\n limit,\n signal: options.signal,\n })\n\n return { provider, candidates, failure: undefined }\n } catch (error) {\n // the caller pulled the plug: their cancellation, their rejection\n if (options.signal?.aborted === true) throw error\n\n // catalogs in the wild are flaky: a failing provider never fails the\n // fetch, it just doesn't contribute. The status, when the catalog\n // answered with one, is what tells a rate limit from an outage.\n Report.error(`fetchMetadata: provider \"${provider.id}\" failed`, error)\n\n return {\n provider,\n candidates: undefined,\n failure: omitUndefined({\n id: provider.id,\n status: responseErrorStatus(error),\n }),\n }\n }\n }),\n )\n\n const failedProviders: FailedMetadataProvider[] = results.flatMap((result) =>\n result.failure !== undefined ? [result.failure] : [],\n )\n\n const sources: Record<string, FetchedMetadataSource> = {}\n const matches: MetadataMatch[] = []\n\n for (const { provider, candidates } of results) {\n if (candidates === undefined) continue\n\n const providerMatches = candidates\n .map((candidate) => {\n const { score, signals } = scoreMetadataCandidate(\n metadata,\n candidate.metadata,\n )\n\n return toMatch({\n candidate,\n providerId: provider.id,\n score,\n signals,\n accepted: score >= minScore,\n includeRaw,\n })\n })\n // `sort` is stable, so equally-scored candidates keep the order the\n // provider ranked them in\n .sort((a, b) => b.score - a.score)\n .slice(0, limit)\n\n // ids key `sources`, so a duplicate would silently drop a provider's\n // detail — the kind of thing that costs an hour to notice\n if (sources[provider.id] !== undefined) {\n Report.warn(\n `fetchMetadata: several providers share the id \"${provider.id}\"`,\n )\n }\n\n sources[provider.id] = {\n provider: { id: provider.id, name: provider.name },\n matches: providerMatches,\n }\n\n matches.push(...providerMatches)\n }\n\n // again stable: providers were walked in declaration order, so that order\n // breaks ties between equally convincing catalogs\n const rankedMatches = [...matches].sort((a, b) => b.score - a.score)\n\n return {\n version: FETCHED_METADATA_VERSION,\n metadata: mergeResolvedMetadata(\n ...rankedMatches\n .filter((match) => match.accepted)\n .map((match) => match.metadata),\n ),\n matches: rankedMatches,\n sources,\n failedProviders,\n }\n}\n","/**\n * Readers for `JSON.parse` output. A catalog's payload is third-party data, so\n * it enters as `unknown` and every field goes through a guard: a renamed or\n * nulled field reads as absent instead of crashing the fetch, or producing a\n * candidate with `undefined` where a string was promised.\n */\nexport const isJsonRecord = (\n value: unknown,\n): value is Record<string, unknown> =>\n typeof value === \"object\" && value !== null && !Array.isArray(value)\n\nexport const readString = (\n record: Record<string, unknown>,\n key: string,\n): string | undefined => {\n const value = record[key]\n\n if (typeof value !== \"string\") return undefined\n\n const trimmed = value.trim()\n\n return trimmed.length > 0 ? trimmed : undefined\n}\n\nexport const readNumber = (\n record: Record<string, unknown>,\n key: string,\n): number | undefined => {\n const value = record[key]\n\n return typeof value === \"number\" && Number.isFinite(value) ? value : undefined\n}\n\nexport const readStringArray = (\n record: Record<string, unknown>,\n key: string,\n): ReadonlyArray<string> | undefined => {\n const value = record[key]\n\n if (!Array.isArray(value)) return undefined\n\n const strings = value\n .filter((entry): entry is string => typeof entry === \"string\")\n .map((entry) => entry.trim())\n .filter((entry) => entry.length > 0)\n\n return strings.length > 0 ? strings : undefined\n}\n\nexport const readRecordArray = (\n record: Record<string, unknown>,\n key: string,\n): ReadonlyArray<Record<string, unknown>> => {\n const value = record[key]\n\n if (!Array.isArray(value)) return []\n\n return value.filter(isJsonRecord)\n}\n","import {\n isJsonRecord,\n readNumber,\n readRecordArray,\n readString,\n readStringArray,\n} from \"../../utils/json.ts\"\n\n/**\n * One entry of an Open Library `search.json` response, field names mirroring\n * the API (same rule as the archive parsers: a parsed source looks like its\n * source document). Every field is optional — the API omits what it doesn't\n * have, and the `fields` parameter decides what it may return at all.\n *\n * @see https://openlibrary.org/dev/docs/api/search\n */\nexport type OpenLibraryDoc = {\n /** Work key, e.g. `/works/OL45883W`. */\n readonly key?: string\n readonly title?: string\n readonly subtitle?: string\n readonly author_name?: ReadonlyArray<string>\n readonly first_publish_year?: number\n readonly publisher?: ReadonlyArray<string>\n /** MARC 21 language codes (`eng`, `fre`), not BCP 47. */\n readonly language?: ReadonlyArray<string>\n readonly subject?: ReadonlyArray<string>\n readonly number_of_pages_median?: number\n /** Cover id, addressing `covers.openlibrary.org`. */\n readonly cover_i?: number\n /** Project Gutenberg ebook ids attached to the work's editions. */\n readonly id_project_gutenberg?: ReadonlyArray<string>\n}\n\nconst parseDoc = (record: Record<string, unknown>): OpenLibraryDoc => ({\n key: readString(record, \"key\"),\n title: readString(record, \"title\"),\n subtitle: readString(record, \"subtitle\"),\n author_name: readStringArray(record, \"author_name\"),\n first_publish_year: readNumber(record, \"first_publish_year\"),\n publisher: readStringArray(record, \"publisher\"),\n language: readStringArray(record, \"language\"),\n subject: readStringArray(record, \"subject\"),\n number_of_pages_median: readNumber(record, \"number_of_pages_median\"),\n cover_i: readNumber(record, \"cover_i\"),\n id_project_gutenberg: readStringArray(record, \"id_project_gutenberg\"),\n})\n\n/**\n * Reads the `docs` of a `search.json` payload. A payload that isn't a record,\n * or carries no `docs` array, yields an empty list: an unexpected response\n * shape is \"this catalog has nothing for us\", not a crash.\n */\nexport const parseOpenLibrarySearchResponse = (\n payload: unknown,\n): ReadonlyArray<OpenLibraryDoc> => {\n if (!isJsonRecord(payload)) return []\n\n return readRecordArray(payload, \"docs\").map(parseDoc)\n}\n","import type { ResolvedMetadata } from \"@prose-reader/archive-reader\"\n\nexport type OpenLibraryProjectGutenbergLookup = {\n readonly id: string\n /** The book's exact identifier spelling, echoed only after OL confirms it. */\n readonly identifier: {\n readonly value: string\n readonly scheme?: string\n }\n}\n\nconst GUTENBERG_HOSTS: ReadonlySet<string> = new Set([\n \"gutenberg.org\",\n \"www.gutenberg.org\",\n])\n\nconst GUTENBERG_PATHS: ReadonlyArray<RegExp> = [\n /^\\/ebooks\\/(\\d+)(?:[./]|$)/i,\n /^\\/files\\/(\\d+)(?:\\/|$)/i,\n /^\\/cache\\/epub\\/(\\d+)(?:\\/|$)/i,\n /^\\/(\\d+)(?:\\/|$)/,\n]\n\nconst projectGutenbergIdFromUrl = (value: string): string | undefined => {\n try {\n const url = new URL(value.trim())\n\n if (url.protocol !== \"http:\" && url.protocol !== \"https:\") return undefined\n if (!GUTENBERG_HOSTS.has(url.hostname.toLowerCase())) return undefined\n\n for (const pattern of GUTENBERG_PATHS) {\n const id = pattern.exec(url.pathname)?.[1]\n\n if (id !== undefined) return id\n }\n } catch {\n return undefined\n }\n\n return undefined\n}\n\n/**\n * Open Library-specific crosswalk: its `id_project_gutenberg` search field\n * stores the numeric id encoded by official Gutenberg URLs. Other providers\n * own their own identifier conventions; this is intentionally not generic.\n */\nexport const projectGutenbergLookupFromMetadata = (\n metadata: ResolvedMetadata,\n): OpenLibraryProjectGutenbergLookup | undefined => {\n for (const identifier of metadata.identifiers ?? []) {\n const scheme = identifier.scheme?.trim().toLowerCase()\n\n if (scheme !== undefined && scheme !== \"url\" && scheme !== \"uri\") continue\n\n const id = projectGutenbergIdFromUrl(identifier.value)\n\n if (id === undefined) continue\n\n return {\n id,\n identifier: {\n value: identifier.value,\n ...(identifier.scheme !== undefined\n ? { scheme: identifier.scheme }\n : {}),\n },\n }\n }\n\n return undefined\n}\n","/**\n * MARC 21 language codes → BCP 47 primary subtags, for the codes a general\n * book catalog actually returns. Open Library reports `language` as MARC\n * (`eng`, `fre`), while {@link ResolvedMetadata.languages} is BCP 47 by\n * contract, so the mapping is part of normalizing — not a nicety.\n *\n * Both MARC variants are listed where they differ (bibliographic `fre` and\n * terminology `fra`), since records in the wild carry either.\n */\nconst MARC_TO_BCP47: Readonly<Record<string, string>> = {\n afr: \"af\",\n alb: \"sq\",\n amh: \"am\",\n ara: \"ar\",\n arm: \"hy\",\n asm: \"as\",\n aze: \"az\",\n baq: \"eu\",\n bel: \"be\",\n ben: \"bn\",\n bos: \"bs\",\n bul: \"bg\",\n bur: \"my\",\n cat: \"ca\",\n ces: \"cs\",\n chi: \"zh\",\n cym: \"cy\",\n cze: \"cs\",\n dan: \"da\",\n deu: \"de\",\n dut: \"nl\",\n ell: \"el\",\n eng: \"en\",\n epo: \"eo\",\n est: \"et\",\n eus: \"eu\",\n fao: \"fo\",\n fas: \"fa\",\n fin: \"fi\",\n fra: \"fr\",\n fre: \"fr\",\n geo: \"ka\",\n ger: \"de\",\n gle: \"ga\",\n glg: \"gl\",\n gre: \"el\",\n guj: \"gu\",\n heb: \"he\",\n hin: \"hi\",\n hrv: \"hr\",\n hun: \"hu\",\n hye: \"hy\",\n ice: \"is\",\n ind: \"id\",\n isl: \"is\",\n ita: \"it\",\n jpn: \"ja\",\n kan: \"kn\",\n kat: \"ka\",\n kaz: \"kk\",\n khm: \"km\",\n kor: \"ko\",\n lao: \"lo\",\n lat: \"la\",\n lav: \"lv\",\n lit: \"lt\",\n ltz: \"lb\",\n mac: \"mk\",\n mal: \"ml\",\n mar: \"mr\",\n may: \"ms\",\n mkd: \"mk\",\n mlt: \"mt\",\n mon: \"mn\",\n msa: \"ms\",\n mya: \"my\",\n nep: \"ne\",\n nld: \"nl\",\n nno: \"nn\",\n nob: \"nb\",\n nor: \"no\",\n ori: \"or\",\n pan: \"pa\",\n per: \"fa\",\n pol: \"pl\",\n por: \"pt\",\n rom: \"ro\",\n ron: \"ro\",\n rum: \"ro\",\n rus: \"ru\",\n scc: \"sr\",\n scr: \"hr\",\n sin: \"si\",\n slk: \"sk\",\n slo: \"sk\",\n slv: \"sl\",\n snd: \"sd\",\n spa: \"es\",\n sqi: \"sq\",\n srp: \"sr\",\n swa: \"sw\",\n swe: \"sv\",\n tam: \"ta\",\n tel: \"te\",\n tgl: \"tl\",\n tha: \"th\",\n tur: \"tr\",\n ukr: \"uk\",\n urd: \"ur\",\n uzb: \"uz\",\n vie: \"vi\",\n wel: \"cy\",\n yid: \"yi\",\n zho: \"zh\",\n}\n\n/**\n * MARC placeholders that say \"no single language\": keeping them would put a\n * non-language in a BCP 47 field, so they resolve to nothing.\n */\nconst NON_LANGUAGE_CODES = new Set([\"mul\", \"und\", \"zxx\", \"sgn\"])\n\n/**\n * Normalizes one MARC language code to its BCP 47 primary subtag. Unknown\n * codes (the long tail of MARC) pass through lowercased rather than being\n * dropped — a language we can't name is still a language the record stated.\n */\nexport const marcLanguageToBcp47 = (code: string): string | undefined => {\n const normalized = code.trim().toLowerCase()\n\n if (normalized.length === 0) return undefined\n if (NON_LANGUAGE_CODES.has(normalized)) return undefined\n\n return MARC_TO_BCP47[normalized] ?? normalized\n}\n","import type { ResolvedMetadata } from \"@prose-reader/archive-reader\"\nimport { omitUndefined } from \"../../utils/omitUndefined.ts\"\nimport { marcLanguageToBcp47 } from \"./marcLanguage.ts\"\nimport type { OpenLibraryDoc } from \"./parse.ts\"\n\n/**\n * A popular work carries hundreds of subject headings, most of them long-tail\n * noise (\"Accessible book\", \"Protected DAISY\"), which would dwarf the entity a\n * consumer persists. The API puts the meaningful ones first.\n */\nexport const OPEN_LIBRARY_MAX_SUBJECTS = 25\n\n/**\n * Where every {@link OpenLibraryDoc} field lands in {@link ResolvedMetadata},\n * compile-enforced: adding a field to the parser without declaring its home is\n * a type error. The archive parsers' losslessness contract, and the mapping\n * documentation.\n */\nexport const openLibraryMetadataHomes = {\n key: \"identifiers\",\n title: \"title\",\n subtitle: \"title\",\n author_name: \"contributors\",\n first_publish_year: \"published\",\n publisher: \"publisher\",\n language: \"languages\",\n subject: \"subjects\",\n number_of_pages_median: \"numberOfPages\",\n cover_i: \"cover\",\n id_project_gutenberg: \"identifiers\",\n} as const satisfies Record<\n keyof OpenLibraryDoc,\n keyof ResolvedMetadata | \"identifiers\"\n>\n\nexport const OPEN_LIBRARY_IDENTIFIER_SCHEME = \"OpenLibrary\"\nconst PROJECT_GUTENBERG_IDENTIFIER_SCHEME = \"ProjectGutenberg\"\n\nconst emptyToUndefined = <T>(\n values: ReadonlyArray<T>,\n): ReadonlyArray<T> | undefined => (values.length > 0 ? values : undefined)\n\n/**\n * Normalizes one search hit into the cross-format vocabulary. Two choices\n * worth stating:\n *\n * - **`title` folds in `subtitle`** (`Dune: Messiah`): the vocabulary has one\n * title field, and an OPF `dc:title` normally carries the subtitle too, so\n * comparing a bare title against a full one would cost match score.\n * - **`isbn` is set only for an ISBN lookup**, to the queried one — the API\n * answered \"this work has that ISBN\", a fact about the record. A hit\n * describes a *work*, whose editions each have their own ISBN, so picking\n * one out of a title search would be fabrication.\n */\nexport const resolveOpenLibraryDoc = (\n doc: OpenLibraryDoc,\n options: {\n readonly coversBaseUrl: string\n /** The ISBN this record was looked up by, when it was. */\n readonly isbn?: string\n /** The source identifier that an exact Gutenberg-id lookup confirmed. */\n readonly matchedProjectGutenbergIdentifier?: {\n readonly value: string\n readonly scheme?: string\n }\n },\n): ResolvedMetadata => {\n const title =\n doc.title !== undefined && doc.subtitle !== undefined\n ? `${doc.title}: ${doc.subtitle}`\n : doc.title\n\n const languages = emptyToUndefined([\n ...new Set(\n (doc.language ?? [])\n .map(marcLanguageToBcp47)\n .filter((language): language is string => language !== undefined),\n ),\n ])\n\n const identifiers = [\n ...(options.isbn !== undefined\n ? [{ value: options.isbn, scheme: \"ISBN\" }]\n : []),\n ...(options.matchedProjectGutenbergIdentifier !== undefined\n ? [options.matchedProjectGutenbergIdentifier]\n : []),\n ...(doc.id_project_gutenberg ?? []).map((value) => ({\n value,\n scheme: PROJECT_GUTENBERG_IDENTIFIER_SCHEME,\n })),\n ...(doc.key !== undefined\n ? [{ value: doc.key, scheme: OPEN_LIBRARY_IDENTIFIER_SCHEME }]\n : []),\n ]\n\n return omitUndefined({\n title,\n cover:\n doc.cover_i !== undefined\n ? {\n // absolute, not container-relative: a catalog addresses its\n // covers in its own space, with nothing to rebase onto\n uri: `${options.coversBaseUrl}/b/id/${doc.cover_i}-L.jpg`,\n mediaType: \"image/jpeg\",\n confidence: \"derived\" as const,\n }\n : undefined,\n publisher: doc.publisher?.[0],\n languages,\n subjects: emptyToUndefined(\n (doc.subject ?? []).slice(0, OPEN_LIBRARY_MAX_SUBJECTS),\n ),\n contributors: emptyToUndefined(\n (doc.author_name ?? []).map((name) => ({\n name,\n roles: [\"author\" as const],\n })),\n ),\n published:\n doc.first_publish_year !== undefined\n ? { year: doc.first_publish_year }\n : undefined,\n numberOfPages: doc.number_of_pages_median,\n isbn: options.isbn,\n identifiers: emptyToUndefined(identifiers),\n })\n}\n","import type { ResolvedMetadata } from \"@prose-reader/archive-reader\"\nimport type {\n MetadataCandidate,\n MetadataProvider,\n MetadataProviderContext,\n} from \"../../types/provider.ts\"\nimport { metadataAuthors } from \"../../utils/metadataAuthors.ts\"\nimport { MetadataProviderResponseError } from \"../responseError.ts\"\nimport { type OpenLibraryDoc, parseOpenLibrarySearchResponse } from \"./parse.ts\"\nimport {\n type OpenLibraryProjectGutenbergLookup,\n projectGutenbergLookupFromMetadata,\n} from \"./projectGutenbergIdentifier.ts\"\nimport { resolveOpenLibraryDoc } from \"./resolve.ts\"\n\nexport const OPEN_LIBRARY_PROVIDER_ID = \"openLibrary\"\n\nconst DEFAULT_BASE_URL = \"https://openlibrary.org\"\nconst DEFAULT_COVERS_BASE_URL = \"https://covers.openlibrary.org\"\n\n/**\n * Only what {@link resolveOpenLibraryDoc} maps: `search.json` returns every\n * edition key and IA identifier by default, so `fields` is the difference\n * between a few hundred bytes per hit and a few hundred kilobytes.\n */\nconst SEARCH_FIELDS = [\n \"key\",\n \"title\",\n \"subtitle\",\n \"author_name\",\n \"first_publish_year\",\n \"publisher\",\n \"language\",\n \"subject\",\n \"number_of_pages_median\",\n \"cover_i\",\n \"id_project_gutenberg\",\n].join(\",\")\n\nexport type OpenLibraryProviderOptions = {\n /** API origin. Defaults to `https://openlibrary.org`. */\n readonly baseUrl?: string\n /** Cover service origin. Defaults to `https://covers.openlibrary.org`. */\n readonly coversBaseUrl?: string\n /** For tests, a custom agent, or a caching layer. Defaults to the global one. */\n readonly fetch?: typeof globalThis.fetch\n /**\n * Open Library's API etiquette asks for an identifying `User-Agent` (app\n * name + contact) and throttles anonymous traffic harder.\n */\n readonly userAgent?: string\n}\n\nconst searchTerms = (\n metadata: ResolvedMetadata,\n): Record<string, string> | undefined => {\n const title = metadata.title?.trim()\n\n if (title === undefined || title.length === 0) return undefined\n\n const author = metadataAuthors(metadata)[0]\n\n return {\n title,\n ...(author !== undefined ? { author } : {}),\n }\n}\n\n/**\n * Open Library — the Internet Archive's open bibliographic catalog. Free, no\n * key, covers books broadly (much less so comics and manga).\n *\n * ```ts\n * const provider = createOpenLibraryProvider({\n * userAgent: \"MyReader/1.0 (contact@example.com)\",\n * })\n * ```\n *\n * Lookup strategy, at most three requests: an **ISBN search** when the book\n * states one; an exact **Project Gutenberg id search** when an identifier is\n * an official Gutenberg URL; then a **title (+ first author) search**. A query\n * with none of those terms yields no candidates rather than a fishing\n * expedition.\n */\nexport const createOpenLibraryProvider = (\n options: OpenLibraryProviderOptions = {},\n): MetadataProvider => {\n const baseUrl = options.baseUrl ?? DEFAULT_BASE_URL\n const coversBaseUrl = options.coversBaseUrl ?? DEFAULT_COVERS_BASE_URL\n\n const searchDocs = async (\n terms: Record<string, string>,\n context: MetadataProviderContext,\n ): Promise<ReadonlyArray<OpenLibraryDoc>> => {\n const url = new URL(\"/search.json\", baseUrl)\n\n for (const [key, value] of Object.entries(terms)) {\n url.searchParams.set(key, value)\n }\n\n url.searchParams.set(\"fields\", SEARCH_FIELDS)\n url.searchParams.set(\"limit\", String(context.limit))\n\n // read lazily so a consumer stubbing the global afterwards still wins\n const doFetch = options.fetch ?? globalThis.fetch\n const response = await doFetch(url.toString(), {\n signal: context.signal,\n headers: {\n Accept: \"application/json\",\n ...(options.userAgent !== undefined\n ? { \"User-Agent\": options.userAgent }\n : {}),\n },\n })\n\n if (!response.ok) {\n throw new MetadataProviderResponseError(\n response.status,\n `Open Library search failed with status ${response.status}`,\n )\n }\n\n return parseOpenLibrarySearchResponse(await response.json())\n }\n\n const toCandidates = (\n docs: ReadonlyArray<OpenLibraryDoc>,\n options: {\n readonly isbn?: string\n readonly confirmedProjectGutenberg?: OpenLibraryProjectGutenbergLookup\n } = {},\n ): ReadonlyArray<MetadataCandidate> =>\n docs.map((doc) => ({\n metadata: resolveOpenLibraryDoc(doc, {\n coversBaseUrl,\n isbn: options.isbn,\n matchedProjectGutenbergIdentifier:\n options.confirmedProjectGutenberg?.identifier,\n }),\n id: doc.key,\n url: doc.key !== undefined ? `${baseUrl}${doc.key}` : undefined,\n raw: doc,\n }))\n\n return {\n id: OPEN_LIBRARY_PROVIDER_ID,\n name: \"Open Library\",\n search: async (metadata, context) => {\n const isbn = metadata.isbn?.trim() || undefined\n const projectGutenberg = projectGutenbergLookupFromMetadata(metadata)\n\n if (isbn !== undefined) {\n const docs = await searchDocs({ isbn }, context)\n\n if (docs.length > 0) return toCandidates(docs, { isbn })\n }\n\n if (projectGutenberg !== undefined) {\n const docs = await searchDocs(\n { q: `id_project_gutenberg:${projectGutenberg.id}` },\n context,\n )\n\n if (docs.length > 0) {\n return toCandidates(docs, {\n confirmedProjectGutenberg: projectGutenberg,\n })\n }\n }\n\n const terms = searchTerms(metadata)\n\n if (terms === undefined) return []\n\n return toCandidates(await searchDocs(terms, context))\n },\n }\n}\n","import type { ResolvedMetadata } from \"@prose-reader/archive-reader\"\nimport { metadataAuthors } from \"./metadataAuthors.ts\"\n\n/**\n * Whether a catalog has anything to go on, so a lookup with nothing to ask\n * about costs no round trip. Descriptive fields alone — a publisher, a\n * language, a page count — narrow a search but cannot start one.\n */\nexport const hasSearchTerms = (metadata: ResolvedMetadata): boolean =>\n [\n metadata.title,\n metadata.isbn,\n metadata.gtin,\n metadata.belongsTo?.series?.[0]?.name,\n ].some((value) => value !== undefined && value.trim().length > 0) ||\n (metadata.identifiers ?? []).length > 0 ||\n metadataAuthors(metadata).length > 0\n"],"mappings":"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"}