{"id":22547088,"url":"https://github.com/kiwilan/php-opds","last_synced_at":"2025-08-23T15:09:23.979Z","repository":{"id":162928498,"uuid":"637908729","full_name":"kiwilan/php-opds","owner":"kiwilan","description":"PHP package to create OPDS feed (Open Publication Distribution System) for eBooks.","archived":false,"fork":false,"pushed_at":"2025-06-11T08:12:50.000Z","size":460,"stargazers_count":9,"open_issues_count":2,"forks_count":2,"subscribers_count":0,"default_branch":"main","last_synced_at":"2025-06-11T09:15:20.985Z","etag":null,"topics":["api","atom","book","ebook","opds","php","rss"],"latest_commit_sha":null,"homepage":"https://packagist.org/packages/kiwilan/php-opds","language":"PHP","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/kiwilan.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":".github/FUNDING.yml","license":"LICENSE.md","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null},"funding":{"github":"kiwilan"}},"created_at":"2023-05-08T17:05:43.000Z","updated_at":"2025-06-11T08:12:54.000Z","dependencies_parsed_at":null,"dependency_job_id":"b0d18b58-ac24-47b8-8a3a-1cda7f9826a5","html_url":"https://github.com/kiwilan/php-opds","commit_stats":null,"previous_names":[],"tags_count":28,"template":false,"template_full_name":"spatie/package-skeleton-php","purl":"pkg:github/kiwilan/php-opds","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kiwilan%2Fphp-opds","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kiwilan%2Fphp-opds/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kiwilan%2Fphp-opds/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kiwilan%2Fphp-opds/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/kiwilan","download_url":"https://codeload.github.com/kiwilan/php-opds/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kiwilan%2Fphp-opds/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":265083538,"owners_count":23708736,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["api","atom","book","ebook","opds","php","rss"],"created_at":"2024-12-07T15:10:06.811Z","updated_at":"2025-07-15T09:09:49.505Z","avatar_url":"https://github.com/kiwilan.png","language":"PHP","funding_links":["https://github.com/sponsors/kiwilan"],"categories":[],"sub_categories":[],"readme":"# PHP OPDS\n\n![Banner with woman with eReader picture in background and PHP OPDS title](https://raw.githubusercontent.com/kiwilan/php-opds/main/docs/banner.jpg)\n\n[![php][php-version-src]][php-version-href]\n[![version][version-src]][version-href]\n[![downloads][downloads-src]][downloads-href]\n[![license][license-src]][license-href]\n[![tests][tests-src]][tests-href]\n[![codecov][codecov-src]][codecov-href]\n\nPHP package to create [OPDS feed](https://opds.io/) (Open Publication Distribution System) for eBooks.\n\n-   **Demo**: \u003chttps://bookshelves.ink/opds\u003e from [`bookshelves-project/bookshelves`](https://github.com/bookshelves-project/bookshelves)\n\n| Version | Supported |       Date        | Format | Query param |\n| :-----: | :-------: | :---------------: | :----: | :---------: |\n|   1.2   |    ✅     | November 11, 2018 |  XML   |  `?v=1.2`   |\n|   2.0   |    ✅     |       Draft       |  JSON  |  `?v=2.0`   |\n\nAll old versions: 0.9, 1.0 and 1.1 have a fallback to OPDS 1.2.\n\n## Requirements\n\n-   `php` v8.1 minimum\n\n## About\n\nOPDS is like RSS feeds but adapted for eBooks, it's a standard to share eBooks between libraries, bookstores, publishers, and readers. Developed by [Hadrien Gardeur](https://github.com/HadrienGardeur) and [Leonard Richardson](https://github.com/leonardr).\n\nThis package has been created to be used with [`bookshelves-project/bookshelves`](https://github.com/bookshelves-project/bookshelves), an open source eBook web app.\n\n\u003e [!NOTE]\n\u003e The Open Publication Distribution System (OPDS) catalog format is a syndication format for electronic publications based on Atom and HTTP. OPDS catalogs enable the aggregation, distribution, discovery, and acquisition of electronic publications. OPDS catalogs use existing or emergent open standards and conventions, with a priority on simplicity.\n\u003e\n\u003e The Open Publication Distribution System specification is prepared by an informal grouping of partners, combining Internet Archive, O'Reilly Media, Feedbooks, OLPC, and others.\n\u003e\n\u003e From [Wikipedia](https://en.wikipedia.org/wiki/Open_Publication_Distribution_System)\n\nSome resources about OPDS and eBooks:\n\n-   [opds.io](https://opds.io/): OPDS official website\n-   OPDS feeds examples\n    -   [bookshelves.ink](https://bookshelves.ink/opds): Bookshelves (eBook web app, which use `kiwilan/php-opds`)\n    -   [gallica.bnf.fr](https://gallica.bnf.fr/opds): Gallica (French National Library)\n    -   [cops-demo.slucas.fr](https://cops-demo.slucas.fr/feed.php): COPS (OPDS PHP Server)\n    -   [feedbooks.com](https://catalog.feedbooks.com/catalog/public_domain.atom): Feedbooks\n-   [`kiwilan/php-ebook`](https://github.com/kiwilan/php-ebook): PHP package to handle eBook\n-   [`koreader/koreader`](https://github.com/koreader/koreader): eBook reader for Android, iOS, Kindle, Kobo, Linux, macOS, Windows, and more. If your eReader can't use OPDS feeds, you can install KOReader on it\n-   [`edrlab/thorium-reader`](https://github.com/edrlab/thorium-reader): A cross platform desktop reading app, based on the Readium Desktop toolkit. You can use it to use OPDS feeds and read eBooks\n\n## Features\n\n-   ⚛️ Generate OPDS XML and JSON feed (navigation feeds and acquisition feeds)\n-   👌 Support OPDS 1.2 and 2.0\n-   🔖 With pagination option\n-   🔍 Search page included, but NOT search engine\n-   🌐 Option to handle response to browser as XML or JSON\n\n### Roadmap\n\n-   [ ] OPDS 1.2: support advanced acquisition feeds\n-   [ ] OPDS 2.0: support `Facets`, `Groups`, advanced `belongsTo`\n-   [ ] Add [OPDS Page Streaming Extension](https://github.com/anansi-project/opds-pse) from `anansi-project`\n\n## Installation\n\nYou can install the package via composer:\n\n```bash\ncomposer require kiwilan/php-opds\n```\n\n## Usage\n\nYou have to use `Opds::make()` method to create an OPDS instance, the only param is `config` to set OPDS config, totally optional. Default response is XML with OPDS version 1.2, you can force JSON response with `OpdsConfig::class` method `forceJson()` to use only OPDS 2.0. With `get()` method, you can get full instance of `Opds` with `OpdsEngine` and `OpdsResponse`.\n\n```php\nuse Kiwilan\\Opds\\Opds;\nuse Kiwilan\\Opds\\OpdsConfig;\n\n$opds = Opds::make(new OpdsConfig()) // OpdsConfig::class, optional\n  -\u003etitle('My feed')\n  -\u003efeeds([...]) // OpdsEntryNavigation[]|OpdsEntryBook[]|OpdsEntryNavigation|OpdsEntryBook\n  -\u003eget()\n;\n```\n\nYou have different informations into `Opds::class`.\n\nSome informations about OPDS instance:\n\n```php\nuse Kiwilan\\Opds\\Opds;\n\n$opds = Opds::make()\n  -\u003etitle('My feed')\n  -\u003efeeds([...])\n  -\u003eget()\n;\n\n$opds-\u003egetConfig(); // OpdsConfig - Configuration used to create OPDS feed set into `make()` method\n$opds-\u003egetUrl(); // string|null - Current URL, generated automatically but can be overrided with `url()` method\n$opds-\u003egetTitle(); // string - Title of OPDS feed set with `title()` method\n$opds-\u003egetVersion(); // OpdsVersionEnum - OPDS version used, determined by query parameter `v` or `OpdsConfig::class` method `forceJson()`\n$opds-\u003egetQueryVersion(); // OpdsVersionEnum|null - Name of query parameter used to set OPDS version, default is `v`\n$opds-\u003egetUrlParts(); // array - URL parts, determined from `url`\n$opds-\u003egetQuery(); // array - Query parameters, determined from `url`\n$opds-\u003egetFeeds(); // array - Feeds set with `feeds()` method\n$opds-\u003echeckIfSearch(); // bool, default is false, set to true if `isSearch()` method is used\n```\n\nAnd about engine and response:\n\n```php\nuse Kiwilan\\Opds\\Opds;\n\n$opds = Opds::make()\n  -\u003etitle('My feed')\n  -\u003efeeds([...])\n  -\u003eget()\n;\n\n$opds-\u003egetEngine(); // OpdsEngine|null - Engine used to create OPDS feed, determined by OPDS version, can be `OpdsXmlEngine::class` or `OpdsJsonEngine::class`\n$opds-\u003egetOutput(); // OpdsOutputEnum|null - Output of response, useful for debug\n$opds-\u003egetPaginator(); // OpdsPaginator|OpdsPaginate|null - Paginator used to paginate feeds, if you use `paginate()` method\n$opds-\u003egetResponse(); // OpdsResponse|null - Response of OPDS feed, will use `OpdsEngine` to create a response\n```\n\n### OPDS Version\n\nYou can use query parameter `version` to set it dynamically. You could change this query into `OpdsConfig::class`.\n\n-   Version `1.2` can be set with `?v=1.2`\n-   Version `2.0` can be set with `?v=2.0`\n\n\u003e [!WARNING]\n\u003e\n\u003e If you set `v` query parameter to `1.2` with `OpdsConfig::class` method `forceJson()`, query param will be ignored.\n\n### OPDS Engine\n\nEngine will convert your feeds to OPDS, depending of OPDS version.\n\n-   [OPDS 1.2](https://specs.opds.io/opds-1.2) will use `OpdsXmlEngine::class`\n-   [OPDS 2.0](https://drafts.opds.io/opds-2.0) will use `OpdsJsonEngine::class`\n\nYou can get engine used with `getEngine()` method from `Opds::class`. Property `contents` contains array of feeds, `OpdsEngine` allow conversion into XML or JSON with `__toString()` method, the output depends of OPDS version.\n\n```php\nuse Kiwilan\\Opds\\Opds;\n\n$opds = Opds::make()\n  -\u003etitle('My feed')\n  -\u003efeeds([...])\n  -\u003eget()\n;\n\n$engine = $opds-\u003egetEngine(); // OpdsEngine\n$contents = $engine-\u003egetContents(); // array\n$output = $engine-\u003e__toString(); // string\n```\n\n### OPDS Response\n\nTo build OPDS feed, you have to `get()` method. It will return an instance of `Opds` with `OpdsEngine`, `OpdsResponse` and paginator filled.\n\n```php\nuse Kiwilan\\Opds\\Opds;\n\n$opds = Opds::make()\n  -\u003etitle('My feed')\n  -\u003efeeds([...])\n  -\u003eget() // `Opds` to fill `OpdsEngine`, `OpdsResponse` and paginator\n;\n```\n\nTo get response, you can use `getResponse()` method from `Opds::class`.\n\n```php\nuse Kiwilan\\Opds\\Opds;\n\n$opds = Opds::make()\n  -\u003etitle('My feed')\n  -\u003efeeds([...])\n  -\u003eget()\n;\n\n$response = $opds-\u003egetResponse(); // OpdsResponse\n\n$response-\u003egetStatus(); // int - Status code of response\n$response-\u003eisJson(); // bool - If response is JSON\n$response-\u003eisXml(); // bool - If response is XML\n$response-\u003egetHeaders(); // array - Headers of response\n$response-\u003egetContents(); // string - Contents of response\n```\n\n#### Send response\n\n\u003e [!NOTE]\n\u003e\n\u003e This method is totally optional, you can send response to browser by yourself.\n\nYou can send response to browser by yourself from `OpdsResponse` to get status code, headers and contents or use `send()` method available into `Opds` and `OpdsResponse`.\n\n-   You can use `send()` from `Opds` or `OpdsResponse` to send response to browser (exactly the same)\n-   You don't have to call `get()` method before `send()` method, `send()` will call `get()` automatically\n\n```php\nuse Kiwilan\\Opds\\Opds;\n\nOpds::make()\n  -\u003etitle('My feed')\n  -\u003efeeds([...])\n  -\u003esend(); // XML or JSON response\n;\n```\n\nYou can call `get()` method before `send()` method if you want to get `OpdsResponse` instance.\n\n```php\nuse Kiwilan\\Opds\\Opds;\n\n$opds = Opds::make()\n  -\u003etitle('My feed')\n  -\u003efeeds([...])\n  -\u003eget()\n;\n\n// do something with `OpdsResponse` instance\n\n$opds-\u003esend(); // XML or JSON response\n```\n\nTo get response\n\n```php\nuse Kiwilan\\Opds\\Opds;\n\n$opds = Opds::make()\n  -\u003etitle('My feed')\n  -\u003efeeds([...])\n  -\u003eget();\n\n$response = $opds-\u003egetResponse(); // OpdsResponse\n$response-\u003esend(); // XML or JSON response\n```\n\n\u003e [!NOTE]\n\u003e\n\u003e You can use `exit` parameter from `send()` method to stop script after sending response.\n\n### OPDS Config\n\nOPDS config can be set with `OpdsConfig::class`:\n\n```php\n\u003c?php\n\nuse Kiwilan\\Opds\\OpdsConfig;\n\n$config = new OpdsConfig(\n  name: 'My OPDS Catalog', // Name of OPDS feed\n  author: 'John Doe', // Author name\n  authorUrl: 'https://example.com', // Author URL\n  iconUrl: 'https://example.com/icon.png', // Icon URL\n  startUrl: 'https://example.com/opds', // Start URL, will be included in top navigation\n  searchUrl: 'https://example.com/opds/search', // Search URL, will be included in top navigation\n  versionQuery: 'v', // query parameter for version\n  paginationQuery: 'page', // query parameter for pagination\n  updated: new DateTime(), // Last update of OPDS feed\n  maxItemsPerPage: 16, // Max items per page, default is 16\n  forceJson: false, // To force JSON response as OPDS 2.0, default is false\n);\n```\n\n\u003e [!NOTE]\n\u003e\n\u003e You can override `OpdsConfig` with setter methods.\n\n#### OPDS Pagination\n\nYou can use pagination from `Opds` with `paginate()` method, it will generate pagination based on `maxItemsPerPage` property from `OpdsConfig::class`.\n\n-   If you not set any parameter, it will generate pagination\n-   If you set `OpdsPaginate` object, it will generate pagination based on it\n\n```php\nuse Kiwilan\\Opds\\Opds;\n\n$opds = Opds::make()\n  -\u003etitle('My feed')\n  -\u003efeeds([...])\n  -\u003epaginate() // will generate pagination\n  -\u003eget();\n\n$opds-\u003egetPaginator(); // OpdsPaginator\n```\n\nYou can use `OpdsPaginate::class` to handle manual pagination\n\n```php\nuse Kiwilan\\Opds\\Opds;\n\n$opds = Opds::make(getConfig())\n  -\u003etitle('My feed')\n  -\u003eurl('http://localhost:8080/opds?u=2')\n  -\u003efeeds([...])\n  -\u003epaginate(new OpdsPaginate(\n    currentPage: $page,\n    totalItems: $total,\n    firstUrl: 'http://localhost:8080/opds?f=1',\n    lastUrl: 'http://localhost:8080/opds?l=42',\n    previousUrl: 'http://localhost:8080/opds?p=1',\n    nextUrl: 'http://localhost:8080/opds?n=3',\n  )) // will generate pagination based on `OpdsPaginate` object\n  -\u003eget();\n\n$opds-\u003egetPaginator(); // OpdsPaginate\n```\n\n### OPDS entry\n\n#### Navigation\n\nYou can create a navigation entry with `OpdsEntryNavigation::class`:\n\n```php\nuse Kiwilan\\Opds\\Entries\\OpdsEntryNavigation;\n\n$entry = new OpdsEntryNavigation(\n  id: 'authors',\n  title: 'Authors',\n  route: 'http://mylibrary.com/opds/authors',\n  summary: 'Authors, 1 available',\n  media: 'https://user-images.githubusercontent.com/48261459/201463225-0a5a084e-df15-4b11-b1d2-40fafd3555cf.svg',\n  updated: new DateTime(),\n  properties: [\n    'numberOfItems' =\u003e 1,\n  ], // to include extra properties (like numberOfItems for facets)\n  relation: 'current', // to specify the relation to use (instead of `current`)\n);\n```\n\n\u003e [!TIP]\n\u003e\n\u003e You can override `OpdsEntryNavigation` with setter methods.\n\nAnd you can add this entry to OPDS feed with `feeds()` method:\n\n```php\nuse Kiwilan\\Opds\\Opds;\n\n$opds = Opds::make()\n  -\u003efeeds([$entry])\n  -\u003eget();\n```\n\n#### Book\n\nYou can create a book entry with `OpdsEntryBook::class`:\n\n\u003e [!WARNING]\n\u003e\n\u003e Some properties can be used only into OPDS 2.0, see [OPDS 2.0 specification](https://drafts.opds.io/opds-2.0.html#book).\n\n```php\nuse Kiwilan\\Opds\\Entries\\OpdsEntryBook;\nuse Kiwilan\\Opds\\Entries\\OpdsEntryBookAuthor;\n\n$entry = new OpdsEntryBook(\n  id: 'the-clan-of-the-cave-bear-epub-en',\n  title: 'The Clan of the Cave Bear',\n  route: 'http://mylibrary.com/opds/books/the-clan-of-the-cave-bear-epub-en',\n  summary: 'The Clan of the Cave Bear is an epic work of prehistoric fiction by Jean M. Auel.',\n  content: 'The Clan of the Cave Bear is an epic work of prehistoric fiction by Jean M. Auel about prehistoric times. It is the first book in the Earth\\'s Children book series which speculates on the possibilities of interactions between Neanderthal and modern Cro-Magnon humans.',\n  media: 'https://user-images.githubusercontent.com/48261459/201463225-0a5a084e-df15-4b11-b1d2-40fafd3555cf.svg',\n  updated: new DateTime(),\n  download: 'http://mylibrary.com/api/download/books/the-clan-of-the-cave-bear-epub-en',\n  mediaThumbnail: 'https://user-images.githubusercontent.com/48261459/201463225-0a5a084e-df15-4b11-b1d2-40fafd3555cf.svg',\n  categories: ['category'],\n  authors: [\n    new OpdsEntryBookAuthor(\n      name: 'Jean M. Auel',\n      uri: 'http://mylibrary.com/opds/authors/jean-m-auel',\n    ),\n  ],\n  published: new DateTime(),\n  volume: 1,\n  serie: 'Earth\\'s Children',\n  language: 'English',\n  identifier: 'urn:isbn:9780553381672', // to specify the actual identifier to use (instead of `urn:isbn:...`)\n  translator: 'translator',\n  publisher: 'publisher',\n);\n```\n\n\u003e [!TIP]\n\u003e\n\u003e You can override `OpdsEntryBook` with setter methods.\n\nAnd you can add this entry to OPDS feed with `feeds()` method:\n\n```php\n$opds = Opds::make()\n  -\u003efeeds([$entry])\n  -\u003eget();\n```\n\n### Search\n\nThis package do NOT implements any search engine, you can use your own search engine and use `Opds::class` to create OPDS feed.\n\n**Query parameters used for search are statically defined into specifications**:\n\n-   `q` param is used by OPDS 1.2\n-   `query` param is used by OPDS 2.0\n\n\u003e [!TIP]\n\u003e\n\u003e I advice [Meilisearch](https://www.meilisearch.com/) for search engine, it's a powerful and easy to use search engine.\n\nHere an example:\n\n```php\nuse Kiwilan\\Opds\\Opds;\nuse Kiwilan\\Opds\\Entries\\OpdsEntryBook;\n\n$query = // get query from URL, `q` or `query` param\n$feeds = [];\n\nif ($query) {\n    $results = []; // use your search engine here\n\n    foreach ($results as $result) {\n      $feeds[] = new OpdsEntryBook(\n        title: $result-\u003etitle,\n        // ...\n      );\n    }\n}\n\n$opds = Opds::make()\n  -\u003etitle(\"Search for {$query}\")\n  -\u003eisSearch()\n  -\u003efeeds($feeds)\n  -\u003eget();\n```\n\n### More usages\n\n-   [Basic usage](docs/basic-usage.md)\n-   [Advanced usage](docs/advanced-usage.md)\n\n## Testing\n\n```bash\ncomposer test\n```\n\n## Changelog\n\nPlease see [CHANGELOG](CHANGELOG.md) for more information on what has changed recently.\n\n## Credits\n\n-   [OPDS creators](https://opds.io/): for OPDS specifications\n-   [`ewilan-riviere`](https://github.com/ewilan-riviere): author\n-   [Contributors](https://github.com/kiwilan/php-opds/graphs/contributors)\n-   [`spatie/array-to-xml`](https://github.com/spatie/array-to-xml): to convert array to XML\n-   [`spatie/package-skeleton-php`](https://github.com/spatie/package-skeleton-php): skeleton for PHP package\n\n## License\n\nThe MIT License (MIT). Please see [License File](LICENSE.md) for more information.\n\n[\u003cimg src=\"https://user-images.githubusercontent.com/48261459/201463225-0a5a084e-df15-4b11-b1d2-40fafd3555cf.svg\" height=\"120rem\" width=\"100%\" /\u003e](https://github.com/kiwilan)\n\n[version-src]: https://img.shields.io/packagist/v/kiwilan/php-opds.svg?style=flat\u0026colorA=18181B\u0026colorB=777BB4\n[version-href]: https://packagist.org/packages/kiwilan/php-opds\n[php-version-src]: https://img.shields.io/static/v1?style=flat\u0026label=PHP\u0026message=v8.1\u0026color=777BB4\u0026logo=php\u0026logoColor=ffffff\u0026labelColor=18181b\n[php-version-href]: https://www.php.net/\n[downloads-src]: https://img.shields.io/packagist/dt/kiwilan/php-opds.svg?style=flat\u0026colorA=18181B\u0026colorB=777BB4\n[downloads-href]: https://packagist.org/packages/kiwilan/php-opds\n[license-src]: https://img.shields.io/github/license/kiwilan/php-opds.svg?style=flat\u0026colorA=18181B\u0026colorB=777BB4\n[license-href]: https://github.com/kiwilan/php-opds/blob/main/README.md\n[tests-src]: https://img.shields.io/github/actions/workflow/status/kiwilan/php-opds/run-tests.yml?branch=main\u0026label=tests\u0026style=flat\u0026colorA=18181B\n[tests-href]: https://packagist.org/packages/kiwilan/php-opds\n[codecov-src]: https://img.shields.io/codecov/c/gh/kiwilan/php-opds/main?style=flat\u0026colorA=18181B\u0026colorB=777BB4\n[codecov-href]: https://codecov.io/gh/kiwilan/php-opds\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fkiwilan%2Fphp-opds","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fkiwilan%2Fphp-opds","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fkiwilan%2Fphp-opds/lists"}