├── cache └── .gitkeep ├── .gitignore ├── .htaccess ├── routes ├── ErrorRoute.php ├── HomeRoute.php ├── FlickrRoute.php ├── GitHubRoute.php ├── GoodreadsRoute.php ├── StravaRoute.php ├── AbstractRoutes.php ├── SpotifyRoutes.php ├── LastFmRoutes.php └── Router.php ├── composer.json ├── index.php ├── config.sample.php ├── README.md ├── services ├── GoodreadsService.php ├── FlickrService.php ├── AbstractService.php ├── GitHubService.php ├── StravaService.php ├── SpotifyService.php └── LastFmService.php ├── middleware └── Cache.php └── composer.lock /cache/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor 2 | /cache/* 3 | !/cache/.gitkeep 4 | composer.phar 5 | config.php 6 | deploy.sh -------------------------------------------------------------------------------- /.htaccess: -------------------------------------------------------------------------------- 1 | 2 | RewriteEngine On 3 | RewriteBase / 4 | RewriteCond %{REQUEST_FILENAME} !-f 5 | RewriteCond %{REQUEST_FILENAME} !-d 6 | RewriteRule ^ index.php [QSA,L] 7 | -------------------------------------------------------------------------------- /routes/ErrorRoute.php: -------------------------------------------------------------------------------- 1 | respond('Route not found', 404); 19 | } 20 | } -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "require": { 3 | "slim/slim": "2.*", 4 | "jdorn/file-system-cache": "dev-master", 5 | "jwilsson/spotify-web-api-php": "0.8.*", 6 | "basvandorst/StravaPHP": "1.0.1" 7 | }, 8 | "autoload": { 9 | "psr-4": { 10 | "Api\\Middleware\\": "middleware/", 11 | "Api\\Services\\": "services/", 12 | "Api\\Routes\\": "routes/" 13 | } 14 | } 15 | } -------------------------------------------------------------------------------- /index.php: -------------------------------------------------------------------------------- 1 | add(new \Api\Middleware\Cache); 12 | 13 | // Set up routes 14 | $app->hook('slim.before.router', new \Api\Routes\Router(array( 15 | new \Api\Routes\HomeRoute, 16 | new \Api\Routes\GitHubRoute, 17 | new \Api\Routes\LastFmRoutes, 18 | new \Api\Routes\FlickrRoute, 19 | new \Api\Routes\GoodreadsRoute, 20 | new \Api\Routes\SpotifyRoutes, 21 | new \Api\Routes\StravaRoute, 22 | ))); 23 | 24 | // Go! 25 | $app->run(); -------------------------------------------------------------------------------- /config.sample.php: -------------------------------------------------------------------------------- 1 | array( 8 | 'id' => '' 9 | ), 10 | 'github' => array( 11 | 'handle' => '' 12 | ), 13 | 'goodreads' => 1, 14 | 'lastfm' => array( 15 | 'user' => '', 16 | 'api_key' => '' 17 | ), 18 | 'spotify' => array( 19 | 'client_id' => '', 20 | 'client_secret' => '', 21 | 'user' => '' 22 | ), 23 | 'strava' => array( 24 | 'client_id' => '', 25 | 'client_secret' => '', 26 | 'access_token' => '' 27 | ) 28 | ); -------------------------------------------------------------------------------- /routes/HomeRoute.php: -------------------------------------------------------------------------------- 1 | 'Home route (lists all available routes)', 18 | 'url' => '/', 19 | 'methods' => 'GET', 20 | 'classMethod' => 'getHomeRoute' 21 | ), 22 | ); 23 | 24 | /** 25 | * Return list of available routes 26 | * (stored in each route, parsed when creating routes in Router class) 27 | */ 28 | public function getHomeRoute () 29 | { 30 | $this->respond($this->app->routes); 31 | } 32 | } -------------------------------------------------------------------------------- /routes/FlickrRoute.php: -------------------------------------------------------------------------------- 1 | 'Flickr latest photos', 20 | 'url' => '/flickr/photos', 21 | 'methods' => 'GET', 22 | 'params' => 'limit', 23 | 'classMethod' => 'getPhotos' 24 | ), 25 | ); 26 | 27 | /** 28 | * Public route 29 | * Request data from service and respond 30 | */ 31 | public function getPhotos () 32 | { 33 | $flickrService = new FlickrService; 34 | $limit = $this->app->request->params('limit', 10); 35 | $data = $flickrService->getPhotos($limit); 36 | $this->respond($data); 37 | } 38 | } -------------------------------------------------------------------------------- /routes/GitHubRoute.php: -------------------------------------------------------------------------------- 1 | 'GitHub latest commits', 20 | 'url' => '/github/commits', 21 | 'methods' => 'GET', 22 | 'params' => 'limit', 23 | 'classMethod' => 'getCommits' 24 | ), 25 | ); 26 | 27 | /** 28 | * Public route 29 | * Request data from service and respond 30 | */ 31 | public function getCommits () 32 | { 33 | $githubService = new GitHubService; 34 | $limit = $this->app->request->params('limit', 1); 35 | $data = $githubService->getCommits($limit); 36 | $this->respond($data); 37 | } 38 | } -------------------------------------------------------------------------------- /routes/GoodreadsRoute.php: -------------------------------------------------------------------------------- 1 | 'Goodreads latest books read', 20 | 'url' => '/goodreads/books', 21 | 'methods' => 'GET', 22 | 'params' => 'limit', 23 | 'classMethod' => 'getBooks' 24 | ), 25 | ); 26 | 27 | /** 28 | * Public route 29 | * Request data from service and respond 30 | */ 31 | public function getBooks () 32 | { 33 | $goodreadsService = new GoodreadsService; 34 | $limit = $this->app->request->params('limit', 1); 35 | $data = $goodreadsService->getBooks($limit); 36 | $this->respond($data); 37 | } 38 | } -------------------------------------------------------------------------------- /routes/StravaRoute.php: -------------------------------------------------------------------------------- 1 | 'Strava latest activities', 20 | 'url' => '/strava/latest-activities', 21 | 'methods' => 'GET', 22 | 'params' => 'limit', 23 | 'classMethod' => 'getActivities' 24 | ), 25 | ); 26 | 27 | /** 28 | * Public route 29 | * Request data from service and respond 30 | */ 31 | public function getActivities () 32 | { 33 | $stravaService = new StravaService; 34 | $limit = $this->app->request->params('limit', 1); 35 | $data = $stravaService->getActivities($limit); 36 | $this->respond($data); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # folio-api 2 | 3 | API for my new 'folio 4 | 5 | ## Installation 6 | 7 | Install composer: 8 | 9 | ``` 10 | curl -s http://getcomposer.org/installer | php 11 | ``` 12 | 13 | Install packages: 14 | 15 | ``` 16 | php composer.phar install 17 | ``` 18 | 19 | Visit the site in a browser... 20 | 21 | 22 | ## Config 23 | 24 | There is a sample config file in the root of the repo: `config.sample.php` 25 | 26 | This file needs to be renamed (or duplicated) to `config.php` and add the appropriate keys needed for API access 27 | 28 | ## Cache 29 | 30 | The cache directory may need `777` permissions 31 | 32 | 33 | ## API Endpoints 34 | 35 | (Visit the home route in a browser for a list of available routes) 36 | 37 | 38 | ## Technologies 39 | 40 | - [Slim Framework](http://www.slimframework.com/) 41 | - [Composer](https://getcomposer.org/) 42 | - [File System Cache](https://github.com/jdorn/FileSystemCache) 43 | - [Spotify Web API (PHP)](http://jwilsson.github.io/spotify-web-api-php/) 44 | - [StravaPHP](https://github.com/basvandorst/StravaPHP/) 45 | 46 | ## APIs & Feeds 47 | 48 | - [Flickr](https://www.flickr.com/services/feeds/) 49 | - [GitHub](https://developer.github.com/v3/) 50 | - [Goodreads](https://www.goodreads.com/api) 51 | - [Last.fm](http://www.last.fm/api/feeds) 52 | - [Spotify](https://developer.spotify.com/web-api/) 53 | - [Strava](http://strava.github.io/api/) -------------------------------------------------------------------------------- /routes/AbstractRoutes.php: -------------------------------------------------------------------------------- 1 | app = \Slim\Slim::getInstance(); 34 | } 35 | 36 | /** 37 | * Expose public access to routes 38 | * 39 | * @return array available routes 40 | */ 41 | public function getRoutes () 42 | { 43 | return $this->routes; 44 | } 45 | 46 | /** 47 | * Output data 48 | * 49 | * @param mixed $data Data to output 50 | * @param int $status The HTTP status (if 200 this response will be cached) 51 | * 52 | * @return void 53 | */ 54 | public function respond ($data = '', $status = 200) 55 | { 56 | $this->app->response->setStatus($status); 57 | $this->app->response->headers->set('Content-Type', 'application/json'); 58 | $this->app->response->setBody(json_encode($data)); 59 | } 60 | } -------------------------------------------------------------------------------- /routes/SpotifyRoutes.php: -------------------------------------------------------------------------------- 1 | 'Spotify playlists', 20 | 'url' => '/spotify/playlists', 21 | 'methods' => 'GET', 22 | 'params' => 'limit', 23 | 'classMethod' => 'getPlaylists' 24 | ), 25 | array( 26 | 'name' => 'Spotify playlist tracklisting', 27 | 'url' => '/spotify/playlists/:id', 28 | 'methods' => 'GET', 29 | 'params' => 'id', 30 | 'classMethod' => 'getPlaylistTracks' 31 | ) 32 | ); 33 | 34 | /** 35 | * Public route 36 | * Request data from service and respond 37 | */ 38 | public function getPlaylists () 39 | { 40 | $spotifyService = new SpotifyService; 41 | $limit = $this->app->request->params('limit', 50); 42 | $data = $spotifyService->getPlaylists($limit); 43 | $this->respond($data); 44 | } 45 | 46 | /** 47 | * Public route 48 | * Request data from service and respond 49 | */ 50 | public function getPlaylistTracks ($id) 51 | { 52 | $spotifyService = new SpotifyService; 53 | $data = $spotifyService->getPlaylistTracks($id); 54 | $this->respond($data); 55 | } 56 | } -------------------------------------------------------------------------------- /routes/LastFmRoutes.php: -------------------------------------------------------------------------------- 1 | 'Last.fm latest tracks', 20 | 'url' => '/lastfm/latest-tracks', 21 | 'methods' => 'GET', 22 | 'params' => 'limit', 23 | 'classMethod' => 'getLatestTracks' 24 | ), 25 | array( 26 | 'name' => 'Last.fm top artists', 27 | 'url' => '/lastfm/top-artists', 28 | 'methods' => 'GET', 29 | 'params' => 'limit', 30 | 'classMethod' => 'getTopArtists' 31 | ) 32 | ); 33 | 34 | /** 35 | * Public route 36 | * Request data from service and respond 37 | */ 38 | public function getLatestTracks () 39 | { 40 | $lastFmService = new LastFmService; 41 | $limit = $this->app->request->params('limit', 1); 42 | $data = $lastFmService->getLatestTracks($limit); 43 | $this->respond($data); 44 | } 45 | 46 | /** 47 | * Public route 48 | * Request data from service and respond 49 | */ 50 | public function getTopArtists () 51 | { 52 | $lastFmService = new LastFmService; 53 | $limit = $this->app->request->params('limit', 20); 54 | $data = $lastFmService->getTopArtists($limit); 55 | $this->respond($data); 56 | } 57 | } -------------------------------------------------------------------------------- /services/GoodreadsService.php: -------------------------------------------------------------------------------- 1 | app->config('goodreads'); 28 | $this->url = sprintf($this->apiBase, $config); 29 | 30 | } 31 | 32 | /** 33 | * Perform service request 34 | * 35 | * @param int $limit Limit request to required number of responses 36 | * 37 | * @return mixed Parsed response 38 | */ 39 | public function getBooks ($limit) 40 | { 41 | $data = $this->request($this->url); 42 | return $this->parseBooks($data, $limit); 43 | } 44 | 45 | /** 46 | * Parse service response 47 | * 48 | * @param mixed $data Response received from service 49 | * @param int $limit Limit request to required number of responses 50 | * 51 | * @return mixed Parsed response 52 | */ 53 | protected function parseBooks ($data, $limit) 54 | { 55 | $return = array(); 56 | 57 | $books = simplexml_load_string($data); 58 | 59 | $counter = 0; 60 | 61 | foreach($books->channel->item as $book) { 62 | 63 | if ($counter++ == $limit) { 64 | break; 65 | } 66 | 67 | $return[] = array( 68 | 'title' => (string) $book->title, 69 | 'author' => (string) $book->author_name, 70 | 'image' => (string) $book->book_small_image_url, 71 | 'date' => (string) $book->user_date_added, 72 | 'rating' => (int) $book->user_rating 73 | ); 74 | } 75 | 76 | return $return; 77 | } 78 | } -------------------------------------------------------------------------------- /routes/Router.php: -------------------------------------------------------------------------------- 1 | app = \Slim\Slim::getInstance(); 36 | 37 | // create all routes 38 | $this->parseRoutes($routeClasses); 39 | $this->createErrorRoutes(); 40 | 41 | // make list of routes publicly accessible to other classes 42 | $this->app->routes = $this->routes; 43 | } 44 | 45 | /** 46 | * Parse and generate routes 47 | * 48 | * @param array $routeClasses Array of routes to instantiate 49 | * 50 | * @return void 51 | */ 52 | private function parseRoutes ($routeClasses) 53 | { 54 | foreach($routeClasses as $routeClass) { 55 | 56 | // some route classes have multiple routes 57 | foreach($routeClass->getRoutes() as $route) { 58 | 59 | // create route 60 | $this->app 61 | ->map($route['url'], array($routeClass, $route['classMethod'])) 62 | ->via($route['methods']); 63 | 64 | // store route for later use 65 | $this->routes[] = $route; 66 | } 67 | } 68 | } 69 | 70 | /** 71 | * Create catch-all route for any unmatched routes 72 | * 73 | * @return void 74 | */ 75 | private function createErrorRoutes () 76 | { 77 | $this->app->get('/:method', '\Api\Routes\ErrorRoute:getRouteNotFound') 78 | ->conditions(array('method' => '.+')); 79 | } 80 | } -------------------------------------------------------------------------------- /services/FlickrService.php: -------------------------------------------------------------------------------- 1 | 'json', 18 | 'nojsoncallback' => 1 19 | ); 20 | 21 | /** 22 | * Service request base 23 | * Will be appended to with params, etc 24 | * 25 | * @param string $apiBase 26 | */ 27 | protected $apiBase = 'https://api.flickr.com/services/feeds/photos_public.gne?'; 28 | 29 | /** 30 | * Prepare service 31 | * Retrieve config data and prep params 32 | * 33 | * @return void 34 | */ 35 | public function configureService () 36 | { 37 | $params = $this->app->config('flickr'); 38 | $this->params = array_merge($this->params, $params); 39 | } 40 | 41 | /** 42 | * Perform service request 43 | * 44 | * @param int $limit Limit request to required number of responses 45 | * 46 | * @return mixed Parsed response 47 | */ 48 | public function getPhotos ($limit) 49 | { 50 | $data = $this->constructRequest(); 51 | return $this->parsePhotos($data, $limit); 52 | } 53 | 54 | /** 55 | * Parse service response 56 | * 57 | * @param mixed $data Response received from service 58 | * @param int $limit Limit request to required number of responses 59 | * 60 | * @return mixed Parsed response 61 | */ 62 | protected function parsePhotos ($data, $limit) 63 | { 64 | $return = array(); 65 | 66 | // fix flickr invalid JSON response 67 | $data = str_replace('\\\'', '\'', $data); 68 | 69 | $photos = json_decode($data); 70 | 71 | $counter = 0; 72 | 73 | foreach($photos->items as $photo) { 74 | 75 | if ($counter++ == $limit) { 76 | break; 77 | } 78 | 79 | $return[] = array( 80 | 'title' => $photo->title, 81 | 'url' => $photo->link, 82 | 'image' => $photo->media->m, 83 | 'date' => $photo->published 84 | ); 85 | } 86 | 87 | return $return; 88 | } 89 | } -------------------------------------------------------------------------------- /services/AbstractService.php: -------------------------------------------------------------------------------- 1 | app = \Slim\Slim::getInstance(); 56 | $this->configureService(); 57 | } 58 | 59 | /** 60 | * Every service needs configuring before use - 61 | * Set up Service URL, headers, etc 62 | */ 63 | abstract function configureService (); 64 | 65 | /** 66 | * Add additional headers for cURL requests 67 | * 68 | * @param array $headers 69 | * 70 | * @return void 71 | */ 72 | protected function addHeaders ($headers) 73 | { 74 | $this->headers = $headers; 75 | } 76 | 77 | /** 78 | * Merge any pre-defined parameters with others specified for the route 79 | * and request data 80 | * 81 | * @param array $params If set, combine with pre-defined params 82 | * 83 | * @return mixed Data returned from the service request 84 | */ 85 | protected function constructRequest ($params = null) 86 | { 87 | if ($params) { 88 | $this->params = array_merge($this->params, $params); 89 | } 90 | $url = $this->apiBase . http_build_query($this->params); 91 | return $this->request($url); 92 | } 93 | 94 | /** 95 | * Perform service request 96 | * 97 | * @param string $url URL to request data from 98 | * 99 | * @return mixed $context Data returned from the service 100 | */ 101 | protected function request ($url) 102 | { 103 | $ch = curl_init(); 104 | curl_setopt($ch, CURLOPT_URL, $url); 105 | curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); 106 | 107 | if ($this->headers) { 108 | curl_setopt($ch, CURLOPT_HTTPHEADER, $this->headers); 109 | } 110 | 111 | $contents = curl_exec($ch); 112 | curl_close($ch); 113 | if ($contents) { 114 | return $contents; 115 | } else { 116 | return false; 117 | } 118 | } 119 | } -------------------------------------------------------------------------------- /services/GitHubService.php: -------------------------------------------------------------------------------- 1 | app->config('github'); 20 | $this->url = sprintf('https://api.github.com/users/%s/events/public', $config['handle']); 21 | $this->configureHeaders(); 22 | } 23 | 24 | /** 25 | * Prepare additional headers to add to the request 26 | * 27 | * @return void 28 | */ 29 | public function configureHeaders () 30 | { 31 | $config = $this->app->config('github'); 32 | 33 | $headers = array( 34 | 'Accept: application/vnd.github.v3+json', 35 | sprintf('User-Agent: %s', $config['handle']) 36 | ); 37 | 38 | $this->addHeaders($headers); 39 | } 40 | 41 | /** 42 | * Perform service request 43 | * 44 | * @param int $limit Limit request to required number of responses 45 | * 46 | * @return mixed Parsed response 47 | */ 48 | public function getCommits ($limit) 49 | { 50 | $data = $this->request($this->url); 51 | return $this->parseCommits($data, $limit); 52 | } 53 | 54 | /** 55 | * Parse service response 56 | * 57 | * @param mixed $data Response received from service 58 | * @param int $limit Limit request to required number of responses 59 | * 60 | * @return mixed Parsed response 61 | */ 62 | protected function parseCommits ($data, $limit) 63 | { 64 | $return = array(); 65 | 66 | $commits = json_decode($data); 67 | 68 | $counter = 0; 69 | 70 | foreach($commits as $commit) { 71 | 72 | if ($counter++ == $limit) { 73 | break; 74 | } 75 | 76 | // interaction type 77 | switch ($commit->type) { 78 | case 'CreateEvent': 79 | $type = 'Created'; 80 | break; 81 | case 'PushEvent': 82 | $type = 'Pushed to'; 83 | break; 84 | case 'IssuesEvent': 85 | if ($commit->payload->action == 'opened') { 86 | $type = sprintf('Created issue %d', $commit->payload->issue->number); 87 | } else if ($commit->payload->action == 'closed') { 88 | $type = sprintf('Closed issue %d', $commit->payload->issue->number); 89 | } else { 90 | $type = sprintf('Referenced issue %d', $commit->payload->issue->number); 91 | } 92 | break; 93 | case 'IssueCommentEvent': 94 | $type = sprintf('Commented on issue %d', $commit->payload->issue->number); 95 | break; 96 | default: 97 | $type = 'Worked on'; 98 | break; 99 | } 100 | 101 | $return[] = array( 102 | 'type' => $type, 103 | 'title' => $commit->repo->name, 104 | 'url' => sprintf('https://github.com/%s', $commit->repo->name), 105 | 'date' => $commit->created_at 106 | ); 107 | } 108 | 109 | return $return; 110 | } 111 | } -------------------------------------------------------------------------------- /services/StravaService.php: -------------------------------------------------------------------------------- 1 | config = $this->app->config('strava'); 39 | 40 | // connect to service 41 | $adapter = new Pest('https://www.strava.com/api/v3'); 42 | $service = new REST($this->config['access_token'], $adapter); 43 | $this->stravaApi = new Client($service); 44 | } 45 | 46 | /** 47 | * Perform service request 48 | * 49 | * @param int $limit Limit request to required number of responses 50 | * 51 | * @return mixed Parsed response 52 | */ 53 | public function getActivities ($limit) 54 | { 55 | $activities = $this->stravaApi->getAthleteActivities(null, null, 1, $limit); 56 | return $this->parseActivities($activities); 57 | } 58 | 59 | /** 60 | * Parse service response 61 | * 62 | * @param mixed $activities Response received from service 63 | * 64 | * @return mixed Parsed response 65 | */ 66 | protected function parseActivities ($activities) 67 | { 68 | $return = array(); 69 | 70 | foreach($activities as $activity) { 71 | 72 | $return[] = array( 73 | 'id' => $activity['id'], 74 | 'name' => $activity['name'], 75 | 'location' => $this->getSegmentLocation($activity), 76 | 'date' => $activity['start_date_local'], 77 | 'type' => $activity['type'], 78 | 'distance' => sprintf('%skm', round($activity['distance'] * 0.001, 2)), 79 | 'time' => $this->secondsToTime($activity['moving_time']), 80 | 'url' => sprintf('https://strava.com/activities/%d', $activity['id']), 81 | ); 82 | } 83 | 84 | return $return; 85 | } 86 | 87 | /** 88 | * Turn time in seconds into nicer format 89 | * 90 | * @param int $seconds Time in seconds 91 | * 92 | * @return string 93 | */ 94 | protected function secondsToTime ($seconds) { 95 | $dtF = new \DateTime("@0"); 96 | $dtT = new \DateTime("@$seconds"); 97 | return $dtF->diff($dtT)->format('%h:%I:%S'); 98 | } 99 | 100 | /** 101 | * Attempt to retrieve location, from segments 102 | * 103 | * @param obj $activity Actitivty object 104 | * 105 | * @return string 106 | */ 107 | protected function getSegmentLocation ($activity) { 108 | $activityDetail = $this->stravaApi->getActivity($activity['id'], true); 109 | $location = ''; 110 | if (isset($activityDetail['segment_efforts']) && count($activityDetail['segment_efforts']) > 0) { 111 | $location = $activityDetail['segment_efforts'][0]['segment']['city']; 112 | } 113 | return $location; 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /services/SpotifyService.php: -------------------------------------------------------------------------------- 1 | config = $this->app->config('spotify'); 34 | 35 | // connect to Spotify 36 | $session = new \SpotifyWebAPI\Session($this->config['client_id'], $this->config['client_secret'], '/'); 37 | $this->spotifyApi = new \SpotifyWebAPI\SpotifyWebAPI(); 38 | $session->requestCredentialsToken(); 39 | $accessToken = $session->getAccessToken(); 40 | $this->spotifyApi->setAccessToken($accessToken); 41 | } 42 | 43 | /** 44 | * Perform service request 45 | * 46 | * @param int $limit Limit request to required number of responses 47 | * 48 | * @return mixed Parsed response 49 | */ 50 | public function getPlaylists ($limit) 51 | { 52 | $data = $this->spotifyApi->getUserPlaylists($this->config['user'], array( 53 | 'limit' => $limit 54 | )); 55 | 56 | return $this->parsePlaylists($data); 57 | } 58 | 59 | /** 60 | * Perform service request 61 | * 62 | * @param int $limit Limit request to required number of responses 63 | * 64 | * @return mixed Parsed response 65 | */ 66 | public function getPlaylistTracks ($id) 67 | { 68 | $data = $this->spotifyApi->getUserPlaylistTracks($this->config['user'], $id); 69 | return $this->parsePlaylistTracks($data); 70 | } 71 | 72 | /** 73 | * Parse service response 74 | * 75 | * @param mixed $data Response received from service 76 | * 77 | * @return mixed Parsed response 78 | */ 79 | protected function parsePlaylists ($data) 80 | { 81 | $return = array(); 82 | 83 | foreach($data->items as $playlist) { 84 | 85 | $return[] = array( 86 | 'id' => $playlist->id, 87 | 'title' => $playlist->name, 88 | 'url' => $playlist->external_urls->spotify, 89 | 'image' => $playlist->images[count($playlist->images) -1]->url, 90 | 'tracks' => $playlist->tracks->total, 91 | ); 92 | } 93 | 94 | return $return; 95 | } 96 | 97 | 98 | /** 99 | * Parse service response 100 | * 101 | * @param mixed $data Response received from service 102 | * 103 | * @return mixed Parsed response 104 | */ 105 | protected function parsePlaylistTracks ($data) 106 | { 107 | $return = array(); 108 | 109 | foreach($data->items as $track) { 110 | 111 | $artists = array(); 112 | foreach($track->track->artists as $artist) { 113 | $artists[] = $artist->name; 114 | } 115 | 116 | $return[] = array( 117 | 'url' => $track->track->external_urls->spotify, 118 | 'title' => $track->track->name, 119 | 'artist' => join($artists, ' / '), 120 | 'album' => $track->track->album->name, 121 | 'duration' => $track->track->duration_ms, 122 | 'date' => $track->added_at 123 | ); 124 | } 125 | 126 | return $return; 127 | } 128 | } -------------------------------------------------------------------------------- /middleware/Cache.php: -------------------------------------------------------------------------------- 1 | ttl = ($ttl) ? $ttl : 60 * 5; 39 | 40 | // set cache directory to initialisation value (or default) 41 | $this->cacheDir = ($cacheDir) ? $cacheDir : (__DIR__ . '/../cache'); 42 | 43 | // set up cache 44 | \FileSystemCache::$cacheDir = $this->cacheDir; 45 | } 46 | 47 | /** 48 | * Middleware call method 49 | * Activated before requests hit the router 50 | * 51 | * Returns cached response if cache exists 52 | * Caches response and passes through to the next step if no cache exists 53 | * 54 | * @return void 55 | */ 56 | public function call () 57 | { 58 | $app = $this->app; 59 | $request = $app->request; 60 | $response = $app->response; 61 | 62 | // retrieve request path (with params) 63 | $url = $request->getResourceUri(); 64 | $params = $request->params(); 65 | 66 | // check if cached response exists 67 | $cachedResponse = $this->fetch($url, $params); 68 | 69 | // cache found, return cached content 70 | if ($cachedResponse) { 71 | $response->headers->set('Content-Type', $cachedResponse['content_type']); 72 | $response->setStatus($cachedResponse['status']); 73 | $response->setBody($cachedResponse['body']); 74 | return; 75 | } 76 | 77 | // cache not found, continue 78 | $this->next->call(); 79 | 80 | // cache successful results for next time 81 | if ($response->getStatus() === 200) { 82 | $this->store($url, $params, array( 83 | 'content_type' => $response->headers->get('Content-Type'), 84 | 'status' => $response->getStatus(), 85 | 'body' => $response->getBody() 86 | )); 87 | } 88 | } 89 | 90 | /** 91 | * Attempt to retrieve a cached response 92 | * 93 | * @param string $url The request URL 94 | * @param array $params Any parameters at the end of the URL 95 | * 96 | * @return array/null $data The cached data, if set 97 | */ 98 | protected function fetch($url, $params) 99 | { 100 | $encodedUrl = $this->encodeUrlWithParams($url, $params); 101 | $key = \FileSystemCache::generateCacheKey($encodedUrl); 102 | 103 | $data = \FileSystemCache::retrieve($key); 104 | 105 | return $data; 106 | } 107 | 108 | /** 109 | * Store a repsonse 110 | * 111 | * @param string $url The request URL 112 | * @param array $params Any parameters at the end of the URL 113 | * @param array $data The data to store 114 | * 115 | * @return void 116 | */ 117 | protected function store($url, $params, $data) 118 | { 119 | $encodedUrl = $this->encodeUrlWithParams($url, $params); 120 | $key = \FileSystemCache::generateCacheKey($encodedUrl); 121 | 122 | \FileSystemCache::store($key, $data, $this->ttl); 123 | } 124 | 125 | /** 126 | * Encode a URL with params 127 | * Allows cache to differentiate requests with different params 128 | * 129 | * @param string $url The request URL 130 | * @param array $params Any parameters at the end of the URL 131 | * 132 | * @return string $url URL encoded with params 133 | */ 134 | private function encodeUrlWithParams ($url, $params) 135 | { 136 | if ($params) { 137 | $url .= '?' . http_build_query($params); 138 | } 139 | return $url; 140 | } 141 | 142 | } -------------------------------------------------------------------------------- /services/LastFmService.php: -------------------------------------------------------------------------------- 1 | 'json'); 17 | 18 | /** 19 | * Service request base 20 | * Will be appended to with params, etc 21 | * 22 | * @param string $apiBase 23 | */ 24 | protected $apiBase = 'http://ws.audioscrobbler.com/2.0/?'; 25 | 26 | /** 27 | * Prepare service 28 | * Retrieve config data and prep params 29 | * 30 | * @return void 31 | */ 32 | public function configureService () 33 | { 34 | $params = $this->app->config('lastfm'); 35 | $this->params = array_merge($this->params, $params); 36 | } 37 | 38 | /** 39 | * Perform service request 40 | * 41 | * @param int $limit Limit request to required number of responses 42 | * 43 | * @return mixed Parsed response 44 | */ 45 | public function getLatestTracks ($limit) 46 | { 47 | $params = array( 48 | 'method' => 'user.getrecenttracks', 49 | 'limit' => $limit 50 | ); 51 | $data = $this->constructRequest($params); 52 | return $this->parseLatestTracks($data, $limit); 53 | } 54 | 55 | /** 56 | * Perform service request 57 | * 58 | * @param int $limit Limit request to required number of responses 59 | * 60 | * @return mixed Parsed response 61 | */ 62 | public function getTopArtists ($limit) 63 | { 64 | $params = array( 65 | 'method' => 'user.gettopartists', 66 | 'limit' => $limit 67 | ); 68 | $data = $this->constructRequest($params); 69 | return $this->parseTopArtists($data); 70 | } 71 | 72 | /** 73 | * Parse service response 74 | * 75 | * @param mixed $data Response received from service 76 | * @param int $limit Limit request to required number of responses 77 | * 78 | * @return mixed Parsed response 79 | */ 80 | private function parseLatestTracks ($data, $limit) 81 | { 82 | $return = array(); 83 | 84 | $latestTracks = json_decode($data); 85 | 86 | $tracks = $latestTracks->recenttracks->track; 87 | if (!is_array($tracks)) { 88 | $tracks = array($tracks); 89 | } 90 | 91 | foreach($tracks as $count => $track) { 92 | 93 | // ensure we don't exceed the requested number of items to return 94 | if ($count >= $limit) { 95 | return $return; 96 | } 97 | 98 | // if the track is currently playing there will be no date 99 | if (isset($track->date)) { 100 | $date = $track->date->{'#text'}; 101 | } else { 102 | $date = date(DATE_W3C); 103 | } 104 | 105 | $return[] = array( 106 | 'artist' => $track->artist->{'#text'}, 107 | 'title' => $track->name, 108 | 'date' => $date 109 | ); 110 | } 111 | 112 | return $return; 113 | } 114 | 115 | /** 116 | * Parse service response 117 | * 118 | * @param mixed $data Response received from service 119 | * 120 | * @return mixed Parsed response 121 | */ 122 | private function parseTopArtists ($data) 123 | { 124 | $return = array(); 125 | 126 | $topArtists = json_decode($data); 127 | 128 | $artists = $topArtists->topartists->artist; 129 | if (!is_array($artists)) { 130 | $artists = array($artists); 131 | } 132 | 133 | $topPlayCount = null; 134 | 135 | foreach($artists as $artist) { 136 | 137 | if (!$topPlayCount) { 138 | $topPlayCount = $artist->playcount; 139 | } 140 | 141 | $return[] = array( 142 | 'artist' => $artist->name, 143 | 'playcount' => $artist->playcount, 144 | 'playcount_percentage' => round(($artist->playcount / $topPlayCount) * 100), 145 | 'image' => $artist->image[0]->{'#text'} 146 | ); 147 | } 148 | 149 | return $return; 150 | } 151 | } -------------------------------------------------------------------------------- /composer.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_readme": [ 3 | "This file locks the dependencies of your project to a known state", 4 | "Read more about it at http://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", 5 | "This file is @generated automatically" 6 | ], 7 | "hash": "25b60c4776edf2f7ed33055283e31fd1", 8 | "packages": [ 9 | { 10 | "name": "basvandorst/stravaphp", 11 | "version": "1.0.1", 12 | "source": { 13 | "type": "git", 14 | "url": "https://github.com/basvandorst/StravaPHP.git", 15 | "reference": "296c6d0a317390acc233d100b4a0630e8bcc723f" 16 | }, 17 | "dist": { 18 | "type": "zip", 19 | "url": "https://api.github.com/repos/basvandorst/StravaPHP/zipball/296c6d0a317390acc233d100b4a0630e8bcc723f", 20 | "reference": "296c6d0a317390acc233d100b4a0630e8bcc723f", 21 | "shasum": "" 22 | }, 23 | "require": { 24 | "educoder/pest": "1.0.0", 25 | "league/oauth2-client": "0.8.1" 26 | }, 27 | "type": "library", 28 | "autoload": { 29 | "psr-0": { 30 | "Strava": "src/" 31 | } 32 | }, 33 | "notification-url": "https://packagist.org/downloads/", 34 | "license": [ 35 | "MIT" 36 | ], 37 | "authors": [ 38 | { 39 | "name": "Bas van Dorst", 40 | "email": "basvandorst@gmail.com" 41 | } 42 | ], 43 | "description": "Strava V3 API PHP client with OAuth authentication", 44 | "keywords": [ 45 | "StravaPHP", 46 | "api", 47 | "oauth", 48 | "php", 49 | "strava" 50 | ], 51 | "time": "2015-02-23 08:57:10" 52 | }, 53 | { 54 | "name": "educoder/pest", 55 | "version": "1.0.0", 56 | "source": { 57 | "type": "git", 58 | "url": "https://github.com/educoder/pest.git", 59 | "reference": "7c4d24d6aafc4ebac711c73cc8797d9b1779525b" 60 | }, 61 | "dist": { 62 | "type": "zip", 63 | "url": "https://api.github.com/repos/educoder/pest/zipball/7c4d24d6aafc4ebac711c73cc8797d9b1779525b", 64 | "reference": "7c4d24d6aafc4ebac711c73cc8797d9b1779525b", 65 | "shasum": "" 66 | }, 67 | "require": { 68 | "lib-curl": "*", 69 | "php": ">=5.2" 70 | }, 71 | "type": "library", 72 | "autoload": { 73 | "classmap": [ 74 | "" 75 | ] 76 | }, 77 | "notification-url": "https://packagist.org/downloads/", 78 | "license": [ 79 | "MIT" 80 | ], 81 | "authors": [ 82 | { 83 | "name": "Matt Zukowski", 84 | "email": "matt@zukowski.ca" 85 | }, 86 | { 87 | "name": "Donald Sipe", 88 | "email": "donald.sipe@gmail.com" 89 | } 90 | ], 91 | "description": "A proper REST client for PHP.", 92 | "homepage": "https://github.com/educoder/pest", 93 | "time": "2013-11-17 12:58:01" 94 | }, 95 | { 96 | "name": "guzzle/guzzle", 97 | "version": "v3.9.3", 98 | "source": { 99 | "type": "git", 100 | "url": "https://github.com/guzzle/guzzle3.git", 101 | "reference": "0645b70d953bc1c067bbc8d5bc53194706b628d9" 102 | }, 103 | "dist": { 104 | "type": "zip", 105 | "url": "https://api.github.com/repos/guzzle/guzzle3/zipball/0645b70d953bc1c067bbc8d5bc53194706b628d9", 106 | "reference": "0645b70d953bc1c067bbc8d5bc53194706b628d9", 107 | "shasum": "" 108 | }, 109 | "require": { 110 | "ext-curl": "*", 111 | "php": ">=5.3.3", 112 | "symfony/event-dispatcher": "~2.1" 113 | }, 114 | "replace": { 115 | "guzzle/batch": "self.version", 116 | "guzzle/cache": "self.version", 117 | "guzzle/common": "self.version", 118 | "guzzle/http": "self.version", 119 | "guzzle/inflection": "self.version", 120 | "guzzle/iterator": "self.version", 121 | "guzzle/log": "self.version", 122 | "guzzle/parser": "self.version", 123 | "guzzle/plugin": "self.version", 124 | "guzzle/plugin-async": "self.version", 125 | "guzzle/plugin-backoff": "self.version", 126 | "guzzle/plugin-cache": "self.version", 127 | "guzzle/plugin-cookie": "self.version", 128 | "guzzle/plugin-curlauth": "self.version", 129 | "guzzle/plugin-error-response": "self.version", 130 | "guzzle/plugin-history": "self.version", 131 | "guzzle/plugin-log": "self.version", 132 | "guzzle/plugin-md5": "self.version", 133 | "guzzle/plugin-mock": "self.version", 134 | "guzzle/plugin-oauth": "self.version", 135 | "guzzle/service": "self.version", 136 | "guzzle/stream": "self.version" 137 | }, 138 | "require-dev": { 139 | "doctrine/cache": "~1.3", 140 | "monolog/monolog": "~1.0", 141 | "phpunit/phpunit": "3.7.*", 142 | "psr/log": "~1.0", 143 | "symfony/class-loader": "~2.1", 144 | "zendframework/zend-cache": "2.*,<2.3", 145 | "zendframework/zend-log": "2.*,<2.3" 146 | }, 147 | "suggest": { 148 | "guzzlehttp/guzzle": "Guzzle 5 has moved to a new package name. The package you have installed, Guzzle 3, is deprecated." 149 | }, 150 | "type": "library", 151 | "extra": { 152 | "branch-alias": { 153 | "dev-master": "3.9-dev" 154 | } 155 | }, 156 | "autoload": { 157 | "psr-0": { 158 | "Guzzle": "src/", 159 | "Guzzle\\Tests": "tests/" 160 | } 161 | }, 162 | "notification-url": "https://packagist.org/downloads/", 163 | "license": [ 164 | "MIT" 165 | ], 166 | "authors": [ 167 | { 168 | "name": "Michael Dowling", 169 | "email": "mtdowling@gmail.com", 170 | "homepage": "https://github.com/mtdowling" 171 | }, 172 | { 173 | "name": "Guzzle Community", 174 | "homepage": "https://github.com/guzzle/guzzle/contributors" 175 | } 176 | ], 177 | "description": "PHP HTTP client. This library is deprecated in favor of https://packagist.org/packages/guzzlehttp/guzzle", 178 | "homepage": "http://guzzlephp.org/", 179 | "keywords": [ 180 | "client", 181 | "curl", 182 | "framework", 183 | "http", 184 | "http client", 185 | "rest", 186 | "web service" 187 | ], 188 | "time": "2015-03-18 18:23:50" 189 | }, 190 | { 191 | "name": "jdorn/file-system-cache", 192 | "version": "dev-master", 193 | "source": { 194 | "type": "git", 195 | "url": "https://github.com/jdorn/FileSystemCache.git", 196 | "reference": "08f33ff840ec4728a0bd28e4d68d97635584952e" 197 | }, 198 | "dist": { 199 | "type": "zip", 200 | "url": "https://api.github.com/repos/jdorn/FileSystemCache/zipball/08f33ff840ec4728a0bd28e4d68d97635584952e", 201 | "reference": "08f33ff840ec4728a0bd28e4d68d97635584952e", 202 | "shasum": "" 203 | }, 204 | "require": { 205 | "php": ">=5.3.0" 206 | }, 207 | "type": "library", 208 | "extra": { 209 | "branch-alias": { 210 | "dev-master": "1.0.x-dev" 211 | } 212 | }, 213 | "autoload": { 214 | "classmap": [ 215 | "lib" 216 | ] 217 | }, 218 | "notification-url": "https://packagist.org/downloads/", 219 | "license": [ 220 | "LGPL" 221 | ], 222 | "authors": [ 223 | { 224 | "name": "Jeremy Dorn", 225 | "email": "jeremy@jeremydorn.com", 226 | "homepage": "http://jeremydorn.com/" 227 | } 228 | ], 229 | "description": "an easy way to cache data in the file system", 230 | "homepage": "https://github.com/jdorn/FileSystemCache/", 231 | "keywords": [ 232 | "cache", 233 | "file system" 234 | ], 235 | "time": "2013-07-05 13:28:27" 236 | }, 237 | { 238 | "name": "jwilsson/spotify-web-api-php", 239 | "version": "0.8.2", 240 | "source": { 241 | "type": "git", 242 | "url": "https://github.com/jwilsson/spotify-web-api-php.git", 243 | "reference": "3e8d6cc277a6c0c95aa27641cfddef5911f5eb07" 244 | }, 245 | "dist": { 246 | "type": "zip", 247 | "url": "https://api.github.com/repos/jwilsson/spotify-web-api-php/zipball/3e8d6cc277a6c0c95aa27641cfddef5911f5eb07", 248 | "reference": "3e8d6cc277a6c0c95aa27641cfddef5911f5eb07", 249 | "shasum": "" 250 | }, 251 | "require": { 252 | "ext-curl": "*", 253 | "php": ">=5.3.3" 254 | }, 255 | "require-dev": { 256 | "phpunit/phpunit": "4.*", 257 | "satooshi/php-coveralls": "dev-master", 258 | "vlucas/phpdotenv": "1.*" 259 | }, 260 | "type": "library", 261 | "autoload": { 262 | "psr-4": { 263 | "SpotifyWebAPI\\": "src/" 264 | } 265 | }, 266 | "notification-url": "https://packagist.org/downloads/", 267 | "license": [ 268 | "MIT" 269 | ], 270 | "authors": [ 271 | { 272 | "name": "Jonathan Wilsson", 273 | "email": "jonathan.wilsson@gmail.com" 274 | } 275 | ], 276 | "description": "PHP implementation of Spotify's Web API.", 277 | "homepage": "http://jwilsson.github.io/spotify-web-api-php/", 278 | "keywords": [ 279 | "spotify" 280 | ], 281 | "time": "2015-05-02 11:38:12" 282 | }, 283 | { 284 | "name": "league/oauth2-client", 285 | "version": "0.8.1", 286 | "source": { 287 | "type": "git", 288 | "url": "https://github.com/thephpleague/oauth2-client.git", 289 | "reference": "bb2d4e4be7a4af05260de9eaa4337920bc5e0ad1" 290 | }, 291 | "dist": { 292 | "type": "zip", 293 | "url": "https://api.github.com/repos/thephpleague/oauth2-client/zipball/bb2d4e4be7a4af05260de9eaa4337920bc5e0ad1", 294 | "reference": "bb2d4e4be7a4af05260de9eaa4337920bc5e0ad1", 295 | "shasum": "" 296 | }, 297 | "require": { 298 | "guzzle/guzzle": "~3.7", 299 | "php": ">=5.4.0" 300 | }, 301 | "require-dev": { 302 | "mockery/mockery": "~0.9", 303 | "phpunit/phpunit": "~4.0", 304 | "squizlabs/php_codesniffer": "~2.0" 305 | }, 306 | "type": "library", 307 | "extra": { 308 | "branch-alias": { 309 | "dev-master": "0.8.x-dev" 310 | } 311 | }, 312 | "autoload": { 313 | "psr-4": { 314 | "League\\OAuth2\\Client\\": "src/" 315 | } 316 | }, 317 | "notification-url": "https://packagist.org/downloads/", 318 | "license": [ 319 | "MIT" 320 | ], 321 | "authors": [ 322 | { 323 | "name": "Alex Bilbie", 324 | "email": "hello@alexbilbie.com", 325 | "homepage": "http://www.alexbilbie.com", 326 | "role": "Developer" 327 | } 328 | ], 329 | "description": "OAuth 2.0 Client Library", 330 | "keywords": [ 331 | "Authentication", 332 | "SSO", 333 | "authorization", 334 | "identity", 335 | "idp", 336 | "oauth", 337 | "oauth2", 338 | "single sign on" 339 | ], 340 | "time": "2015-02-12 17:10:16" 341 | }, 342 | { 343 | "name": "slim/slim", 344 | "version": "2.6.2", 345 | "source": { 346 | "type": "git", 347 | "url": "https://github.com/slimphp/Slim.git", 348 | "reference": "20a02782f76830b67ae56a5c08eb1f563c351a37" 349 | }, 350 | "dist": { 351 | "type": "zip", 352 | "url": "https://api.github.com/repos/slimphp/Slim/zipball/20a02782f76830b67ae56a5c08eb1f563c351a37", 353 | "reference": "20a02782f76830b67ae56a5c08eb1f563c351a37", 354 | "shasum": "" 355 | }, 356 | "require": { 357 | "php": ">=5.3.0" 358 | }, 359 | "suggest": { 360 | "ext-mcrypt": "Required for HTTP cookie encryption" 361 | }, 362 | "type": "library", 363 | "autoload": { 364 | "psr-0": { 365 | "Slim": "." 366 | } 367 | }, 368 | "notification-url": "https://packagist.org/downloads/", 369 | "license": [ 370 | "MIT" 371 | ], 372 | "authors": [ 373 | { 374 | "name": "Josh Lockhart", 375 | "email": "info@joshlockhart.com", 376 | "homepage": "http://www.joshlockhart.com/" 377 | } 378 | ], 379 | "description": "Slim Framework, a PHP micro framework", 380 | "homepage": "http://github.com/codeguy/Slim", 381 | "keywords": [ 382 | "microframework", 383 | "rest", 384 | "router" 385 | ], 386 | "time": "2015-03-08 18:41:17" 387 | }, 388 | { 389 | "name": "symfony/event-dispatcher", 390 | "version": "v2.6.6", 391 | "target-dir": "Symfony/Component/EventDispatcher", 392 | "source": { 393 | "type": "git", 394 | "url": "https://github.com/symfony/EventDispatcher.git", 395 | "reference": "70f7c8478739ad21e3deef0d977b38c77f1fb284" 396 | }, 397 | "dist": { 398 | "type": "zip", 399 | "url": "https://api.github.com/repos/symfony/EventDispatcher/zipball/70f7c8478739ad21e3deef0d977b38c77f1fb284", 400 | "reference": "70f7c8478739ad21e3deef0d977b38c77f1fb284", 401 | "shasum": "" 402 | }, 403 | "require": { 404 | "php": ">=5.3.3" 405 | }, 406 | "require-dev": { 407 | "psr/log": "~1.0", 408 | "symfony/config": "~2.0,>=2.0.5", 409 | "symfony/dependency-injection": "~2.6", 410 | "symfony/expression-language": "~2.6", 411 | "symfony/phpunit-bridge": "~2.7", 412 | "symfony/stopwatch": "~2.3" 413 | }, 414 | "suggest": { 415 | "symfony/dependency-injection": "", 416 | "symfony/http-kernel": "" 417 | }, 418 | "type": "library", 419 | "extra": { 420 | "branch-alias": { 421 | "dev-master": "2.6-dev" 422 | } 423 | }, 424 | "autoload": { 425 | "psr-0": { 426 | "Symfony\\Component\\EventDispatcher\\": "" 427 | } 428 | }, 429 | "notification-url": "https://packagist.org/downloads/", 430 | "license": [ 431 | "MIT" 432 | ], 433 | "authors": [ 434 | { 435 | "name": "Symfony Community", 436 | "homepage": "http://symfony.com/contributors" 437 | }, 438 | { 439 | "name": "Fabien Potencier", 440 | "email": "fabien@symfony.com" 441 | } 442 | ], 443 | "description": "Symfony EventDispatcher Component", 444 | "homepage": "http://symfony.com", 445 | "time": "2015-03-13 17:37:22" 446 | } 447 | ], 448 | "packages-dev": [], 449 | "aliases": [], 450 | "minimum-stability": "stable", 451 | "stability-flags": { 452 | "jdorn/file-system-cache": 20 453 | }, 454 | "prefer-stable": false, 455 | "prefer-lowest": false, 456 | "platform": [], 457 | "platform-dev": [] 458 | } 459 | --------------------------------------------------------------------------------