├── LICENSE.md ├── README.md └── GoodReads.php /LICENSE.md: -------------------------------------------------------------------------------- 1 | # License Agreement 2 | 3 | Copyright (c) 2014, Daniel G Wood 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | * Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | * Redistributions in binary form must reproduce the above copyright notice, 12 | this list of conditions and the following disclaimer in the documentation 13 | and/or other materials provided with the distribution. 14 | 15 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 16 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 17 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 18 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 19 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 20 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 21 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 22 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 24 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | 26 | The views and conclusions contained in the software and documentation are those 27 | of the authors and should not be interpreted as representing official policies, 28 | either expressed or implied, of the FreeBSD Project. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | goodreads-api 2 | ============= 3 | 4 | A quick PHP wrapper class for the GoodReads API. 5 | 6 | **This repository is no longer maintained.** I stopped using GoodReads (moved to The StoryGraph), so I won't be updating this in future. **If you've created a newer/better PHP GoodReads API, let me know and I'll link it here.** 7 | 8 | --- 9 | 10 | Disclaimer 11 | ---------- 12 | This is something I hacked together quickly, it isn't beautiful, and can only access a few API endpoints. However it's a good starting point, and from a cursory glance implementing other methods wouldn't be particularly tricky. Caching support is also baked in. 13 | 14 | Please also bear in mind the GoodReads API itself isn't great (some methods for example only support XML!), you can read/comment on that in their forums. 15 | 16 | Requirements 17 | ------------ 18 | * PHP 5.3.x or higher 19 | * cURL 20 | * GoodReads API key 21 | 22 | Available methods 23 | ----------------- 24 | * [author.show](https://www.goodreads.com/api#author.show) 25 | * [author.books](https://www.goodreads.com/api#author.books) 26 | * [book.show](https://www.goodreads.com/api#book.show) 27 | * [book.show_by_isbn](https://www.goodreads.com/api#book.show_by_isbn) 28 | * [book.title](https://www.goodreads.com/api#book.title) 29 | * [reviews.list](https://www.goodreads.com/api#reviews.list) 30 | * [review.show](https://www.goodreads.com/api#review.show) 31 | * [user.show](https://www.goodreads.com/api#user.show) 32 | 33 | Usage 34 | ----- 35 | 1. Include class 36 | 2. Initialise wrapper `$api = new GoodReads('PUT YOUR API KEY HERE', 'writable directory for caching');` 37 | 3. Call a method `$data = $api->getLatestReads(4148474);` 38 | 39 | License 40 | ------- 41 | Simplified BSD (included). 42 | 43 | Changes 44 | ------- 45 | 2018-06-02 [Tyler Paulson](https://github.com/tyler-paulson) 46 | * Added function to get a user by username rather than ID. 47 | 48 | 2018-03-04 [Tyler Paulson](https://github.com/tyler-paulson) 49 | * Added function to get all of a user's books, read or otherwise. 50 | 51 | 2017-12-10 [Daniel G Wood](https://github.com/danielgwood) 52 | * Added 5 endpoints: author.books, book.show, book.show_by_isbn, book.title and review.show 53 | * Updated documentation. 54 | 55 | 2016-05-13 [Victor Davis](https://github.com/victordavis/goodreads-api) 56 | * Added LIBXML_NOCDATA arg to simplexml_load_string to capture CDATA in returned XML 57 | * Added 2 endpoints: author.show & user.show 58 | -------------------------------------------------------------------------------- /GoodReads.php: -------------------------------------------------------------------------------- 1 | 16 | */ 17 | class GoodReads 18 | { 19 | /** 20 | * Root URL of the API (no trailing slash). 21 | */ 22 | const API_URL = 'https://www.goodreads.com'; 23 | 24 | /** 25 | * How long do cached items live for? (seconds) 26 | */ 27 | const CACHE_TTL = 3600; 28 | 29 | /** 30 | * How long to sleep between requests to prevent flooding/TOS violation (milliseconds). 31 | */ 32 | const SLEEP_BETWEEN_REQUESTS = 1000; 33 | 34 | /** 35 | * @var string Your API key. 36 | */ 37 | protected $apiKey = ''; 38 | 39 | /** 40 | * @var string Cache directory (defaults to ./cache). 41 | */ 42 | protected $cacheDir = 'cache'; 43 | 44 | /** 45 | * @var integer When was the last request made? 46 | */ 47 | protected $lastRequestTime = 0; 48 | 49 | /** 50 | * Initialise the API wrapper instance. 51 | * 52 | * @param string $apiKey 53 | * @param string $cacheDirectory 54 | */ 55 | public function __construct($apiKey, $cacheDirectory = '') 56 | { 57 | $this->apiKey = (string)$apiKey; 58 | $this->cacheDir = (string)$cacheDirectory; 59 | $this->clearExpiredCache(); 60 | } 61 | 62 | /** 63 | * Get details for a given author. 64 | * 65 | * @param integer $authorId 66 | * @return array 67 | */ 68 | public function getAuthor($authorId) 69 | { 70 | return $this->request( 71 | 'author/show', 72 | array( 73 | 'key' => $this->apiKey, 74 | 'id' => (int)$authorId 75 | ) 76 | ); 77 | } 78 | 79 | /** 80 | * Get books by a given author. 81 | * 82 | * @param integer $authorId 83 | * @param integer $page Optional page offset, 1-N 84 | * @return array 85 | */ 86 | public function getBooksByAuthor($authorId, $page = 1) 87 | { 88 | return $this->request( 89 | 'author/list', 90 | array( 91 | 'key' => $this->apiKey, 92 | 'id' => (int)$authorId, 93 | 'page' => (int)$page 94 | ) 95 | ); 96 | } 97 | 98 | /** 99 | * Get details for a given book. 100 | * 101 | * @param integer $bookId 102 | * @return array 103 | */ 104 | public function getBook($bookId) 105 | { 106 | return $this->request( 107 | 'book/show', 108 | array( 109 | 'key' => $this->apiKey, 110 | 'id' => (int)$bookId 111 | ) 112 | ); 113 | } 114 | 115 | /** 116 | * Get details for a given book by ISBN. 117 | * 118 | * @param string $isbn 119 | * @return array 120 | */ 121 | public function getBookByISBN($isbn) 122 | { 123 | return $this->request( 124 | 'book/isbn/' . urlencode($isbn), 125 | array( 126 | 'key' => $this->apiKey 127 | ) 128 | ); 129 | } 130 | 131 | /** 132 | * Get details for a given book by title. 133 | * 134 | * @param string $title 135 | * @param string $author Optionally provide this for more accuracy. 136 | * @return array 137 | */ 138 | public function getBookByTitle($title, $author = '') 139 | { 140 | return $this->request( 141 | 'book/title', 142 | array( 143 | 'key' => $this->apiKey, 144 | 'title' => urlencode($title), 145 | 'author' => $author 146 | ) 147 | ); 148 | } 149 | 150 | /** 151 | * Get details for a given user. 152 | * 153 | * @param integer $userId 154 | * @return array 155 | */ 156 | public function getUser($userId) 157 | { 158 | return $this->request( 159 | 'user/show', 160 | array( 161 | 'key' => $this->apiKey, 162 | 'id' => (int)$userId 163 | ) 164 | ); 165 | } 166 | 167 | /** 168 | * Get details for a given user by username. 169 | * 170 | * @param string $username 171 | * @return array 172 | */ 173 | public function getUserByUsername($username) 174 | { 175 | return $this->request( 176 | 'user/show', 177 | array( 178 | 'key' => $this->apiKey, 179 | 'username' => $username 180 | ) 181 | ); 182 | } 183 | 184 | /** 185 | * Get details for of a particular review 186 | * 187 | * @param integer $reviewId 188 | * @param integer $page Optional page number of comments, 1-N 189 | * @return array 190 | */ 191 | public function getReview($reviewId, $page = 1) 192 | { 193 | return $this->request( 194 | 'review/show', 195 | array( 196 | 'key' => $this->apiKey, 197 | 'id' => (int)$reviewId, 198 | 'page' => (int)$page 199 | ) 200 | ); 201 | } 202 | 203 | /** 204 | * Get a shelf for a given user. 205 | * 206 | * @param integer $userId 207 | * @param string $shelf read|currently-reading|to-read etc 208 | * @param string $sort title|author|rating|year_pub|date_pub|date_read|date_added|avg_rating etc 209 | * @param integer $limit 1-200 210 | * @param integer $page 1-N 211 | * @return array 212 | */ 213 | public function getShelf($userId, $shelf, $sort = 'title', $limit = 100, $page = 1) 214 | { 215 | return $this->request( 216 | 'review/list', 217 | array( 218 | 'v' => 2, 219 | 'format' => 'xml', // :( GoodReads still doesn't support JSON for this endpoint 220 | 'key' => $this->apiKey, 221 | 'id' => (int)$userId, 222 | 'shelf' => $shelf, 223 | 'sort' => $sort, 224 | 'page' => $page, 225 | 'per_page' => $limit 226 | ) 227 | ); 228 | } 229 | 230 | /** 231 | * Get all books for a given user. 232 | * 233 | * @param integer $userId 234 | * @param string $sort title|author|rating|year_pub|date_pub|date_read|date_added|avg_rating etc 235 | * @param integer $limit 1-200 236 | * @param integer $page 1-N 237 | * @return array 238 | */ 239 | public function getAllBooks($userId, $sort = 'title', $limit = 100, $page = 1) 240 | { 241 | return $this->request( 242 | 'review/list', 243 | array( 244 | 'v' => 2, 245 | 'format' => 'xml', // :( GoodReads still doesn't support JSON for this endpoint 246 | 'key' => $this->apiKey, 247 | 'id' => (int)$userId, 248 | 'sort' => $sort, 249 | 'page' => $page, 250 | 'per_page' => $limit 251 | ) 252 | ); 253 | } 254 | 255 | /** 256 | * Get the details of an author. 257 | * 258 | * @param integer $authorId 259 | * @return array 260 | */ 261 | public function showAuthor($authorId) 262 | { 263 | return $this->getAuthor($authorId); 264 | } 265 | 266 | /** 267 | * Get the details of a user. 268 | * 269 | * @param integer $userId 270 | * @return array 271 | */ 272 | public function showUser($userId) 273 | { 274 | return $this->getUser($userId); 275 | } 276 | 277 | /** 278 | * Get the latest books read for a given user. 279 | * 280 | * @param integer $userId 281 | * @param string $sort title|author|rating|year_pub|date_pub|date_read|date_added|avg_rating etc 282 | * @param integer $limit 1-200 283 | * @param integer $page 1-N 284 | * @return array 285 | */ 286 | public function getLatestReads($userId, $sort = 'date_read', $limit = 100, $page = 1) 287 | { 288 | return $this->getShelf($userId, 'read', $sort, $limit, $page); 289 | } 290 | 291 | /** 292 | * Makes requests to the API. 293 | * 294 | * @param string $endpoint A GoodReads API function name 295 | * @param array $params Optional parameters 296 | * @return array 297 | * @throws Exception If it didn't work 298 | */ 299 | private function request($endpoint, array $params = array()) 300 | { 301 | // Check the cache 302 | $cachedData = $this->getCache($endpoint, $params); 303 | if($cachedData !== false) { 304 | return $cachedData; 305 | } 306 | 307 | // Prepare the URL and headers 308 | $url = self::API_URL .'/'. $endpoint . '?' . ((!empty($params)) ? http_build_query($params, '', '&') : ''); 309 | $headers = array( 310 | 'Accept: application/xml', 311 | ); 312 | if(isset($params['format']) && $params['format'] === 'json') { 313 | $headers = array( 314 | 'Accept: application/json', 315 | ); 316 | } 317 | 318 | // Execute via CURL 319 | $response = null; 320 | if(extension_loaded('curl')) { 321 | $ch = curl_init(); 322 | curl_setopt($ch, CURLOPT_URL, $url); 323 | curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); 324 | curl_setopt($ch, CURLOPT_HEADER, 1); 325 | curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); 326 | $response = curl_exec($ch); 327 | usleep(self::SLEEP_BETWEEN_REQUESTS); 328 | $headerSize = curl_getinfo($ch, CURLINFO_HEADER_SIZE); 329 | $header = substr($response, 0, $headerSize); 330 | $body = substr($response, $headerSize); 331 | $errorNumber = curl_errno($ch); 332 | $errorMessage = curl_error($ch); 333 | if($errorNumber > 0) 334 | { 335 | throw new Exception('Method failed: ' . $endpoint . ': ' . $errorMessage); 336 | } 337 | curl_close($ch); 338 | } else { 339 | throw new Exception('CURL library not loaded!'); 340 | } 341 | 342 | // Try and cadge the results into a half-decent array 343 | $results = null; 344 | if(isset($params['format']) && $params['format'] === 'json') { 345 | $results = json_decode($body); 346 | } else { 347 | $results = json_decode(json_encode((array)simplexml_load_string($body, 'SimpleXMLElement', LIBXML_NOCDATA)), 1); // I know, I'm a terrible human being 348 | } 349 | 350 | if($results !== null) { 351 | // Cache & return results 352 | $this->addCache($endpoint, $params, $results); 353 | return $results; 354 | } else { 355 | throw new Exception('Server error on "' . $url . '": ' . $response); 356 | } 357 | } 358 | 359 | /** 360 | * Attempt to get something from the cache. 361 | * 362 | * @param string $endpoint 363 | * @param array $params 364 | * @return array|false 365 | */ 366 | private function getCache($endpoint, array $params = array()) 367 | { 368 | if (file_exists($this->cacheDir) && is_writable($this->cacheDir)) { 369 | $filename = str_replace('/', '_', $endpoint) . '-' . md5(serialize($params)); 370 | $filename = $this->cacheDir . '/' . $filename; 371 | if(file_exists($filename)) { 372 | $contents = unserialize(file_get_contents($filename)); 373 | if(!is_array($contents) || $contents['cache_expiry'] <= time()) { 374 | unlink($filename); 375 | return false; 376 | } else { 377 | unset($contents['cache_expiry']); 378 | return $contents; 379 | } 380 | } 381 | return false; 382 | } else { 383 | throw new Exception('Cache directory not writable.'); 384 | } 385 | } 386 | 387 | /** 388 | * Add an item to the cache. 389 | * 390 | * @param string $endpoint 391 | * @param array $params 392 | * @param array $contents 393 | * @return boolean 394 | */ 395 | private function addCache($endpoint, array $params = array(), array $contents) 396 | { 397 | if (file_exists($this->cacheDir) && is_writable($this->cacheDir)) { 398 | $filename = str_replace('/', '_', $endpoint) . '-' . md5(serialize($params)); 399 | $filename = $this->cacheDir . '/' . $filename; 400 | $contents['cache_expiry'] = time() + self::CACHE_TTL; 401 | return file_put_contents($filename, serialize($contents)); 402 | } else { 403 | throw new Exception('Cache directory not writable.'); 404 | } 405 | } 406 | 407 | /** 408 | * Remove old cache items. 409 | */ 410 | private function clearExpiredCache() 411 | { 412 | if (file_exists($this->cacheDir) && is_writable($this->cacheDir)) { 413 | foreach (new DirectoryIterator($this->cacheDir) as $file) { 414 | if ($file->isDot()) { 415 | continue; 416 | } 417 | if (time() - $file->getCTime() >= self::CACHE_TTL) { 418 | unlink($file->getRealPath()); 419 | } 420 | } 421 | } else { 422 | throw new Exception('Cache directory not writable.'); 423 | } 424 | } 425 | } 426 | --------------------------------------------------------------------------------