Skip to content
Open
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ Feedsmith aims to fully support all major feed formats and namespaces in complet
| [Source](https://feedsmith.dev/reference/namespaces/source) | `<source:*>` | RSS | ✅ | ✅ |
| [blogChannel](https://feedsmith.dev/reference/namespaces/blogchannel) | `<blogChannel:*>` | RSS | ✅ | ✅ |
| [YouTube](https://feedsmith.dev/reference/namespaces/yt) | `<yt:*>` | Atom | ✅ | ✅ |
| [OPDS](https://feedsmith.dev/reference/namespaces/opds) | `<opds:*>` | Atom | ✅ | ✅ |
| [W3C Basic Geo](https://feedsmith.dev/reference/namespaces/geo) | `<geo:*>` | RSS, Atom | ✅ | ✅ |
| [GeoRSS Simple](https://feedsmith.dev/reference/namespaces/georss) | `<georss:*>` | RSS, Atom, RDF | ✅ | ✅ |
| [RDF](https://feedsmith.dev/reference/namespaces/rdf) | `<rdf:*>` | RDF | ✅ | ✅ |
Expand Down
1 change: 1 addition & 0 deletions docs/.vitepress/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,7 @@ export default defineConfig({
{ text: 'Source', link: '/reference/namespaces/source' },
{ text: 'blogChannel', link: '/reference/namespaces/blogchannel' },
{ text: 'YouTube', link: '/reference/namespaces/yt' },
{ text: 'OPDS', link: '/reference/namespaces/opds' },
{ text: 'W3C Basic Geo', link: '/reference/namespaces/geo' },
{ text: 'GeoRSS Simple', link: '/reference/namespaces/georss' },
{ text: 'RDF', link: '/reference/namespaces/rdf' },
Expand Down
1 change: 1 addition & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ Feedsmith aims to fully support all major feed formats and namespaces in complet
| [Source](/reference/namespaces/source) | `<source:*>` | RSS | ✅ | ✅ |
| [blogChannel](/reference/namespaces/blogchannel) | `<blogChannel:*>` | RSS | ✅ | ✅ |
| [YouTube](/reference/namespaces/yt) | `<yt:*>` | Atom | ✅ | ✅ |
| [OPDS](/reference/namespaces/opds) | `<opds:*>` | Atom | ✅ | ✅ |
| [W3C Basic Geo](/reference/namespaces/geo) | `<geo:*>` | RSS, Atom | ✅ | ✅ |
| [GeoRSS Simple](/reference/namespaces/georss) | `<georss:*>` | RSS, Atom, RDF | ✅ | ✅ |
| [RDF](/reference/namespaces/rdf) | `<rdf:*>` | RDF | ✅ | ✅ |
Expand Down
1 change: 1 addition & 0 deletions docs/reference/feeds/atom.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ Atom is a syndication format based on XML that provides a robust framework for w
<a href="/reference/namespaces/pingback">Pingback</a>,
<a href="/reference/namespaces/trackback">Trackback</a>,
<a href="/reference/namespaces/yt">YouTube</a>,
<a href="/reference/namespaces/opds">OPDS</a>,
<a href="/reference/namespaces/geo">W3C Basic Geo</a>,
<a href="/reference/namespaces/georss">GeoRSS Simple</a>
</td>
Expand Down
36 changes: 36 additions & 0 deletions docs/reference/namespaces/opds.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# OPDS Namespace Reference

The OPDS (Open Publication Distribution System) namespace extends Atom links with catalog-specific metadata for digital publication distribution, including pricing, acquisition methods, and faceted navigation.

<table>
<tbody>
<tr>
<th>Namespace URI</th>
<td><code>http://opds-spec.org/2010/catalog</code></td>
</tr>
<tr>
<th>Specification</th>
<td><a href="https://specs.opds.io/opds-1.2" target="_blank">OPDS Catalog 1.2</a></td>
</tr>
<tr>
<th>Prefix</th>
<td><code>&lt;opds:*&gt;</code></td>
</tr>
<tr>
<th>Available in</th>
<td><a href="/reference/feeds/atom">Atom</a></td>
</tr>
<tr>
<th>Property</th>
<td><code>opds</code> (on <code>Link</code>)</td>
</tr>
</tbody>
</table>

## Types

<<< @/../src/namespaces/opds/common/types.ts#reference

## Related

- **[Parsing Namespaces](/parsing/namespaces)** - How namespace parsing works
6 changes: 6 additions & 0 deletions src/common/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,10 @@ import {
stopNodes as mediaStopNodes,
uris as mediaUris,
} from '../namespaces/media/common/config.js'
import {
stopNodes as opdsStopNodes,
uris as opdsUris,
} from '../namespaces/opds/common/config.js'
import {
stopNodes as opensearchStopNodes,
uris as opensearchUris,
Expand Down Expand Up @@ -165,6 +169,7 @@ export const namespaceUris = {
trackback: trackbackUris,
prism: prismUris,
acast: acastUris,
opds: opdsUris,
}

export const namespacePrefixes = Object.entries(namespaceUris).reduce(
Expand Down Expand Up @@ -196,6 +201,7 @@ export const namespaceStopNodes = [
...googleplayStopNodes,
...itunesStopNodes,
...mediaStopNodes,
...opdsStopNodes,
...opensearchStopNodes,
...pingbackStopNodes,
...podcastStopNodes,
Expand Down
2 changes: 2 additions & 0 deletions src/feeds/atom/common/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import type { GeoRssNs } from '../../../namespaces/georss/common/types.js'
import type { GooglePlayNs } from '../../../namespaces/googleplay/common/types.js'
import type { ItunesNs } from '../../../namespaces/itunes/common/types.js'
import type { MediaNs } from '../../../namespaces/media/common/types.js'
import type { OpdsNs } from '../../../namespaces/opds/common/types.js'
import type { OpenSearchNs } from '../../../namespaces/opensearch/common/types.js'
import type { PingbackNs } from '../../../namespaces/pingback/common/types.js'
import type { PscNs } from '../../../namespaces/psc/common/types.js'
Expand Down Expand Up @@ -48,6 +49,7 @@ export namespace Atom {
title?: string
length?: number
thr?: ThrNs.Link<TDate>
opds?: OpdsNs.Link<TDate>
}

export type Person = {
Expand Down
184 changes: 184 additions & 0 deletions src/feeds/atom/generate/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1363,6 +1363,190 @@ describe('generate with lenient mode', () => {
</app:control>
</entry>
</feed>
`

expect(generate(value)).toEqual(expected)
})

it('should generate Atom feed with OPDS catalog entry', () => {
const value = {
id: 'urn:uuid:example-catalog',
title: 'Example OPDS Catalog',
updated: new Date('2024-01-15T12:00:00Z'),
entries: [
{
id: 'urn:isbn:9780000000001',
title: 'Example Book',
updated: new Date('2024-01-15T12:00:00Z'),
links: [
{
href: 'https://example.com/book.epub',
rel: 'http://opds-spec.org/acquisition/buy',
type: 'application/epub+zip',
opds: {
prices: [{ value: 9.99, currencyCode: 'USD' }],
},
},
{
href: 'https://example.com/cover.jpg',
rel: 'http://opds-spec.org/image',
type: 'image/jpeg',
},
],
},
],
}
const expected = `<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom" xmlns:opds="http://opds-spec.org/2010/catalog">
<id>urn:uuid:example-catalog</id>
<title>Example OPDS Catalog</title>
<updated>2024-01-15T12:00:00.000Z</updated>
<entry>
<id>urn:isbn:9780000000001</id>
<link href="https://example.com/book.epub" rel="http://opds-spec.org/acquisition/buy" type="application/epub+zip">
<opds:price currencycode="USD">9.99</opds:price>
</link>
<link href="https://example.com/cover.jpg" rel="http://opds-spec.org/image" type="image/jpeg"/>
<title>Example Book</title>
<updated>2024-01-15T12:00:00.000Z</updated>
</entry>
</feed>
`

expect(generate(value)).toEqual(expected)
})

it('should generate Atom feed with OPDS faceted navigation', () => {
const value = {
id: 'urn:uuid:catalog-facets',
title: 'Catalog with Facets',
updated: new Date('2024-01-15T12:00:00Z'),
links: [
{
href: 'https://example.com/catalog?sort=author',
rel: 'http://opds-spec.org/facet',
opds: {
facetGroup: 'Sort',
activeFacet: true,
},
},
{
href: 'https://example.com/catalog?sort=title',
rel: 'http://opds-spec.org/facet',
opds: {
facetGroup: 'Sort',
activeFacet: false,
},
},
],
}
const expected = `<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom" xmlns:opds="http://opds-spec.org/2010/catalog">
<id>urn:uuid:catalog-facets</id>
<link href="https://example.com/catalog?sort=author" rel="http://opds-spec.org/facet" opds:facetGroup="Sort" opds:activeFacet="true"/>
<link href="https://example.com/catalog?sort=title" rel="http://opds-spec.org/facet" opds:facetGroup="Sort" opds:activeFacet="false"/>
<title>Catalog with Facets</title>
<updated>2024-01-15T12:00:00.000Z</updated>
</feed>
`

expect(generate(value)).toEqual(expected)
})

it('should generate Atom feed with OPDS library lending extensions', () => {
const value = {
id: 'urn:uuid:library-catalog',
title: 'Library Catalog',
updated: new Date('2024-01-15T12:00:00Z'),
entries: [
{
id: 'urn:isbn:9780000000003',
title: 'Borrowable Book',
updated: new Date('2024-01-15T12:00:00Z'),
links: [
{
href: 'https://example.com/borrow',
rel: 'http://opds-spec.org/acquisition/borrow',
type: 'application/atom+xml;type=entry;profile=opds-catalog',
opds: {
availability: {
status: 'unavailable',
since: new Date('2024-01-01T00:00:00Z'),
until: new Date('2024-06-30T23:59:59Z'),
},
holds: { total: 5, position: 2 },
copies: { total: 3, available: 1 },
},
},
],
},
],
}
const expected = `<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom" xmlns:opds="http://opds-spec.org/2010/catalog">
<id>urn:uuid:library-catalog</id>
<title>Library Catalog</title>
<updated>2024-01-15T12:00:00.000Z</updated>
<entry>
<id>urn:isbn:9780000000003</id>
<link href="https://example.com/borrow" rel="http://opds-spec.org/acquisition/borrow" type="application/atom+xml;type=entry;profile=opds-catalog">
<opds:availability status="unavailable" since="2024-01-01T00:00:00.000Z" until="2024-06-30T23:59:59.000Z"/>
<opds:holds total="5" position="2"/>
<opds:copies total="3" available="1"/>
</link>
<title>Borrowable Book</title>
<updated>2024-01-15T12:00:00.000Z</updated>
</entry>
</feed>
`

expect(generate(value)).toEqual(expected)
})

it('should generate Atom feed with OPDS indirect acquisition', () => {
const value = {
id: 'urn:uuid:catalog-indirect',
title: 'Catalog with Indirect Acquisition',
updated: new Date('2024-01-15T12:00:00Z'),
entries: [
{
id: 'urn:isbn:9780000000002',
title: 'Book via Checkout',
updated: new Date('2024-01-15T12:00:00Z'),
links: [
{
href: 'https://example.com/checkout',
rel: 'http://opds-spec.org/acquisition',
type: 'text/html',
opds: {
indirectAcquisitions: [
{
type: 'application/epub+zip',
indirectAcquisitions: [{ type: 'application/x-mobipocket-ebook' }],
},
],
},
},
],
},
],
}
const expected = `<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom" xmlns:opds="http://opds-spec.org/2010/catalog">
<id>urn:uuid:catalog-indirect</id>
<title>Catalog with Indirect Acquisition</title>
<updated>2024-01-15T12:00:00.000Z</updated>
<entry>
<id>urn:isbn:9780000000002</id>
<link href="https://example.com/checkout" rel="http://opds-spec.org/acquisition" type="text/html">
<opds:indirectAcquisition type="application/epub+zip">
<opds:indirectAcquisition type="application/x-mobipocket-ebook"/>
</opds:indirectAcquisition>
</link>
<title>Book via Checkout</title>
<updated>2024-01-15T12:00:00.000Z</updated>
</entry>
</feed>
`

expect(generate(value)).toEqual(expected)
Expand Down
Loading
Loading