diff --git a/README.md b/README.md index 9bd04af..b405489 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,8 @@ PHP package to create [OPDS feed](https://opds.io/) (Open Publication Distributi | 1.2 | βœ… | November 11, 2018 | XML | `?version=1.2` | | 2.0 | βœ… | Draft | JSON | `?version=2.0` | +All old versions: 0.9, 1.0 and 1.1 have a fallback to OPDS 1.2. + ## Requirements - PHP >= 8.1 @@ -24,7 +26,7 @@ PHP package to create [OPDS feed](https://opds.io/) (Open Publication Distributi OPDS 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). -This package has been created to be used with [bookshelves-project/bookshelves](https://github.com/bookshelves-project/bookshelves), an open source eBook web app. +This package has been created to be used with [`bookshelves-project/bookshelves`](https://github.com/bookshelves-project/bookshelves), an open source eBook web app. > 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. > @@ -35,13 +37,14 @@ This package has been created to be used with [bookshelves-project/bookshelves]( Some resources about OPDS and eBooks: - [opds.io](https://opds.io/): OPDS official website -- [thorium-reader](https://github.com/edrlab/thorium-reader): test OPDS feed with Thorium Reader - OPDS feeds examples (these projects don't use `kiwilan/php-opds`) - [gallica.bnf.fr](https://gallica.bnf.fr/opds): Gallica (French National Library) - [cops-demo.slucas.fr](https://cops-demo.slucas.fr/feed.php): COPS (OPDS PHP Server) -- [kiwilan/php-ebook](https://github.com/kiwilan/php-ebook): PHP package to handle eBook -- [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. -- [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. + - [feedbooks.com](https://www.feedbooks.com/catalog.atom): Feedbooks + https://bookshelves.ink/opds +- [`kiwilan/php-ebook`](https://github.com/kiwilan/php-ebook): PHP package to handle eBook +- [`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 +- [`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 ## Features @@ -49,7 +52,7 @@ Some resources about OPDS and eBooks: - πŸ‘Œ Support OPDS 1.2 and 2.0 - πŸ”– With pagination option - πŸ” Search page included, but NOT search engine -- 🌐 Can handle sending response to browser +- 🌐 Option to handle response to browser as XML or JSON ## Installation @@ -61,14 +64,7 @@ composer require kiwilan/php-opds:1.0.0-alpha.5 ## Usage -### Version - -You can use query parameter `version` to set it dynamically. You could change this query into `OpdsConfig::class`. - -- Version `1.2` can be set with `?version=1.2` -- Version `2.0` can be set with `?version=2.0` - -You can use the `Opds::make()` method to create an OPDS instance, default response is XML with OPDS version 1.2, you can force JSON response with `OpdsConfig::class` method `forceJson()`. +You 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`. ```php use Kiwilan\Opds\Opds; @@ -76,7 +72,7 @@ use Kiwilan\Opds\OpdsConfig; $opds = Opds::make(new OpdsConfig()) // OpdsConfig::class, optional ->title('My feed') - ->feeds([]) // OpdsEntryNavigation[]|OpdsEntryBook[]|OpdsEntryNavigation|OpdsEntryBook + ->feeds([...]) // OpdsEntryNavigation[]|OpdsEntryBook[]|OpdsEntryNavigation|OpdsEntryBook ->get() ; ``` @@ -105,35 +101,91 @@ $opds->getOutput(); // OpdsOutputEnum|null - Output of response, useful for debu $opds->getResponse(); // OpdsResponse|null - Response of OPDS feed, will use `OpdsEngine` to create a response ``` -### Response +### Version -You can send response to browser if you want: +You can use query parameter `version` to set it dynamically. You could change this query into `OpdsConfig::class`. + +- Version `1.2` can be set with `?version=1.2` +- Version `2.0` can be set with `?version=2.0` > **Warning** > -> If you send response to browser, you can't use any method after that. +> If you set `version` query parameter to `1.2` with `OpdsConfig::class` method `forceJson()`, it will be ignored. + +### Engine + +Engine will convert your feeds to OPDS, depending of OPDS version. + +- OPDS 1.2 will use `OpdsXmlEngine::class` +- OPDS 2.0 will use `OpdsJsonEngine::class` + +You can get engine used with `getEngine()` method from `Opds::class`. Property `content` contains array of feeds, `OpdsEngine` allow conversion into XML or JSON with `__toString()` method, the output depends of OPDS version. ```php use Kiwilan\Opds\Opds; $opds = Opds::make() ->title('My feed') - ->feeds([]) + ->feeds([...]) ->get() ; -return $opds->response(); // XML or JSON response +$engine = $opds->getEngine(); // OpdsEngine +$content = $engine->getContents(); // array +$output = $engine->__toString(); // string +``` + +### Response + +You can use `get()` method and after that, use `send()` method to send response to browser. + +```php +use Kiwilan\Opds\Opds; + +$opds = Opds::make() + ->title('My feed') + ->feeds([...]) + ->get() +; + +$opds->send(); // XML or JSON response, stop script ``` You can send directly response to browser: +> **Warning** +> +> If you send response to browser, you can't use any method after that. + +```php +use Kiwilan\Opds\Opds; + +Opds::make() + ->title('My feed') + ->feeds([...]) + ->send(); // XML or JSON response, stop script +``` + +To get only instance of `OpdsResponse`, you can use `getResponse()` method from `Opds::class`. You can use this response to get status code, headers and contents, you can send it to browser by yourself or use `send()` method. + ```php use Kiwilan\Opds\Opds; -return Opds::make() +$opds = Opds::make() ->title('My feed') - ->feeds([]) - ->response(); + ->feeds([...]) + ->get() +; + +$response = $opds->getResponse(); // OpdsResponse + +$response->getStatus(); // int - Status code of response +$response->isJson(); // bool - If response is JSON +$response->isXml(); // bool - If response is XML +$response->getHeaders(); // array - Headers of response +$response->getContents(); // string - Contents of response + +$response->send(); // Send response to browser, stop script ``` ### Config @@ -161,6 +213,10 @@ $config = new OpdsConfig( ); ``` +> **Note** +> +> You can override `OpdsConfig` with setter methods. + ### OPDS entry #### Navigation @@ -180,6 +236,10 @@ $entry = new OpdsEntryNavigation( ); ``` +> **Note** +> +> You can override `OpdsEntryNavigation` with setter methods. + And you can add this entry to OPDS feed with `feeds()` method: ```php @@ -200,6 +260,7 @@ You can create a book entry with `OpdsEntryBook::class`: ```php use Kiwilan\Opds\Entries\OpdsEntryBook; +use Kiwilan\Opds\Entries\OpdsEntryBookAuthor; $entry = new OpdsEntryBook( id: 'the-clan-of-the-cave-bear-epub-en', @@ -228,6 +289,10 @@ $entry = new OpdsEntryBook( ); ``` +> **Note** +> +> You can override `OpdsEntryBook` with setter methods. + And you can add this entry to OPDS feed with `feeds()` method: ```php @@ -240,6 +305,10 @@ $opds = Opds::make() This package do NOT implements any search engine, you can use your own search engine and use `Opds::class` to create OPDS feed. +> **Note** +> +> I advice [Meilisearch](https://www.meilisearch.com/) for search engine, it's a powerful and easy to use search engine. + Here an example: ```php @@ -281,9 +350,10 @@ Please see [CHANGELOG](CHANGELOG.md) for more information on what has changed re ## Credits -- [Ewilan RiviΓ¨re](https://github.com/ewilan-riviere) -- [spatie/array-to-xml](https://github.com/spatie/array-to-xml) -- [spatie/package-skeleton-php](https://github.com/spatie/package-skeleton-php) +- [`ewilan-riviere`](https://github.com/ewilan-riviere): Author +- [`spatie/array-to-xml`](https://github.com/spatie/array-to-xml): to convert array to XML +- [`spatie/package-skeleton-php`](https://github.com/spatie/package-skeleton-php): skeleton for PHP package +- [Contributors](https://github.com/kiwilan/php-opds/graphs/contributors) ## License diff --git a/composer.json b/composer.json index 7244c0f..98af1ee 100644 --- a/composer.json +++ b/composer.json @@ -1,7 +1,7 @@ { "name": "kiwilan/php-opds", "description": "PHP package to create OPDS feed for eBooks.", - "version": "1.0.0-alpha.5", + "version": "1.0.0-alpha.6", "keywords": [ "php", "ebook", diff --git a/docs/advanced-usage.md b/docs/advanced-usage.md index 81f7deb..5d3ada9 100644 --- a/docs/advanced-usage.md +++ b/docs/advanced-usage.md @@ -145,7 +145,7 @@ class IndexController extends Controller { return Opds::make(MyOpds::config()) ->feeds(MyOpds::home()) - ->get() + ->send() ; } @@ -165,7 +165,7 @@ class IndexController extends Controller ->title("Search for {$query}") ->isSearch() ->feeds($feeds) - ->get() + ->send() ; } } @@ -200,7 +200,7 @@ class BookController extends Controller return Opds::make(MyOpds::config()) ->title("Book {$book->title}") ->feeds(MyOpds::bookToEntry($book)) - ->get() + ->send() ; } } diff --git a/docs/basic-usage.md b/docs/basic-usage.md index 9b59e03..f26fef1 100644 --- a/docs/basic-usage.md +++ b/docs/basic-usage.md @@ -36,7 +36,7 @@ class OpdsController ]) ); - return $opds->response(); + return $opds->send(); } public function books() @@ -67,7 +67,7 @@ class OpdsController ), ]); - return $opds->response(); + return $opds->send(); } private function config(): OpdsConfig diff --git a/docs/draft.md b/docs/draft.md deleted file mode 100644 index 5ad319b..0000000 --- a/docs/draft.md +++ /dev/null @@ -1,17 +0,0 @@ -## 1.0.0 - draft - -### BREAKING CHANGES - -- `Opds::response()` is now `Opds::make()` and `response()` is now a direct method: - -```php -$opds = Opds::make(); - -return $opds->response(); -``` - -- `entries` options is now `feeds` - -### Added - -- add ODPS 2.0 support diff --git a/phpunit.xml.dist b/phpunit.xml.dist index a7e9c48..67f1cbe 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -15,7 +15,7 @@ failOnWarning="true" failOnRisky="true" failOnEmptyTestSuite="true" - beStrictAboutOutputDuringTests="true" + beStrictAboutOutputDuringTests="false" verbose="true" > @@ -28,12 +28,12 @@ ./src - - - + + + - + - + \ No newline at end of file diff --git a/src/Engine/OpdsEngine.php b/src/Engine/OpdsEngine.php index 603e301..61344d1 100644 --- a/src/Engine/OpdsEngine.php +++ b/src/Engine/OpdsEngine.php @@ -4,16 +4,18 @@ use Kiwilan\Opds\Entries\OpdsEntryBook; use Kiwilan\Opds\Entries\OpdsEntryNavigation; +use Kiwilan\Opds\Enums\OpdsOutputEnum; use Kiwilan\Opds\Enums\OpdsVersionEnum; use Kiwilan\Opds\Opds; use Kiwilan\Opds\OpdsConfig; +use Spatie\ArrayToXml\ArrayToXml; abstract class OpdsEngine { protected function __construct( protected Opds $opds, - protected array $content = [], - protected ?string $response = null, + protected ?OpdsPaginator $paginator = null, + protected array $contents = [], ) { } @@ -54,16 +56,9 @@ abstract public function addNavigationEntry(OpdsEntryNavigation $entry): array; */ abstract public function addBookEntry(OpdsEntryBook $entry): array; - public function setContent(array $content): self + public function setContents(array $content): self { - $this->content = $content; - - return $this; - } - - public function setResponse(string $response): self - { - $this->response = $response; + $this->contents = $content; return $this; } @@ -76,20 +71,56 @@ public function getOpds(): Opds return $this->opds; } + /** + * Get paginator instance. + */ + public function getPaginator(): ?OpdsPaginator + { + return $this->paginator; + } + /** * Get content array. */ - public function getContent(): array + public function getContents(): array + { + return $this->contents; + } + + /** + * Convert `content` to XML. + */ + protected function toXML(): string + { + return ArrayToXml::convert( + array: $this->contents, + rootElement: [ + 'rootElementName' => 'feed', + '_attributes' => OpdsNamespaces::VERSION_1_2, + ], + replaceSpacesByUnderScoresInKeyNames: true, + xmlEncoding: 'UTF-8' + ); + } + + /** + * Convert `content` to JSON. + */ + protected function toJSON(): string { - return $this->content; + return json_encode($this->contents); } /** - * Get response as XML or JSON. + * Convert `content` to string, XML or JSON. */ - public function getResponse(): string + public function __toString(): string { - return $this->response; + if ($this->opds->getOutput() === OpdsOutputEnum::xml) { + return $this->toXML(); + } + + return $this->toJSON(); } /** @@ -143,7 +174,7 @@ protected function addXmlNode(string $value = null, ?array $attributes = []): ar return $node; } - protected function addJsonLink( + public static function addJsonLink( string $href = null, string $title = null, string $rel = null, @@ -173,7 +204,7 @@ protected function addJsonLink( return $data; } - protected function addXmlLink( + public static function addXmlLink( string $href = null, string $title = null, string $rel = null, @@ -191,10 +222,6 @@ protected function addXmlLink( protected function route(?string $route): ?string { - if (! $route) { - return null; - } - $query = $this->opds->getQuery(); $query = $query[$this->opds->getConfig()->getVersionQuery()] ?? null; @@ -207,100 +234,14 @@ protected function route(?string $route): ?string return $route.'?'.http_build_query($query); } - protected function handleJsonPagination(): array - { - return [ - 'metadata' => [ - 'title' => 'Paginated feed', - 'numberOfItems' => 5678, - 'itemsPerPage' => 50, - 'currentPage' => 2, - ], - 'links' => [ - ['rel' => 'self', 'href' => '/?page=2', 'type' => 'application/opds+json'], - ['rel' => ['first', 'previous'], 'href' => '/?page=1', 'type' => 'application/opds+json'], - ['rel' => 'next', 'href' => '/?page=3', 'type' => 'application/opds+json'], - ['rel' => 'last', 'href' => '/?page=114', 'type' => 'application/opds+json'], - ], - ]; - } - /** - * Handle XML pagination. + * Paginate feeds. + * + * @param array $content + * @param OpdsEntryNavigation[]|OpdsEntryBook[] $feeds */ - protected function handleXmlPagination(array &$content, array &$feeds): void + protected function paginate(array &$content, array &$feeds): void { - $feeds = $this->opds->getFeeds(); - $paginate = $this->opds->getConfig()->isUsePagination(); - $perPage = $this->opds->getConfig()->getMaxItemsPerPage(); - $page = 1; - - if (! $paginate) { - return; - } - - if (count($feeds) < $perPage) { - return; - } - - $currentUrl = $this->opds->getUrl(); - - if (str_contains($currentUrl, '?')) { - $current = explode('?', $currentUrl)[0]; - } - - $queryStartRecord = $this->opds->getQuery()['startRecord'] ?? 0; - $queryStartRecord = intval($queryStartRecord); - - $count = count($feeds); - $pageNumbers = intval(ceil($count / $perPage)); - $start = $this->opds->getQuery()['startRecord'] ?? $page - 1; - $feeds = array_slice($feeds, $start, $perPage); - - $first = $this->opds->getQuery()['startRecord'] ?? 0; - $last = ($perPage * $pageNumbers) - $perPage; - - $startRecord = $start + $perPage; - - $previousUrl = $currentUrl.'?'.http_build_query([ - 'q' => $this->opds->getQuery()['q'] ?? null, - 'startRecord' => '-'.$startRecord, - 'maximumRecords' => $perPage, - ]); - $nextUrl = $currentUrl.'?'.http_build_query([ - 'q' => $this->opds->getQuery()['q'] ?? null, - 'startRecord' => $startRecord, - 'maximumRecords' => $perPage, - ]); - $firstUrl = $currentUrl.'?'.http_build_query([ - 'q' => $this->opds->getQuery()['q'] ?? null, - 'startRecord' => 0, - 'maximumRecords' => $perPage, - ]); - $lastUrl = $currentUrl.'?'.http_build_query([ - 'q' => $this->opds->getQuery()['q'] ?? null, - 'startRecord' => $last, - 'maximumRecords' => $perPage, - ]); - - if ($queryStartRecord !== 0) { - $content['__custom:link:4'] = $this->addXmlLink(href: $previousUrl, rel: 'previous', title: 'Previous page'); - } - - if ($queryStartRecord !== $last) { - $content['__custom:link:5'] = $this->addXmlLink(href: $nextUrl, rel: 'next', title: 'Next page'); - } - - if ($queryStartRecord !== 0) { - $content['__custom:link:6'] = $this->addXmlLink(href: $firstUrl, rel: 'first', title: 'First page'); - } - - if ($queryStartRecord !== $last) { - $content['__custom:link:7'] = $this->addXmlLink(href: $lastUrl, rel: 'last', title: 'Last page'); - } - - $content['opensearch:totalResults'] = count($this->opds->getFeeds()); - $content['opensearch:itemsPerPage'] = $perPage; - $content['opensearch:startIndex'] = $startRecord === 0 ? 1 : $start; + $this->paginator = OpdsPaginator::make($this)->paginate($content, $feeds); } } diff --git a/src/Engine/OpdsJsonEngine.php b/src/Engine/OpdsJsonEngine.php index cecaa14..067726a 100644 --- a/src/Engine/OpdsJsonEngine.php +++ b/src/Engine/OpdsJsonEngine.php @@ -25,7 +25,7 @@ public static function make(Opds $opds): self public function feed(): self { - $this->content = [ + $this->contents = [ 'metadata' => [ 'title' => $this->getFeedTitle(), ], @@ -38,14 +38,14 @@ public function feed(): self if ($this->opds->getConfig()->getStartUrl()) { if (! $this->opds->getConfig()->isForceJson()) { - $this->content['links'][] = $this->addJsonLink( + $this->contents['links'][] = $this->addJsonLink( rel: 'alternate', href: $this->getVersionUrl(OpdsVersionEnum::v1Dot2), title: 'OPDS 1.2', type: 'application/atom+xml', ); } - $this->content['links'][] = $this->addJsonLink( + $this->contents['links'][] = $this->addJsonLink( rel: 'alternate', href: $this->getVersionUrl(OpdsVersionEnum::v2Dot0), title: 'OPDS 2.0', @@ -53,18 +53,19 @@ public function feed(): self ); } - foreach ($this->opds->getFeeds() as $feed) { + $feeds = $this->opds->getFeeds(); + $this->paginate($this->contents, $feeds); + + foreach ($feeds as $feed) { if ($feed instanceof OpdsEntryBook) { - $this->content['publications'][] = $this->addEntry($feed); + $this->contents['publications'][] = $this->addEntry($feed); continue; } - $this->content['navigation'][] = $this->addEntry($feed); + $this->contents['navigation'][] = $this->addEntry($feed); } - $this->response = json_encode($this->content); - return $this; } diff --git a/src/Engine/Utils/OpdsNamespaces.php b/src/Engine/OpdsNamespaces.php similarity index 95% rename from src/Engine/Utils/OpdsNamespaces.php rename to src/Engine/OpdsNamespaces.php index e713b19..86a48cf 100644 --- a/src/Engine/Utils/OpdsNamespaces.php +++ b/src/Engine/OpdsNamespaces.php @@ -1,6 +1,6 @@ getOpds()->getUrl(); + if (str_contains($url, '?')) { + $url = explode('?', $url)[0]; + } + + $self = new self( + output: $engine->getOpds()->getOutput(), + url: $url, + query: $engine->getOpds()->getQuery(), + usePagination: $engine->getOpds()->getConfig()->isUsePagination(), + perPage: $engine->getOpds()->getConfig()->getMaxItemsPerPage(), + page: 1, + ); + + return $self; + } + + public function getOutput(): OpdsOutputEnum + { + return $this->output; + } + + public function getUrl(): string + { + return $this->url; + } + + public function getQuery(): array + { + return $this->query; + } + + public function usePagination(): bool + { + return $this->usePagination; + } + + public function getPerPage(): int + { + return $this->perPage; + } + + public function getPage(): int + { + return $this->page; + } + + public function getTotal(): int + { + return $this->total; + } + + public function getStart(): int + { + return $this->start; + } + + public function getStartRecord(): int + { + return $this->startRecord; + } + + public function getSize(): int + { + return $this->size; + } + + public function getFirst(): int + { + return $this->first; + } + + public function getLast(): int + { + return $this->last; + } + + /** + * Handle pagination. + * + * @param array $content + * @param OpdsEntryNavigation[]|OpdsEntryBook[] $feeds + */ + public function paginate(array &$content, array &$feeds): self + { + if (! $this->usePagination) { + return $this; + } + + if (count($feeds) < $this->perPage) { + return $this; + } + + $this->total = count($feeds); + $this->start = intval($this->query['startRecord'] ?? 0); + $this->size = intval(ceil($this->total / $this->perPage)); + // $start = $this->query['startRecord'] ?? $this->page - 1; + $this->first = $this->start; + $this->last = ($this->perPage * $this->size) - $this->perPage; + $this->startRecord = $this->start + $this->perPage; + + $feeds = array_slice($feeds, $this->start, $this->perPage); + + if ($this->output === OpdsOutputEnum::json) { + $this->json($content); + } + + if ($this->output === OpdsOutputEnum::xml) { + $this->xml($content); + } + + return $this; + } + + /** + * Handle JSON pagination. + */ + private function json(array &$content): void + { + $content['metadata'] = [ + ...$content['metadata'], + 'numberOfItems' => $this->total, + 'itemsPerPage' => $this->perPage, + 'currentPage' => $this->page, + ]; + + $content['links'] = [ + // ['rel' => 'self', 'href' => '/?page=2', 'type' => 'application/opds+json'], + // ['rel' => ['first', 'previous'], 'href' => '/?page=1', 'type' => 'application/opds+json'], + // ['rel' => 'next', 'href' => '/?page=3', 'type' => 'application/opds+json'], + // ['rel' => 'last', 'href' => '/?page=114', 'type' => 'application/opds+json'], + OpdsEngine::addJsonLink( + rel: 'self', + href: '/?page=2', + ), + OpdsEngine::addJsonLink( + rel: 'first', + href: '/?page=1', + ), + OpdsEngine::addJsonLink( + rel: 'previous', + href: '/?page=1', + ), + OpdsEngine::addJsonLink( + rel: 'next', + href: '/?page=3', + ), + OpdsEngine::addJsonLink( + rel: 'last', + href: '/?page=114', + ), + ]; + } + + /** + * Handle XML pagination. + */ + private function xml(array &$content): void + { + $previousUrl = $this->url.'?'.http_build_query([ + 'q' => $this->query['q'] ?? null, + 'startRecord' => '-'.$this->start, + 'maximumRecords' => $this->perPage, + ]); + $nextUrl = $this->url.'?'.http_build_query([ + 'q' => $this->query['q'] ?? null, + 'startRecord' => $this->start, + 'maximumRecords' => $this->perPage, + ]); + $firstUrl = $this->url.'?'.http_build_query([ + 'q' => $this->query['q'] ?? null, + 'startRecord' => 0, + 'maximumRecords' => $this->perPage, + ]); + $lastUrl = $this->url.'?'.http_build_query([ + 'q' => $this->query['q'] ?? null, + 'startRecord' => $this->last, + 'maximumRecords' => $this->perPage, + ]); + + if ($this->start !== 0) { + $content['__custom:link:4'] = OpdsEngine::addXmlLink( + href: $previousUrl, + rel: 'previous', + title: 'Previous page' + ); + } + + if ($this->start !== $this->last) { + $content['__custom:link:5'] = OpdsEngine::addXmlLink( + href: $nextUrl, + rel: 'next', + title: 'Next page' + ); + } + + if ($this->start !== 0) { + $content['__custom:link:6'] = OpdsEngine::addXmlLink( + href: $firstUrl, + rel: 'first', + title: 'First page' + ); + } + + if ($this->start !== $this->last) { + $content['__custom:link:7'] = OpdsEngine::addXmlLink( + href: $lastUrl, + rel: 'last', + title: 'Last page' + ); + } + + $content['opensearch:totalResults'] = $this->total; + $content['opensearch:itemsPerPage'] = $this->perPage; + $content['opensearch:startIndex'] = $this->start === 0 ? 1 : $this->start; + } +} diff --git a/src/Engine/OpdsXmlEngine.php b/src/Engine/OpdsXmlEngine.php index aee0cc7..f6c5f02 100644 --- a/src/Engine/OpdsXmlEngine.php +++ b/src/Engine/OpdsXmlEngine.php @@ -2,13 +2,11 @@ namespace Kiwilan\Opds\Engine; -use Kiwilan\Opds\Engine\Utils\OpdsNamespaces; use Kiwilan\Opds\Entries\OpdsEntryBook; use Kiwilan\Opds\Entries\OpdsEntryNavigation; use Kiwilan\Opds\Enums\OpdsVersionEnum; use Kiwilan\Opds\Opds; use Kiwilan\Opds\OpdsConfig; -use Spatie\ArrayToXml\ArrayToXml; class OpdsXmlEngine extends OpdsEngine { @@ -29,34 +27,47 @@ public function feed(): self $title = $this->getFeedTitle(); $updated = $this->opds->getConfig()->getUpdated(); - $this->content = [ + $this->contents = [ 'id' => $id, 'title' => $title, 'updated' => $updated->format(DATE_ATOM), ]; if ($this->opds->getConfig()->getIconUrl()) { - $this->content['icon'] = $this->opds->getConfig()->getIconUrl(); + $this->contents['icon'] = $this->opds->getConfig()->getIconUrl(); } - $this->content['__custom:link:1'] = $this->addXmlLink(href: OpdsEngine::getCurrentUrl(), title: 'self', rel: 'self'); + $this->contents['__custom:link:1'] = $this->addXmlLink( + href: OpdsEngine::getCurrentUrl(), + title: 'self', + rel: 'self' + ); if ($this->opds->getConfig()->getStartUrl()) { - $this->content['__custom:link:2'] = $this->addXmlLink(href: $this->route($this->opds->getConfig()->getStartUrl()), title: 'Home', rel: 'start'); + $this->contents['__custom:link:2'] = $this->addXmlLink( + href: $this->route($this->opds->getConfig()->getStartUrl()), + title: 'Home', + rel: 'start' + ); } if ($this->opds->getConfig()->getSearchUrl()) { - $this->content['__custom:link:3'] = $this->addXmlLink(href: $this->route($this->opds->getConfig()->getSearchUrl()), title: 'Search here', rel: 'search'); + $this->contents['__custom:link:3'] = $this->addXmlLink( + href: $this->route($this->opds->getConfig()->getSearchUrl()), + title: 'Search here', + rel: 'search', + type: 'application/opensearchdescription+xml', + ); } if ($this->opds->getConfig()->getStartUrl()) { - $this->content['__custom:link:4'] = $this->addXmlLink( + $this->contents['__custom:link:4'] = $this->addXmlLink( href: $this->getVersionUrl(OpdsVersionEnum::v1Dot2), title: 'OPDS 1.2', rel: 'alternate', type: 'application/atom+xml' ); - $this->content['__custom:link:5'] = $this->addXmlLink( + $this->contents['__custom:link:5'] = $this->addXmlLink( href: $this->getVersionUrl(OpdsVersionEnum::v2Dot0), title: 'OPDS 2.0', rel: 'alternate', @@ -65,26 +76,19 @@ public function feed(): self } if ($this->opds->getConfig()->getAuthor()) { - $this->content['author'] = ['name' => $this->opds->getConfig()->getAuthor(), 'uri' => $this->opds->getConfig()->getAuthorUrl()]; + $this->contents['author'] = [ + 'name' => $this->opds->getConfig()->getAuthor(), + 'uri' => $this->opds->getConfig()->getAuthorUrl(), + ]; } $feeds = $this->opds->getFeeds(); - $this->handleXmlPagination($this->content, $feeds); + $this->paginate($this->contents, $feeds); foreach ($feeds as $entry) { - $this->content['entry'][] = $this->addEntry($entry); + $this->contents['entry'][] = $this->addEntry($entry); } - $this->response = ArrayToXml::convert( - array: $this->content, - rootElement: [ - 'rootElementName' => 'feed', - '_attributes' => OpdsNamespaces::VERSION_1_2, - ], - replaceSpacesByUnderScoresInKeyNames: true, - xmlEncoding: 'UTF-8' - ); - return $this; } @@ -102,7 +106,7 @@ public function search(): self return $this; } - $this->content = [ + $this->contents = [ 'ShortName' => $this->addXmlNode($app), 'Description' => $this->addXmlNode("OPDS search engine {$app}"), 'InputEncoding' => $this->addXmlNode('UTF-8'), @@ -122,16 +126,6 @@ public function search(): self 'Language' => $this->addXmlNode('*'), ]; - $this->response = ArrayToXml::convert( - array: $this->content, - rootElement: [ - 'rootElementName' => 'OpenSearchDescription', - '_attributes' => OpdsNamespaces::VERSION_1_2_SEARCH, - ], - replaceSpacesByUnderScoresInKeyNames: true, - xmlEncoding: 'UTF-8', - ); - return $this; } @@ -155,9 +149,9 @@ public function addNavigationEntry(OpdsEntryNavigation $entry): array ); } - if ($entry->getContent()) { + if ($entry->getContents()) { $entryXml['content'] = $this->addXmlNode( - value: $entry->getContent(), + value: $entry->getContents(), attributes: ['type' => 'text/html'] ); } @@ -225,7 +219,7 @@ public function addBookEntry(OpdsEntryBook $entry): array 'updated' => $entry->getUpdated()?->format(DATE_ATOM), 'id' => $id, 'summary' => $this->addXmlNode(value: $entry->getSummary(), attributes: ['type' => 'text']), - 'content' => $this->addXmlNode(value: $entry->getContent(), attributes: ['type' => 'text/html']), + 'content' => $this->addXmlNode(value: $entry->getContents(), attributes: ['type' => 'text/html']), '__custom:link:1' => $this->addXmlLink(href: $this->route($entry->getRoute())), '__custom:link:2' => $this->addXmlLink(href: $media, rel: 'http://opds-spec.org/image', type: $mediaMimeType), '__custom:link:3' => $this->addXmlLink(href: $mediaThumbnail, rel: 'http://opds-spec.org/image/thumbnail', type: $mediaThumbnailMimeType), diff --git a/src/Entries/OpdsEntryNavigation.php b/src/Entries/OpdsEntryNavigation.php index 1a11309..031b276 100644 --- a/src/Entries/OpdsEntryNavigation.php +++ b/src/Entries/OpdsEntryNavigation.php @@ -88,7 +88,7 @@ public function getSummary(): ?string return $this->summary; } - public function getContent(): ?string + public function getContents(): ?string { return $this->content; } diff --git a/src/Enums/OpdsVersionEnum.php b/src/Enums/OpdsVersionEnum.php index 4c78818..c0d3966 100644 --- a/src/Enums/OpdsVersionEnum.php +++ b/src/Enums/OpdsVersionEnum.php @@ -4,6 +4,9 @@ enum OpdsVersionEnum: string { - case v1Dot2 = '1.2'; - case v2Dot0 = '2.0'; + // case v0Dot9 = '0.9'; // May 25th, 2010 + // case v1Dot0 = '1.0'; // August 30th, 2010 + // case v1Dot1 = '1.1'; // June 27th, 2011 + case v1Dot2 = '1.2'; // November 11th, 2018 + case v2Dot0 = '2.0'; // Draft } diff --git a/src/Opds.php b/src/Opds.php index 26d001d..75a4552 100755 --- a/src/Opds.php +++ b/src/Opds.php @@ -4,6 +4,7 @@ use Kiwilan\Opds\Engine\OpdsEngine; use Kiwilan\Opds\Engine\OpdsJsonEngine; +use Kiwilan\Opds\Engine\OpdsPaginator; use Kiwilan\Opds\Engine\OpdsXmlEngine; use Kiwilan\Opds\Entries\OpdsEntryBook; use Kiwilan\Opds\Entries\OpdsEntryNavigation; @@ -28,6 +29,7 @@ protected function __construct( protected array $feeds = [], protected bool $isSearch = false, protected ?OpdsEngine $engine = null, + protected ?OpdsPaginator $paginator = null, protected ?OpdsOutputEnum $output = null, // xml or json protected ?OpdsResponse $response = null, ) { @@ -47,10 +49,6 @@ public static function make(OpdsConfig $config = new OpdsConfig()): self $self->version = OpdsVersionEnum::v2Dot0; } - if ($self->queryVersion) { - $self->version = $self->queryVersion; - } - return $self; } @@ -99,7 +97,7 @@ public function isSearch(): self } /** - * Get OPDS with full response, you generate response with headers with `response()`. + * Get OPDS with `OpdsEngine` and `OpdsResponse`. */ public function get(): self { @@ -123,7 +121,8 @@ public function get(): self OpdsVersionEnum::v1Dot2 => OpdsXmlEngine::make($this), OpdsVersionEnum::v2Dot0 => OpdsJsonEngine::make($this), }; - $this->response = OpdsResponse::make($this->engine, 200); + $this->paginator = $this->engine->getPaginator(); + $this->response = OpdsResponse::make($this->engine->__toString(), $this->output, 200); return $this; } @@ -131,16 +130,16 @@ public function get(): self /** * Send response to browser. * - * @param bool $send To send valid response to browser it should be to `true`. + * @param bool $mock To send valid response to browser it should be to `true`. * @return void|never */ - public function response(bool $send = true) + public function send(bool $mock = true) { if (! $this->response) { $this->get(); } - $this->response->response($send); + $this->response->send($mock); } /** @@ -165,6 +164,9 @@ private function parseUrl(): self } $enumVersion = match ($version) { + '0.9' => OpdsVersionEnum::v1Dot2, + '1.0' => OpdsVersionEnum::v1Dot2, + '1.1' => OpdsVersionEnum::v1Dot2, '1.2' => OpdsVersionEnum::v1Dot2, '2.0' => OpdsVersionEnum::v2Dot0, default => null, @@ -176,6 +178,7 @@ private function parseUrl(): self if ($enumVersion) { $this->queryVersion = $enumVersion; + $this->version = $this->queryVersion; } return $this; @@ -259,6 +262,18 @@ public function getEngine(): ?OpdsEngine return $this->engine; } + /** + * Get OPDS paginator. + */ + public function getPaginator(): ?OpdsPaginator + { + if (! $this->paginator) { + $this->get(); + } + + return $this->paginator; + } + /** * Get OPDS output: xml or json. */ diff --git a/src/OpdsResponse.php b/src/OpdsResponse.php index 3eeb1b2..cd63bd6 100644 --- a/src/OpdsResponse.php +++ b/src/OpdsResponse.php @@ -2,7 +2,7 @@ namespace Kiwilan\Opds; -use Kiwilan\Opds\Engine\OpdsEngine; +use Kiwilan\Opds\Enums\OpdsOutputEnum; class OpdsResponse { @@ -14,23 +14,27 @@ protected function __construct( protected bool $isJson = false, protected bool $isXml = false, protected array $headers = [], - protected ?string $content = null, + protected ?string $contents = null, ) { } /** * Create a new Response. */ - public static function make(OpdsEngine $engine, int $status = 200): self + public static function make(string $contents, OpdsOutputEnum $output, int $status = 200): self { $self = new self($status); + $self->contents = $contents; - $self->isXml = $self->isValidXml($engine->getResponse()); - $self->isJson = $self->isValidJson($engine->getResponse()); + if ($output === OpdsOutputEnum::xml) { + $self->isXml = $self->isValidXml($self->contents); + } + + if ($output === OpdsOutputEnum::json) { + $self->isJson = $self->isValidJson($self->contents); + } - if ($self->isJson || $self->isXml) { - $self->content = $engine->getResponse(); - } else { + if (! $self->isJson && ! $self->isXml) { throw new \Exception('OPDS Response: invalid content'); } @@ -72,20 +76,52 @@ public function isXml(): bool } /** - * Get content. + * Get headers. + * + * @return array + */ + public function getHeaders(): array + { + return $this->headers; + } + + /** + * Get contents. */ - public function getContent(): string + public function getContents(): string { - return $this->content; + return $this->contents; + } + + /** + * Set headers. + * + * @param array $headers + */ + public function setHeaders(array $headers): self + { + $this->headers = $headers; + + return $this; + } + + /** + * Set contents. + */ + public function setContents(string $contents): self + { + $this->contents = $contents; + + return $this; } /** * Send content to browser with correct header. * - * @param bool $send To send valid response to browser it should be to `true`. + * @param bool $mock To send valid response to browser it should be to `true`. * @return never|void */ - public function response(bool $send = true) + public function send(bool $mock = true) { foreach ($this->headers as $type => $value) { header($type.': '.$value); @@ -93,9 +129,9 @@ public function response(bool $send = true) http_response_code($this->status); - echo $this->content; + echo $this->contents; - if ($send) { + if ($mock) { exit; } } diff --git a/tests/OpdsEntryTest.php b/tests/OpdsEntryTest.php index 0d86b24..2affd3c 100644 --- a/tests/OpdsEntryTest.php +++ b/tests/OpdsEntryTest.php @@ -20,7 +20,7 @@ expect($entry->getTitle())->toBe('The Clan of the Cave Bear'); expect($entry->getRoute())->toBe('http://localhost:8000/opds/books/the-clan-of-the-cave-bear-epub-en'); expect($entry->getSummary())->toBeString(); - expect($entry->getContent())->toBeString(); + expect($entry->getContents())->toBeString(); expect($entry->getMedia())->toBe('https://user-images.githubusercontent.com/48261459/201463225-0a5a084e-df15-4b11-b1d2-40fafd3555cf.svg'); expect($entry->getUpdated())->toBeInstanceOf(DateTime::class); expect($entry->getDownload())->toBe('http://localhost:8000/api/download/books/the-clan-of-the-cave-bear-epub-en'); @@ -69,7 +69,7 @@ expect($entry->getTitle())->toBe('The Clan of the Cave Bear 2'); expect($entry->getRoute())->toBe('http://localhost:8000/opds/books/the-clan-of-the-cave-bear-epub-en-2'); expect($entry->getSummary())->toBe('summary'); - expect($entry->getContent())->toBe('content'); + expect($entry->getContents())->toBe('content'); expect($entry->getMedia())->toBe('https://user-images.githubusercontent.com/48261459/201463225-0a5a084e-df15-4b11-b1d2-40fafd3555cf.svg'); expect($entry->getUpdated())->toBeInstanceOf(DateTime::class); expect($entry->getDownload())->toBe('http://localhost:8000/api/download/books/the-clan-of-the-cave-bear-epub-en'); diff --git a/tests/OpdsJsonTest.php b/tests/OpdsJsonTest.php index ff33333..54c4299 100644 --- a/tests/OpdsJsonTest.php +++ b/tests/OpdsJsonTest.php @@ -30,7 +30,7 @@ function getConfigV2(): OpdsConfig ->get(); $response = $opds->getResponse(); - expect($response->getContent())->toBeString(); + expect($response->getContents())->toBeString(); }); it('can use search', function () { @@ -39,7 +39,7 @@ function getConfigV2(): OpdsConfig ->get(); $response = $opds->getResponse(); - expect($response->getContent())->toBeString(); + expect($response->getContents())->toBeString(); }); it('can use feeds', function () { @@ -48,7 +48,7 @@ function getConfigV2(): OpdsConfig ->get(); expect($opds)->toBeInstanceOf(Opds::class); - expect($opds->getEngine()->getContent())->toBeArray(); + expect($opds->getEngine()->getContents())->toBeArray(); }); it('can use navigation feeds', function () { @@ -57,5 +57,20 @@ function getConfigV2(): OpdsConfig ->get(); expect($opds)->toBeInstanceOf(Opds::class); - expect($opds->getEngine()->getContent())->toBeArray(); + expect($opds->getEngine()->getContents())->toBeArray(); }); + +// it('can use pagination', function () { +// $config = getConfigV2(); +// $config->setMaxItemsPerPage(10); +// $config->usePagination(); +// $opds = Opds::make($config) +// ->feeds(manyFeeds()) +// ->get(); + +// ray($opds); +// ray($opds->getEngine()->getContents()); +// ray($opds->getPaginator()); +// // expect($opds)->toBeInstanceOf(Opds::class); +// // expect($opds->getEngine()->getContents())->toBeArray(); +// }); diff --git a/tests/OpdsPaginationTest.php b/tests/OpdsPaginationTest.php index ebedcb1..21e894c 100644 --- a/tests/OpdsPaginationTest.php +++ b/tests/OpdsPaginationTest.php @@ -1,5 +1,7 @@ feeds(manyFeeds()) ->get(); - $xml = XmlReader::make($opds->getResponse()->getContent()) + $xml = XmlReader::make($opds->getResponse()->getContents()) ->find('entry'); expect($xml)->toBeArray(); @@ -19,10 +21,27 @@ ->feeds(manyFeeds()) ->get(); - $xml = XmlReader::make($opds->getResponse()->getContent()) + $xml = XmlReader::make($opds->getResponse()->getContents()) ->find('entry'); $first = $xml[0]; + $pagination = []; + $links = XmlReader::make($opds->getResponse()->getContents()) + ->find('link', strict: false); + + foreach ($links as $link) { + $attrs = XmlReader::parseAttributes($link); + if (str_contains($attrs['href'], 'maximumRecords')) { + $pagination[$attrs['rel']] = $attrs; + } + } + + // expect(count($pagination))->toBe(4); + // expect($pagination['first']['href'])->toBe('http://localhost:8000/opds?startRecord=0&maximumRecords=32'); + // expect($pagination['last']['href'])->toBe('http://localhost:8000/opds?startRecord=96&maximumRecords=32'); + // expect($pagination['next']['href'])->toBe('http://localhost:8000/opds?startRecord=64&maximumRecords=32'); + // expect($pagination['previous']['href'])->toBe('http://localhost:8000/opds?startRecord=-64&maximumRecords=32'); + expect($xml)->toBeArray(); expect(count($xml))->toBe(32); expect($first['id'])->toBe('32'); @@ -33,9 +52,31 @@ ->feeds(manyFeeds(10)) ->get(); - $xml = XmlReader::make($opds->getResponse()->getContent()) + $xml = XmlReader::make($opds->getResponse()->getContents()) ->find('entry'); expect($xml)->toBeArray(); expect(count($xml))->toBe(10); }); + +it('can use paginator', function () { + $opds = Opds::make(getConfig()->usePagination()) + ->feeds(manyFeeds()) + ->get(); + + expect($opds->getPaginator())->toBeInstanceOf(OpdsPaginator::class); + + $paginator = $opds->getPaginator(); + expect($paginator->getOutput())->toBe(OpdsOutputEnum::xml); + expect($paginator->getUrl())->toBe('http://localhost/'); + expect($paginator->getQuery())->toBeArray(); + expect($paginator->usePagination())->toBeTrue(); + expect($paginator->getPerPage())->toBe(32); + expect($paginator->getPage())->toBe(1); + expect($paginator->getTotal())->toBe(100); + expect($paginator->getStart())->toBe(0); + expect($paginator->getStartRecord())->toBe(32); + expect($paginator->getSize())->toBe(4); + expect($paginator->getFirst())->toBe(0); + expect($paginator->getLast())->toBe(96); +}); diff --git a/tests/OpdsResponseTest.php b/tests/OpdsResponseTest.php index ac4093d..e931772 100644 --- a/tests/OpdsResponseTest.php +++ b/tests/OpdsResponseTest.php @@ -9,13 +9,11 @@ $opds = Opds::make(); $engine = OpdsXmlEngine::make($opds); - $engine->setContent([$html]); - $engine->setResponse($html); + $engine->setContents(['html' => $html]); - expect($engine->getContent())->toBe([$html]); - expect($engine->getResponse())->toBe($html); - expect(fn () => OpdsResponse::make($engine, 500))->toThrow(Exception::class); - expect(fn () => OpdsResponse::make($engine, 500))->toThrow('OPDS Response: invalid content'); + expect($engine->getContents())->toBe(['html' => $html]); + expect(fn () => OpdsResponse::make(json_encode($html), $opds->getOutput(), 500))->toThrow(Exception::class); + expect(fn () => OpdsResponse::make(json_encode($html), $opds->getOutput(), 500))->toThrow('OPDS Response: invalid content'); }); it('can use response', function () { @@ -26,17 +24,30 @@ expect($response->getStatus())->toBe(200); expect($response->isJson())->toBeFalse(); expect($response->isXml())->toBeTrue(); - expect($response->getContent())->toBeString(); + expect($response->getHeaders())->toBeArray(); + expect($response->getHeaders())->toHaveKey('Access-Control-Allow-Origin'); + expect($response->getHeaders())->toHaveKey('Content-Type'); + expect($response->getContents())->toBeString(); + + $response->setHeaders(['Content-Encoding' => 'gzip']); + expect($response->getHeaders())->toHaveKey('Content-Encoding'); }); it('can send response', function () { // @phpstan-ignore-line $opds = Opds::make(); $engine = OpdsXmlEngine::make($opds); - $engine->setContent([exampleXml()]); - $engine->setResponse(exampleXml()); - $response = OpdsResponse::make($engine, 200); - $response->response(send: false); + $response = OpdsResponse::make($engine, $opds->getOutput(), 200); + $response->setContents(exampleXml()); + + $response->send(mock: false); expect($opds)->toBeInstanceOf(Opds::class); })->expectOutputString(exampleXml()); + +it('can use response method', function () { + $opds = Opds::make() // @phpstan-ignore-line + ->send(mock: false); + + expect($opds)->toBeNull(); +}); diff --git a/tests/OpdsTest.php b/tests/OpdsTest.php index e64f957..a5a6226 100644 --- a/tests/OpdsTest.php +++ b/tests/OpdsTest.php @@ -1,5 +1,6 @@ get(); $response = $opds->getResponse(); - expect($response->getContent())->toBeString(); + expect($response->getContents())->toBeString(); }); it('is valid xml', function () { @@ -20,7 +21,7 @@ ->get(); $response = $opds->getResponse(); - expect(isValidXml($response->getContent()))->toBeTrue(); + expect(isValidXml($response->getContents()))->toBeTrue(); }); it('can be parsed', function () { @@ -28,7 +29,7 @@ ->get(); $response = $opds->getResponse(); - $xml = XmlReader::make($response->getContent())->toArray(); + $xml = XmlReader::make($response->getContents())->toArray(); expect($xml)->toBeArray(); }); @@ -44,7 +45,14 @@ expect($opds->getOutput())->toBe(OpdsOutputEnum::xml); expect($opds->getResponse())->toBeInstanceOf(OpdsResponse::class); expect($opds->getUrlParts())->toBeArray(); + expect($opds->getPaginator())->toBeInstanceOf(OpdsPaginator::class); +}); + +it('can use opds paginator', function () { + $opds = Opds::make() + ->title('feed'); + expect($opds->getPaginator())->toBeInstanceOf(OpdsPaginator::class); }); it('can use output', function () { @@ -65,6 +73,6 @@ $opds = Opds::make() ->title('feed'); - expect(fn () => $opds->url('http://localhost:8000/opds?version=1.0'))->toThrow(Exception::class); - expect(fn () => $opds->url('http://localhost:8000/opds?version=1.0'))->toThrow('OPDS version 1.0 is not supported.'); + expect(fn () => $opds->url('http://localhost:8000/opds?version=0.8'))->toThrow(Exception::class); + expect(fn () => $opds->url('http://localhost:8000/opds?version=0.8'))->toThrow('OPDS version 0.8 is not supported.'); }); diff --git a/tests/OpdsXmlTest.php b/tests/OpdsXmlTest.php index ebf373e..439286f 100644 --- a/tests/OpdsXmlTest.php +++ b/tests/OpdsXmlTest.php @@ -21,10 +21,10 @@ ->get(); $response = $opds->getResponse(); - $xml = XmlReader::make($response->getContent())->toArray(); + $xml = XmlReader::make($response->getContents())->toArray(); expect($xml)->toBeArray(); - expect($response->getContent())->toBeString(); + expect($response->getContents())->toBeString(); }); it('can be display feeds books', function () { @@ -33,7 +33,7 @@ ->get(); $response = $opds->getResponse(); - expect($response->getContent())->toBeString(); + expect($response->getContents())->toBeString(); }); it('can search', function () { @@ -42,7 +42,7 @@ ->get(); $response = $opds->getResponse(); - expect($response->getContent())->toBeString(); + expect($response->getContents())->toBeString(); }); it('can get opds from engine', function () { @@ -50,7 +50,7 @@ $xml = OpdsXmlEngine::make($opds); expect($xml->getOpds())->toBeInstanceOf(Opds::class); - expect($xml->getContent())->toBeArray(); + expect($xml->getContents())->toBeArray(); }); it('can use search', function () { @@ -59,7 +59,7 @@ ->get(); $response = $opds->getResponse(); - expect($response->getContent())->toBeString(); + expect($response->getContents())->toBeString(); }); it('can use search query', function () { @@ -69,7 +69,7 @@ ->get(); $response = $opds->getResponse(); - expect($response->getContent())->toBeString(); + expect($response->getContents())->toBeString(); }); it('can use navigation feeds', function () { @@ -78,5 +78,5 @@ ->get(); expect($opds)->toBeInstanceOf(Opds::class); - expect($opds->getEngine()->getContent())->toBeArray(); + expect($opds->getEngine()->getContents())->toBeArray(); });