diff --git a/core/feed/annotator/base.py b/core/feed/annotator/base.py index d100cdc2d4..8c7577e8ab 100644 --- a/core/feed/annotator/base.py +++ b/core/feed/annotator/base.py @@ -304,6 +304,9 @@ def annotate_work_entry( if edition.series: computed.series = self.series(edition.series, edition.series_position) + if edition.duration is not None: + computed.duration = float(edition.duration) + content = self.content(work) if content: computed.summary = FeedEntryType(text=content) diff --git a/core/feed/serializer/opds.py b/core/feed/serializer/opds.py index 97f3ac2e39..0f1388a876 100644 --- a/core/feed/serializer/opds.py +++ b/core/feed/serializer/opds.py @@ -153,6 +153,10 @@ def serialize_work_entry(self, feed_entry: WorkEntryData) -> etree._Element: feed_entry.subtitle.text, ) ) + if feed_entry.duration is not None: + entry.append( + OPDSFeed.E(f"{{{OPDSFeed.ATOM_NS}}}duration", str(feed_entry.duration)) + ) if feed_entry.summary: entry.append(OPDSFeed.E("summary", feed_entry.summary.text)) if feed_entry.pwid: diff --git a/core/feed/serializer/opds2.py b/core/feed/serializer/opds2.py index fca7c93a6b..91fe915cb2 100644 --- a/core/feed/serializer/opds2.py +++ b/core/feed/serializer/opds2.py @@ -72,6 +72,8 @@ def serialize_work_entry(self, data: WorkEntryData) -> Dict[str, Any]: metadata["title"] = data.title.text if data.sort_title: metadata["sortAs"] = data.sort_title.text + if data.duration is not None: + metadata["duration"] = data.duration if data.subtitle: metadata["subtitle"] = data.subtitle.text diff --git a/core/feed/types.py b/core/feed/types.py index f81b70fb36..cdf5207bd5 100644 --- a/core/feed/types.py +++ b/core/feed/types.py @@ -150,6 +150,7 @@ class WorkEntryData(BaseModel): identifier: Optional[str] = None pwid: Optional[str] = None issued: Optional[datetime | date] = None + duration: Optional[float] = None summary: Optional[FeedEntryType] = None language: Optional[FeedEntryType] = None diff --git a/tests/api/feed/test_opds2_serializer.py b/tests/api/feed/test_opds2_serializer.py index 506a21d961..9619268028 100644 --- a/tests/api/feed/test_opds2_serializer.py +++ b/tests/api/feed/test_opds2_serializer.py @@ -82,6 +82,7 @@ def test_serialize_work_entry(self): Acquisition(href="http://acquisition", rel="acquisition-rel") ], other_links=[Link(href="http://link", rel="rel")], + duration=10, ) serializer = OPDS2Serializer() @@ -92,6 +93,7 @@ def test_serialize_work_entry(self): assert metadata["@type"] == data.additionalType assert metadata["title"] == data.title.text assert metadata["sortAs"] == data.sort_title.text + assert metadata["duration"] == data.duration assert metadata["subtitle"] == data.subtitle.text assert metadata["identifier"] == data.identifier assert metadata["language"] == data.language.text diff --git a/tests/api/feed/test_opds_serializer.py b/tests/api/feed/test_opds_serializer.py index 142d406e7a..1b506d4423 100644 --- a/tests/api/feed/test_opds_serializer.py +++ b/tests/api/feed/test_opds_serializer.py @@ -155,6 +155,7 @@ def test_serialize_work_entry(self): FeedEntryType.create(scheme="scheme", term="term", label="label") ], ratings=[FeedEntryType(text="rating")], + duration=10, ) element = OPDS1Serializer().serialize_work_entry(data) @@ -238,6 +239,10 @@ def test_serialize_work_entry(self): assert len(child) == 1 assert child[0].text == data.ratings[0].text + child = element.findall(f"{{{OPDSFeed.ATOM_NS}}}duration") + assert len(child) == 1 + assert child[0].text == "10" + def test_serialize_work_entry_empty(self): # A no-data work entry element = OPDS1Serializer().serialize_work_entry(WorkEntryData()) diff --git a/tests/core/test_metadata.py b/tests/core/test_metadata.py index 49ea2b2c6b..7d51d3f95d 100644 --- a/tests/core/test_metadata.py +++ b/tests/core/test_metadata.py @@ -668,6 +668,7 @@ def test_from_edition(self, db: DatabaseTransactionFixture): edition.primary_identifier.add_link( Hyperlink.IMAGE, "image", edition.data_source ) + edition.duration = 100.1 metadata = Metadata.from_edition(edition) # make sure the metadata and the originating edition match @@ -704,6 +705,7 @@ def test_update(self, db: DatabaseTransactionFixture): edition_old.subtitle = "old_subtitile" edition_old.series = "old_series" edition_old.series_position = 5 + edition_old.duration = 10 metadata_old = Metadata.from_edition(edition_old) edition_new, pool = db.edition(with_license_pool=True) @@ -712,6 +714,7 @@ def test_update(self, db: DatabaseTransactionFixture): edition_new.subtitle = "new_updated_subtitile" edition_new.series = "new_series" edition_new.series_position = 0 + edition_new.duration = 11 metadata_new = Metadata.from_edition(edition_new) metadata_old.update(metadata_new) @@ -720,6 +723,7 @@ def test_update(self, db: DatabaseTransactionFixture): assert metadata_old.subtitle == metadata_new.subtitle assert metadata_old.series == edition_new.series assert metadata_old.series_position == edition_new.series_position + assert metadata_old.duration == metadata_new.duration def test_apply(self, db: DatabaseTransactionFixture): edition_old, pool = db.edition(with_license_pool=True) @@ -737,6 +741,7 @@ def test_apply(self, db: DatabaseTransactionFixture): imprint="Follywood", published=datetime.date(1987, 5, 4), issued=datetime.date(1989, 4, 5), + duration=10, ) edition_new, changed = metadata.apply(edition_old, pool.collection) @@ -753,6 +758,7 @@ def test_apply(self, db: DatabaseTransactionFixture): assert edition_new.imprint == "Follywood" assert edition_new.published == datetime.date(1987, 5, 4) assert edition_new.issued == datetime.date(1989, 4, 5) + assert edition_new.duration == 10 edition_new, changed = metadata.apply(edition_new, pool.collection) assert changed == False