From 2f9f82e5b4b5eb64f9e02370fe9153a46b2f7b0f Mon Sep 17 00:00:00 2001 From: Tim Meier Date: Mon, 13 Feb 2017 10:19:37 +0100 Subject: [PATCH] Fix tmdb limit reaching (#47) * Fix tmdb limit reaching * code review * remove unused fixtures --- backend/app/Services/TMDB.php | 115 +++++++++------------ backend/tests/Services/ItemServiceTest.php | 2 +- backend/tests/Services/TMDBTest.php | 26 ++++- backend/tests/fixtures/tmdb/multi.json | 30 ++++++ 4 files changed, 103 insertions(+), 70 deletions(-) create mode 100644 backend/tests/fixtures/tmdb/multi.json diff --git a/backend/app/Services/TMDB.php b/backend/app/Services/TMDB.php index 98fae01..a333c2b 100644 --- a/backend/app/Services/TMDB.php +++ b/backend/app/Services/TMDB.php @@ -8,6 +8,7 @@ use GuzzleHttp\Client; use Illuminate\Support\Collection; use Illuminate\Support\Facades\Cache; + use GuzzleHttp\Exception\ClientException; class TMDB { @@ -37,12 +38,8 @@ */ public function search($title) { - $response = $this->client->get($this->base . '/3/search/multi', [ - 'query' => [ - 'api_key' => $this->apiKey, - 'query' => $title, - 'language' => strtolower($this->translation) - ] + $response = $this->requestTmdb($this->base . '/3/search/multi', [ + 'query' => $title ]); return $this->createItems($response); @@ -79,12 +76,7 @@ */ private function searchSuggestions($mediaType, $tmdbID, $type) { - $response = $this->client->get($this->base . '/3/' . $mediaType . '/' . $tmdbID . '/' . $type, [ - 'query' => [ - 'api_key' => $this->apiKey, - 'language' => strtolower($this->translation) - ] - ]); + $response = $this->requestTmdb($this->base . '/3/' . $mediaType . '/' . $tmdbID . '/' . $type); return collect($this->createItems($response, $mediaType)); } @@ -97,12 +89,7 @@ public function upcoming() { return Cache::remember('upcoming', $this->untilEndOfDay(), function() { - $response = $this->client->get($this->base . '/3/movie/upcoming', [ - 'query' => [ - 'api_key' => $this->apiKey, - 'language' => strtolower($this->translation) - ] - ]); + $response = $this->requestTmdb($this->base . '/3/movie/upcoming'); $items = collect($this->createItems($response, 'movie')); @@ -156,12 +143,7 @@ private function fetchPopular($mediaType) { - return $this->client->get($this->base . '/3/' . $mediaType . '/popular', [ - 'query' => [ - 'api_key' => $this->apiKey, - 'language' => strtolower($this->translation) - ] - ]); + return $this->requestTmdb($this->base . '/3/' . $mediaType . '/popular'); } /** @@ -202,6 +184,34 @@ return $items; } + private function requestTmdb($url, $query = []) + { + $query = array_merge($query, [ + 'api_key' => $this->apiKey, + 'language' => strtolower($this->translation) + ]); + + try { + $response = $this->client->get($url, [ + 'query' => $query + ]); + + if($this->hasLimitRemaining($response)) { + return $response; + } + } catch (ClientException $e) { + // wtf? throws exception because of "bad" statuscode? + $response = $e->getResponse(); + + if($this->hasLimitRemaining($response)) { + return $response; + } + } + + sleep(1); + return $this->requestTmdb($url, $query); + } + /** * Get full movie or tv details. * @@ -211,20 +221,9 @@ */ public function details($tmdbId, $mediaType) { - $response = $this->client->get($this->base . '/3/' . $mediaType . '/' . $tmdbId, [ - 'query' => [ - 'api_key' => $this->apiKey, - 'language' => strtolower($this->translation) - ] - ]); + $response = $this->requestTmdb($this->base . '/3/' . $mediaType . '/' . $tmdbId); - if($this->hasLimitRemaining($response)) { - return json_decode($response->getBody()); - } - - // After 10 seconds the TMDB request limit is resetted. - sleep(10); - return $this->details($tmdbId, $mediaType); + return json_decode($response->getBody()); } /** @@ -237,12 +236,7 @@ private function tvSeasonsCount($id, $mediaType) { if($mediaType == 'tv') { - $response = $this->client->get($this->base . '/3/tv/' . $id, [ - 'query' => [ - 'api_key' => $this->apiKey, - 'language' => strtolower($this->translation) - ] - ]); + $response = $this->requestTmdb($this->base . '/3/tv/' . $id); $seasons = collect(json_decode($response->getBody())->seasons); @@ -267,12 +261,7 @@ $data = []; for($i = 1; $i <= $seasons; $i++) { - $response = $this->client->get($this->base . '/3/tv/' . $id . '/season/' . $i, [ - 'query' => [ - 'api_key' => $this->apiKey, - 'language' => strtolower($this->translation) - ] - ]); + $response = $this->requestTmdb($this->base . '/3/tv/' . $id . '/season/' . $i); $data[$i] = json_decode($response->getBody()); } @@ -290,28 +279,18 @@ { $response = $this->fetchAlternativeTitles($item); - if($this->hasLimitRemaining($response)) { - $body = json_decode($response->getBody()); + $body = json_decode($response->getBody()); - if(property_exists($body, 'titles') || property_exists($body, 'results')) { - return isset($body->titles) ? $body->titles : $body->results; - } - - return []; + if(property_exists($body, 'titles') || property_exists($body, 'results')) { + return isset($body->titles) ? $body->titles : $body->results; } - // After 10 seconds the TMDB request limit is resetted. - sleep(10); - return $this->getAlternativeTitles($item); + return []; } public function fetchAlternativeTitles($item) { - return $this->client->get($this->base . '/3/' . $item['media_type'] . '/' . $item['tmdb_id'] . '/alternative_titles', [ - 'query' => [ - 'api_key' => $this->apiKey - ] - ]); + return $this->requestTmdb($this->base . '/3/' . $item['media_type'] . '/' . $item['tmdb_id'] . '/alternative_titles'); } /** @@ -371,11 +350,15 @@ /** * @param $response - * @return int + * @return boolean */ public function hasLimitRemaining($response) { - return (int) $response->getHeader('X-RateLimit-Remaining')[0] > 1; + if($response->getStatusCode() == 429) { + return false; + } + + return ((int) $response->getHeader('X-RateLimit-Remaining')[0]) > 1; } /** diff --git a/backend/tests/Services/ItemServiceTest.php b/backend/tests/Services/ItemServiceTest.php index 82d3aa4..0452d44 100644 --- a/backend/tests/Services/ItemServiceTest.php +++ b/backend/tests/Services/ItemServiceTest.php @@ -128,4 +128,4 @@ $alternativeTitleMock = $this->mock(AlternativeTitleService::class); $alternativeTitleMock->shouldReceive('create')->once()->andReturn(true); } - } \ No newline at end of file + } diff --git a/backend/tests/Services/TMDBTest.php b/backend/tests/Services/TMDBTest.php index c996023..0b4af02 100644 --- a/backend/tests/Services/TMDBTest.php +++ b/backend/tests/Services/TMDBTest.php @@ -56,6 +56,26 @@ $this->assertArrayNotHasKey('rating', $trending[1]); } + /** @test */ + public function it_should_respect_request_limit() + { + $fixture = $this->tmdbFixtures('multi'); + + $mock = new MockHandler([ + new Response(429, []), + new Response(200, ['X-RateLimit-Remaining' => [40]], $fixture), + ]); + + $handler = HandlerStack::create($mock); + $this->app->instance(Client::class, new Client(['handler' => $handler])); + + $tmdb = app(TMDB::class); + $result = $tmdb->search('Avatar - Legend of Korra'); + + $this->assertCount(1, $result); + $this->assertArrayHasKey('tmdb_id', $result[0]); + } + private function in_array_r($item , $array){ return (bool) preg_match('/"' . $item . '"/i' , json_encode($array)); } @@ -63,11 +83,11 @@ private function createGuzzleMock($fixture, $fixture2 = null) { $mock = new MockHandler([ - new Response(200, [], $fixture), - new Response(200, [], $fixture2), + new Response(200, ['X-RateLimit-Remaining' => [40]], $fixture), + new Response(200, ['X-RateLimit-Remaining' => [40]], $fixture2), ]); $handler = HandlerStack::create($mock); $this->app->instance(Client::class, new Client(['handler' => $handler])); } - } \ No newline at end of file + } diff --git a/backend/tests/fixtures/tmdb/multi.json b/backend/tests/fixtures/tmdb/multi.json new file mode 100644 index 0000000..b398f1b --- /dev/null +++ b/backend/tests/fixtures/tmdb/multi.json @@ -0,0 +1,30 @@ +{ + "page": 1, + "results": [ + { + "poster_path": "\/8uOOycL6r4vqOT8tgw4behO5MmB.jpg", + "popularity": 4.791115, + "id": 33880, + "overview": "The Legend of Korra is an American animated television series that premiered on the Nickelodeon television network in 2012. It was created by Bryan Konietzko and Michael Dante DiMartino as a sequel to their series Avatar: The Last Airbender, which aired on Nickelodeon from 2005 to 2008. Several people involved with creating Avatar, including designer Joaquim Dos Santos and composers Jeremy Zuckerman and Benjamin Wynn, returned to work on The Legend of Korra.\n\nThe series is set in a fictional universe where some people can manipulate, or \"bend\", the elements of water, earth, fire, or air. Only one person, the \"Avatar\", can bend all four elements, and is responsible for maintaining balance in the world. The series follows Avatar Korra, the successor of Aang from the previous series, as she faces political and spiritual unrest in a modernizing world.\n\nThe series, whose style is strongly influenced by Japanese animation, has been a critical and commercial success. It obtained the highest audience total for an animated series in the United States in 2012. The series was praised by reviewers for its high production values and for addressing difficult sociopolitical issues such as social unrest and terrorism. It was initially conceived as a miniseries of 12 episodes, but it is now set to run for 52 episodes separated into four seasons, each of which tells a separate story.", + "backdrop_path": "\/r1oTzR9Ke7pICxe1eiP8ZjoGJju.jpg", + "vote_average": 7.52, + "media_type": "tv", + "first_air_date": "2012-04-14", + "origin_country": [ + "US" + ], + "genre_ids": [ + 10765, + 16, + 18, + 10751 + ], + "original_language": "en", + "vote_count": 44, + "name": "The Legend of Korra", + "original_name": "The Legend of Korra" + } + ], + "total_results": 1, + "total_pages": 1 +}