From 9391d2ce0179e7e74b228c431366a7fefa19a4c8 Mon Sep 17 00:00:00 2001 From: mat Date: Wed, 10 Sep 2025 22:29:17 -0400 Subject: [PATCH 1/8] Modify URI in create_collection3 to conditionally include page --- iiify/resolver.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/iiify/resolver.py b/iiify/resolver.py index 55eae7c3..0f87ed09 100644 --- a/iiify/resolver.py +++ b/iiify/resolver.py @@ -165,8 +165,8 @@ def create_collection3(identifier, domain, page=1, rows=MAX_API_LIMIT): # Get item metadata metadata = requests.get('%s/metadata/%s' % (ARCHIVE, identifier)).json() - # Used to build up URIs for the manifest - uri = f"{domain}{identifier}/collection.json" + # Write Collection uri with conditional page segment + uri = f"{domain}{identifier}{f'/{page}' if page != 1 else ''}/collection.json" config.configs['helpers.auto_fields.AutoLang'].auto_lang = "none" collection = Collection(id=uri, label=metadata["metadata"]["title"]) From 0a0df1c524b151d2787546253ae165156a28d6ff Mon Sep 17 00:00:00 2001 From: Glen Robson Date: Thu, 19 Mar 2026 21:33:31 +0000 Subject: [PATCH 2/8] Adding unit test --- tests/test_collections.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/test_collections.py b/tests/test_collections.py index 3d418bcb..2cca3481 100644 --- a/tests/test_collections.py +++ b/tests/test_collections.py @@ -46,6 +46,11 @@ def test_sanitized_summary(self): f"Expected summary to be sanitized but got: {collection['summary']['none'][0]}" ) + def test_unique_id_sub_page(self): + resp = self.test_app.get("/iiif/usgs_tx/2/collection.json?recache=True") + self.assertEqual(resp.status_code, 200) + collection = resp.json + self.assertEqual(collection['id'], "https://localhost/iiif/usgs_tx/2/collection.json", f"Expected unique ID for the second page of the results but got: {collection['id']}") if __name__ == '__main__': unittest.main() \ No newline at end of file From e28c74218d5a6fd4761e5917a781af0bc94e43ef Mon Sep 17 00:00:00 2001 From: Glen Robson Date: Thu, 30 Apr 2026 21:54:44 +0100 Subject: [PATCH 3/8] A new review was added --- tests/test_annotations.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_annotations.py b/tests/test_annotations.py index f6a300a6..1aaebf71 100644 --- a/tests/test_annotations.py +++ b/tests/test_annotations.py @@ -65,7 +65,7 @@ def test_review_annotations(self): self.assertEqual(resp.status_code, 200) self.assertEqual(annotations['@context'], "http://iiif.io/api/presentation/3/context.json","Unexpected context") self.assertEqual(annotations['type'], "AnnotationPage", "Unexpected type, expected AnnotationPage") - self.assertEqual(len(annotations['items']), 37, "Unexpected number of annotations") + self.assertEqual(len(annotations['items']), 38, "Unexpected number of annotations") ids = [] for anno in annotations['items']: self.assertEqual(anno['type'], "Annotation", "Expected type of Annotation") From 1605e7ed170e9146189c80303bf27d10b6947075 Mon Sep 17 00:00:00 2001 From: Glen Robson Date: Thu, 14 May 2026 21:58:20 +0100 Subject: [PATCH 4/8] Format has changed in the test --- tests/test_manifests.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_manifests.py b/tests/test_manifests.py index f3500604..5968f150 100644 --- a/tests/test_manifests.py +++ b/tests/test_manifests.py @@ -72,7 +72,7 @@ def test_v3_128Kbps_MP3(self): self.assertEqual(resp.status_code, 200) manifest = resp.json self.assertEqual(len(manifest['items']),12,f"Expected 12 canvases but got: {len(manifest['items'])}") - self.assertEqual("128kbps mp3".lower() in resp.text.lower(), True, f"Expected the string '128kbps mp3'") + self.assertEqual("64kbps mp3".lower() in resp.text.lower(), True, f"Expected the string '64kbps mp3'") def test_v3_aiff(self): resp = self.test_app.get("/iiif/3/julian-damian-marley-with-the-uprising-band-shoreline-81697/manifest.json") From 4fb06bea25c94ab30a656767a46cf1a0563d0f05 Mon Sep 17 00:00:00 2001 From: Glen Robson Date: Fri, 15 May 2026 01:02:09 +0100 Subject: [PATCH 5/8] Adding timeouts and retries for IIIF image server --- iiify/resolver.py | 39 ++++++++++++++++++++++++++------------ iiify/utils/http_config.py | 28 +++++++++++++++++++++++++++ 2 files changed, 55 insertions(+), 12 deletions(-) create mode 100644 iiify/utils/http_config.py diff --git a/iiify/resolver.py b/iiify/resolver.py index debcdbb2..034dca76 100644 --- a/iiify/resolver.py +++ b/iiify/resolver.py @@ -3,6 +3,7 @@ import os import requests from .configs import options, cors, approot, cache_root, media_root, apiurl, LINKS +from .utils.http_config import timeout_session from iiif_prezi3 import config, Collection, Manifest, Canvas, Annotation, AnnotationPage, CollectionRef, ManifestRef, CanvasRef, AnnotationPageRef, AnnotationPageRefExtended, AnnotationBody, ServiceV3, Choice, TextualBody, AccompanyingCanvas, Range from urllib.parse import urlparse, parse_qs, quote import json @@ -595,18 +596,31 @@ def addThumbnails(manifest, identifier, files): elif ia_thumb_files: files_to_process = ia_thumb_files - for file in files_to_process: - name = file.get("name", "") - encoded_name = quote(name.replace('/', '%2f')) - # Forward solidus before thumbnail uri must always be %2f - iiif_url = f"{IMG_SRV}/2/{identifier.strip()}%2f{encoded_name}" - try: - manifest.create_thumbnail_from_iiif(iiif_url) - except requests.HTTPError: - print(f"Failed to generate thumbnail from Cantaloupe: {iiif_url}") + MAX_IIIF_THUMB = 15 + # if we use cantaloupe for more than MAX_IIIF_THUMB + # its likely to be too slow and will fail to generate + if len(files_to_process) > MAX_IIIF_THUMB: + for file in files_to_process: + name = file.get("name", "") mimetype = "image/png" if name.endswith(".png") else "image/jpeg" static_url = f"{ARCHIVE}/download/{quote(identifier)}/{quote(name)}" manifest.add_thumbnail(static_url, format=mimetype) + + else: + for file in files_to_process: + name = file.get("name", "") + encoded_name = quote(name.replace('/', '%2f')) + # Forward solidus before thumbnail uri must always be %2f + iiif_url = f"{IMG_SRV}/2/{identifier.strip()}%2f{encoded_name}" + try: + session = timeout_session(timeout=0.2,retry=1) + print(f'Getting image {iiif_url}') + manifest.create_thumbnail_from_iiif(iiif_url, iiif_session=session) + except requests.exceptions.RequestException as e: + print(f"Failed to generate thumbnail from Cantaloupe: {iiif_url} due to {e}") + mimetype = "image/png" if name.endswith(".png") else "image/jpeg" + static_url = f"{ARCHIVE}/download/{quote(identifier)}/{quote(name)}" + manifest.add_thumbnail(static_url, format=mimetype) return def addThumbnailNav(manifest, identifier, files): @@ -755,10 +769,10 @@ def create_manifest3(identifier, domain=None, page=None): manifest = Manifest(id=f"{uri}/manifest.json", label=metadata["metadata"]["title"]) if 'reviews' in metadata: - reviews_as_annotations = AnnotationPageRef(__root__=AnnotationPageRefExtended( + reviews_as_annotations = AnnotationPageRefExtended( id=f"{domain.replace('iiif/', 'iiif/3/annotations/')}{identifier}/comments.json", type="AnnotationPage", - )) + ) manifest.annotations=[reviews_as_annotations] addMetadata(manifest, identifier, metadata['metadata']) addSeeAlso(manifest, identifier, metadata['files']) @@ -1116,7 +1130,8 @@ def create_manifest3(identifier, domain=None, page=None): id=f"{URI_PRIFIX}/{identifier}${pageCount}/canvas", label=f"{file['name']}", anno_page_id=f"{URI_PRIFIX}/{identifier}/{slugged_id}/page", - anno_id=f"{URI_PRIFIX}/{identifier}/{slugged_id}/annotation" + anno_id=f"{URI_PRIFIX}/{identifier}/{slugged_id}/annotation", + iiif_session=timeout_session(retry=2) ) elif mediatype == "collection": raise IsCollection diff --git a/iiify/utils/http_config.py b/iiify/utils/http_config.py new file mode 100644 index 00000000..be5a602f --- /dev/null +++ b/iiify/utils/http_config.py @@ -0,0 +1,28 @@ + +import requests +from urllib3.util import Retry +from requests.adapters import HTTPAdapter + +class TimeoutHTTPAdapter(HTTPAdapter): + def __init__(self, *args, timeout=5, **kwargs): + self.timeout = timeout + super().__init__(*args, **kwargs) + + def send(self, request, **kwargs): + if kwargs.get('timeout') is None: + kwargs['timeout'] = self.timeout + return super().send(request, **kwargs) + +def timeout_session(timeout=1, retry=1): + session = requests.Session() + retries = Retry( + total=retry, + backoff_factor=0.1, + status_forcelist=[502, 503, 504], + ) + adapter = TimeoutHTTPAdapter(max_retries=retries,timeout=timeout) + + session.mount('http://', adapter) + session.mount('https://', adapter) + + return session \ No newline at end of file From d4f091f3e224afb7e5009cd87d53dbe5dc86336d Mon Sep 17 00:00:00 2001 From: Glen Robson Date: Sat, 23 May 2026 01:04:10 +0100 Subject: [PATCH 6/8] Increasing timeout --- iiify/resolver.py | 7 +++++-- tests/test_video.py | 2 +- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/iiify/resolver.py b/iiify/resolver.py index 034dca76..ffdb4459 100644 --- a/iiify/resolver.py +++ b/iiify/resolver.py @@ -601,6 +601,7 @@ def addThumbnails(manifest, identifier, files): # its likely to be too slow and will fail to generate if len(files_to_process) > MAX_IIIF_THUMB: for file in files_to_process: + # print (f"Getting static thumbnail for {file['name']}") name = file.get("name", "") mimetype = "image/png" if name.endswith(".png") else "image/jpeg" static_url = f"{ARCHIVE}/download/{quote(identifier)}/{quote(name)}" @@ -614,7 +615,7 @@ def addThumbnails(manifest, identifier, files): iiif_url = f"{IMG_SRV}/2/{identifier.strip()}%2f{encoded_name}" try: session = timeout_session(timeout=0.2,retry=1) - print(f'Getting image {iiif_url}') + # print(f'Getting image {iiif_url}') manifest.create_thumbnail_from_iiif(iiif_url, iiif_session=session) except requests.exceptions.RequestException as e: print(f"Failed to generate thumbnail from Cantaloupe: {iiif_url} due to {e}") @@ -1120,6 +1121,7 @@ def create_manifest3(identifier, domain=None, page=None): if imgs: pageCount = video_count for file in imgs: + # print (f"Making canvas for {file['name']}") imgId = f"{identifier}/{file['name']}".replace('/','%2f') imgURL = f"{IMG_SRV}/3/{imgId}" pageCount += 1 @@ -1131,7 +1133,8 @@ def create_manifest3(identifier, domain=None, page=None): label=f"{file['name']}", anno_page_id=f"{URI_PRIFIX}/{identifier}/{slugged_id}/page", anno_id=f"{URI_PRIFIX}/{identifier}/{slugged_id}/annotation", - iiif_session=timeout_session(retry=2) + iiif_session=timeout_session(retry=2, timeout=3 + ) ) elif mediatype == "collection": raise IsCollection diff --git a/tests/test_video.py b/tests/test_video.py index 120be6c0..f27da8c7 100644 --- a/tests/test_video.py +++ b/tests/test_video.py @@ -22,7 +22,7 @@ def test_v3_h264_MPEG4_OGG_Theora(self): resp = self.test_app.get("/iiif/3/taboca_201002_03/manifest.json") self.assertEqual(resp.status_code, 200) manifest = resp.json - self.assertEqual(len(manifest['items']),251,f"Expected 251 canvases but got: {len(manifest['items'])}") + self.assertEqual(len(manifest['items']),503,f"Expected 503 canvases but got: {len(manifest['items'])}") self.assertEqual("h.264 MPEG4".lower() in resp.text.lower(), True, f"Expected the string 'h.264 MPEG4'") self.assertEqual("OGG Theora".lower() in resp.text.lower(), True, f"Expected the string 'OGG Theora'") From 7036de5997e56c8195e9ef4661436db603cf4639 Mon Sep 17 00:00:00 2001 From: Glen Robson Date: Sat, 23 May 2026 01:19:32 +0100 Subject: [PATCH 7/8] Fixing test. Object now has two canvases, 1 video 1 image --- iiify/resolver.py | 2 +- tests/test_video.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/iiify/resolver.py b/iiify/resolver.py index ffdb4459..066702f8 100644 --- a/iiify/resolver.py +++ b/iiify/resolver.py @@ -614,7 +614,7 @@ def addThumbnails(manifest, identifier, files): # Forward solidus before thumbnail uri must always be %2f iiif_url = f"{IMG_SRV}/2/{identifier.strip()}%2f{encoded_name}" try: - session = timeout_session(timeout=0.2,retry=1) + session = timeout_session(timeout=1,retry=1) # print(f'Getting image {iiif_url}') manifest.create_thumbnail_from_iiif(iiif_url, iiif_session=session) except requests.exceptions.RequestException as e: diff --git a/tests/test_video.py b/tests/test_video.py index f27da8c7..217a3839 100644 --- a/tests/test_video.py +++ b/tests/test_video.py @@ -16,7 +16,7 @@ def test_v3_single_video_manifest(self): self.assertEqual(resp.status_code, 200) manifest = resp.json - self.assertEqual(len(manifest['items']),1,f"Expected 1 canvas but got: {len(manifest['items'])}") + self.assertEqual(len(manifest['items']),2,f"Expected 2 canvas but got: {len(manifest['items'])}") def test_v3_h264_MPEG4_OGG_Theora(self): resp = self.test_app.get("/iiif/3/taboca_201002_03/manifest.json") From 7394ad83a1e60fde46ec51fe70d3c52d02569bf5 Mon Sep 17 00:00:00 2001 From: Glen Robson Date: Sat, 23 May 2026 01:25:26 +0100 Subject: [PATCH 8/8] Manifest now has 2 canvases --- tests/test_video.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_video.py b/tests/test_video.py index 217a3839..473e85aa 100644 --- a/tests/test_video.py +++ b/tests/test_video.py @@ -31,7 +31,7 @@ def test_vtt_autogenerated(self): self.assertEqual(resp.status_code, 200) manifest = resp.json - self.assertEqual(len(manifest['items']),1,f"Expected 1 canvas but got: {len(manifest['items'])}") + self.assertEqual(len(manifest['items']),2,f"Expected 2 canvas but got: {len(manifest['items'])}") self.assertTrue('annotations' in manifest['items'][0], "Expected annotations in manifest") self.assertTrue(isinstance(manifest['items'][0]['annotations'], list), "Expected annotations to be a list") self.assertEqual(len(manifest['items'][0]['annotations']), 1, "Expected 1 item in annotations")