diff --git a/IsraelHiking.Web/package-lock.json b/IsraelHiking.Web/package-lock.json index f9c1c022..4ef52a13 100644 --- a/IsraelHiking.Web/package-lock.json +++ b/IsraelHiking.Web/package-lock.json @@ -14358,9 +14358,9 @@ } }, "node_modules/nanoid": { - "version": "3.3.7", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", - "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", + "version": "3.3.8", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz", + "integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==", "funding": [ { "type": "github", diff --git a/IsraelHiking.Web/src/application/services/image-attribution.service.spec.ts b/IsraelHiking.Web/src/application/services/image-attribution.service.spec.ts index 5b32df8f..a52e0dde 100644 --- a/IsraelHiking.Web/src/application/services/image-attribution.service.spec.ts +++ b/IsraelHiking.Web/src/application/services/image-attribution.service.spec.ts @@ -33,7 +33,7 @@ describe("ImageAttributionService", () => { expect(response.url).toBe("https://www.example.com"); })); - it("should fetch data from wikipedia when getting wikimedia image", inject([ImageAttributionService, HttpTestingController], + it("should fetch data from wikimedia when getting wikimedia image", inject([ImageAttributionService, HttpTestingController], async (service: ImageAttributionService, mockBackend: HttpTestingController) => { const promise = service.getAttributionForImage("https://upload.wikimedia.org/wikipedia/commons/thumb/a/a1/IHM_Image.jpeg"); mockBackend.match(r => r.url.startsWith("https://commons.wikimedia.org/"))[0].flush({ @@ -59,10 +59,88 @@ describe("ImageAttributionService", () => { expect(response.url).toBe("https://commons.wikimedia.org/wiki/File:IHM_Image.jpeg"); })); - it("should remove html tags and get the value inside", inject([ImageAttributionService, HttpTestingController], + it("should fetch attribution from wikimedia when getting wikimedia image with attribution and no author", inject([ImageAttributionService, HttpTestingController], async (service: ImageAttributionService, mockBackend: HttpTestingController) => { const promise = service.getAttributionForImage("https://upload.wikimedia.org/wikipedia/commons/thumb/a/a1/IHM_Image.jpeg"); mockBackend.match(r => r.url.startsWith("https://commons.wikimedia.org/"))[0].flush({ + query: { + pages: { + "-1": { + imageinfo: [{ + extmetadata: { + Attribution: { + value: "hello" + } + } + }] + } + } + } + }); + + const response = await promise; + + expect(response).not.toBeNull(); + expect(response.author).toBe("hello"); + expect(response.url).toBe("https://commons.wikimedia.org/wiki/File:IHM_Image.jpeg"); + })); + + it("should fetch attribution from wikimedia when getting wikimedia image with permissive license and no author or attribution", inject([ImageAttributionService, HttpTestingController], + async (service: ImageAttributionService, mockBackend: HttpTestingController) => { + const promise = service.getAttributionForImage("https://upload.wikimedia.org/wikipedia/commons/thumb/a/a1/IHM_Image.jpeg"); + mockBackend.match(r => r.url.startsWith("https://commons.wikimedia.org/"))[0].flush({ + query: { + pages: { + "14686480": { + imageinfo: [{ + extmetadata: { + LicenseShortName: { + value: "Cc-by-sa-3.0" + } + } + }] + } + } + } + }); + + const response = await promise; + + expect(response).not.toBeNull(); + expect(response.author).toBe("Unknown"); + expect(response.url).toBe("https://commons.wikimedia.org/wiki/File:IHM_Image.jpeg"); + })); + + it("should fetch data from wikimedia when getting wikimedia file", inject([ImageAttributionService, HttpTestingController], + async (service: ImageAttributionService, mockBackend: HttpTestingController) => { + const promise = service.getAttributionForImage("File:123.jpeg"); + mockBackend.match(r => r.url.startsWith("https://commons.wikimedia.org/"))[0].flush({ + query: { + pages: { + "-1": { + imageinfo: [{ + extmetadata: { + Artist: { + value: "hello" + } + } + }] + } + } + } + }); + + const response = await promise; + + expect(response).not.toBeNull(); + expect(response.author).toBe("hello"); + expect(response.url).toBe("https://commons.wikimedia.org/wiki/File:123.jpeg"); + })); + + it("should remove html tags and get the value inside", inject([ImageAttributionService, HttpTestingController], + async (service: ImageAttributionService, mockBackend: HttpTestingController) => { + const promise = service.getAttributionForImage("https://upload.wikimedia.org/wikipedia/he/thumb/a/a1/IHM_Image.jpeg"); + mockBackend.match(r => r.url.startsWith("https://he.wikipedia.org/"))[0].flush({ query: { pages: { "-1": { @@ -82,7 +160,7 @@ describe("ImageAttributionService", () => { expect(response).not.toBeNull(); expect(response.author).toBe("hello"); - expect(response.url).toBe("https://commons.wikimedia.org/wiki/File:IHM_Image.jpeg"); + expect(response.url).toBe("https://he.wikipedia.org/wiki/File:IHM_Image.jpeg"); })); it("should remove html tags, tabs and get the value inside", inject([ImageAttributionService, HttpTestingController], diff --git a/IsraelHiking.Web/src/application/services/image-attribution.service.ts b/IsraelHiking.Web/src/application/services/image-attribution.service.ts index c91a630c..851b0de3 100644 --- a/IsraelHiking.Web/src/application/services/image-attribution.service.ts +++ b/IsraelHiking.Web/src/application/services/image-attribution.service.ts @@ -1,6 +1,7 @@ import { HttpClient } from "@angular/common/http"; import { inject, Injectable } from "@angular/core"; import { firstValueFrom, timeout } from "rxjs"; +import type { WikiPage } from "./wikidata.service"; export type ImageAttribution = { author: string; @@ -27,10 +28,11 @@ export class ImageAttributionService { return this.attributionImageCache.get(imageUrl); } const url = new URL(imageUrl); - if (!url.hostname) { + const wikidataFileUrl = imageUrl.startsWith("File:"); + if (!url.hostname && !wikidataFileUrl) { return null; } - if (!url.hostname.includes("upload.wikimedia")) { + if (!url.hostname.includes("upload.wikimedia") && !wikidataFileUrl) { const imageAttribution = { author: url.origin, url: url.origin @@ -39,21 +41,37 @@ export class ImageAttributionService { return imageAttribution; } - const imageName = imageUrl.split("/").pop(); - const address = `https://commons.wikimedia.org/w/api.php?action=query&prop=imageinfo&iiprop=extmetadata&format=json&origin=*` + - `&titles=File:${imageName}`; + const imageName = imageUrl.split("/").pop().replace(/^File:/, ""); + let wikiPrefix = "https://commons.wikimedia.org/"; + const languageMatch = imageUrl.match(/https:\/\/upload\.wikimedia\.org\/wikipedia\/(.*?)\//); + if (languageMatch && languageMatch[1] !== "commons") { + wikiPrefix = `https://${languageMatch[1]}.wikipedia.org/`; + } + const address = `${wikiPrefix}w/api.php?action=query&prop=imageinfo&iiprop=extmetadata&format=json&origin=*&titles=File:${imageName}`; try { - const response: any = await firstValueFrom(this.httpClient.get(address).pipe(timeout(3000))); - const extmetadata = response.query.pages[Object.keys(response.query.pages)[0]].imageinfo[0].extmetadata; - if (extmetadata?.Artist.value) { - const author = this.extractPlainText(extmetadata.Artist.value as string); + const response = await firstValueFrom(this.httpClient.get(address).pipe(timeout(3000))) as unknown as WikiPage; + const pagesIds = Object.keys(response.query.pages); + if (pagesIds.length === 0) { + return null; + } + const extmetadata = response.query.pages[pagesIds[0]].imageinfo[0].extmetadata; + const attribution = extmetadata?.Artist?.value || extmetadata?.Attribution?.value; + if (attribution) { + const author = this.extractPlainText(attribution); const imageAttribution = { author, - url: `https://commons.wikimedia.org/wiki/File:${imageName}` + url: `${wikiPrefix}wiki/File:${imageName}` }; this.attributionImageCache.set(imageUrl, imageAttribution); return imageAttribution; } + const licenseLower = extmetadata?.LicenseShortName?.value.toLowerCase() || ""; + if ((licenseLower.includes("cc") && !licenseLower.includes("nc")) || licenseLower.includes("public domain")) { + return { + author: "Unknown", + url: `${wikiPrefix}wiki/File:${imageName}` + }; + } } catch {} // eslint-disable-line return null; } diff --git a/IsraelHiking.Web/src/application/services/poi.service.ts b/IsraelHiking.Web/src/application/services/poi.service.ts index ebbf4273..68c6f818 100644 --- a/IsraelHiking.Web/src/application/services/poi.service.ts +++ b/IsraelHiking.Web/src/application/services/poi.service.ts @@ -771,7 +771,7 @@ export class PoiService { let imagesUrls = Object.keys(feature.properties) .filter(k => k.startsWith("image")) .map(k => feature.properties[k]) - .filter(u => u.includes("wikimedia.org") || u.includes("inature.info") || u.includes("nakeb.co.il") || u.includes("jeepolog.com")); + .filter((u: string) => u.startsWith("File:") || u.includes("wikimedia.org") || u.includes("inature.info") || u.includes("nakeb.co.il") || u.includes("jeepolog.com")); const imageAttributions = await Promise.all(imagesUrls.map(u => this.imageAttributinoService.getAttributionForImage(u))); imagesUrls = imagesUrls.filter((_, i) => imageAttributions[i] != null); return { diff --git a/IsraelHiking.Web/src/application/services/wikidata.service.ts b/IsraelHiking.Web/src/application/services/wikidata.service.ts index 6d480709..083425ad 100644 --- a/IsraelHiking.Web/src/application/services/wikidata.service.ts +++ b/IsraelHiking.Web/src/application/services/wikidata.service.ts @@ -10,13 +10,29 @@ type WikiDataPage = { statements: { [key: string]: { value: { content: any } }[] }; } -type WikipediaPage = { +export type WikiPage = { query: { pages: { [key: string]: { - extract: string + extract: string, + original?: { + source: string + }; + imageinfo: { + extmetadata: { + Artist?: { + value: string; + }; + Attribution?: { + value: string; + }; + LicenseShortName?: { + value: string; + } + }; + }[]; } - } + }, } } @@ -73,10 +89,15 @@ export class WikidataService { const indexString = GeoJSONUtils.setProperty(feature, "website", `https://${language}.wikipedia.org/wiki/${title}`); feature.properties["poiSourceImageUrl" + indexString] = "https://upload.wikimedia.org/wikipedia/en/thumb/8/80/Wikipedia-logo-v2.svg/128px-Wikipedia-logo-v2.svg.png"; } - const wikipediaPage = await firstValueFrom(this.httpClient.get(`https://${language}.wikipedia.org/w/api.php?format=json&action=query&prop=extracts&exintro=&explaintext=&titles=${title}&origin=*`).pipe(timeout(3000))) as unknown as WikipediaPage; + const wikipediaPage = await firstValueFrom(this.httpClient.get(`https://${language}.wikipedia.org/w/api.php?format=json&action=query&prop=extracts|pageimages&piprop=original&exintro=&explaintext=&titles=${title}&origin=*`).pipe(timeout(3000))) as unknown as WikiPage; const pagesIds = Object.keys(wikipediaPage.query.pages); - if (pagesIds.length > 0) { - feature.properties.poiExternalDescription = wikipediaPage.query.pages[pagesIds[0]].extract; + if (pagesIds.length === 0) { + return; + } + const page = wikipediaPage.query.pages[pagesIds[0]]; + feature.properties.poiExternalDescription = page.extract; + if (page.original?.source) { + GeoJSONUtils.setProperty(feature, "image", page.original.source); } }