apiKey = $apiKey; $this->apiSecret = $apiSecret; $this->userId = $userId; } /** * Set OAuth handler for authenticated requests */ public function setOAuth($oauth) { $this->oauth = $oauth; } /** * Check if OAuth is available */ public function hasOAuth() { return $this->oauth !== null && $this->oauth->isAuthorized(); } /** * Set user ID */ public function setUserId($userId) { $this->userId = $userId; } /** * Make API request (with OAuth if available) * * @param string $method Flickr API method * @param array $params Additional parameters * @param bool $useOAuth Force OAuth for this request * @return array Response data */ private function request($method, $params = [], $useOAuth = false) { $params = array_merge([ 'method' => $method, 'api_key' => $this->apiKey, 'format' => 'json', 'nojsoncallback' => 1, ], $params); // Use OAuth if available and requested if (($useOAuth || $this->hasOAuth()) && $this->oauth !== null) { return $this->requestWithOAuth($method, $params); } $url = $this->baseUrl . '?' . http_build_query($params); $context = stream_context_create([ 'http' => [ 'timeout' => 30, 'user_agent' => 'VH_Posting_System/1.0', ], ]); $response = @file_get_contents($url, false, $context); if ($response === false) { throw new RuntimeException('Failed to connect to Flickr API'); } $data = json_decode($response, true); if ($data === null) { throw new RuntimeException('Invalid JSON response from Flickr API'); } if (isset($data['stat']) && $data['stat'] === 'fail') { throw new RuntimeException('Flickr API error: ' . (isset($data['message']) ? $data['message'] : 'Unknown error')); } return $data; } /** * Make OAuth-signed API request */ private function requestWithOAuth($method, $params) { $params['method'] = $method; $params['format'] = 'json'; $params['nojsoncallback'] = 1; $oauthParams = $this->oauth->signRequest('GET', $this->baseUrl, $params); $allParams = array_merge($params, $oauthParams); $url = $this->baseUrl . '?' . http_build_query($allParams); $ch = curl_init(); curl_setopt_array($ch, [ CURLOPT_URL => $url, CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 30, CURLOPT_SSL_VERIFYPEER => true, ]); $response = curl_exec($ch); $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); if ($response === false || $httpCode !== 200) { throw new RuntimeException('Failed to connect to Flickr API (OAuth)'); } $data = json_decode($response, true); if ($data === null) { throw new RuntimeException('Invalid JSON response from Flickr API'); } if (isset($data['stat']) && $data['stat'] === 'fail') { throw new RuntimeException('Flickr API error: ' . (isset($data['message']) ? $data['message'] : 'Unknown error')); } return $data; } /** * Get user's photostream * * @param int $page Page number * @param int $perPage Photos per page (max 500) * @return array Photos data */ public function getPhotos($page = 1, $perPage = 50) { $response = $this->request('flickr.people.getPhotos', [ 'user_id' => $this->userId ? $this->userId : 'me', 'page' => $page, 'per_page' => $perPage, 'extras' => 'url_sq,url_t,url_s,url_m,url_z,url_l,url_k,url_o,description,date_upload,date_taken,owner_name,original_format', ]); return $this->normalizePhotosResponse(isset($response['photos']) ? $response['photos'] : []); } /** * Get user's photosets (albums) * * @param int $page Page number * @param int $perPage Albums per page * @return array Photosets data with pagination info */ public function getPhotosets($page = 1, $perPage = 50) { $response = $this->request('flickr.photosets.getList', [ 'user_id' => $this->userId, 'page' => $page, 'per_page' => $perPage, 'primary_photo_extras' => 'url_sq,url_t,url_s,url_m', ]); $photosets = isset($response['photosets']) ? $response['photosets'] : []; return [ 'albums' => isset($photosets['photoset']) ? $photosets['photoset'] : [], 'page' => (int)($photosets['page'] ?? $page), 'pages' => (int)($photosets['pages'] ?? 1), 'perpage' => (int)($photosets['perpage'] ?? $perPage), 'total' => (int)($photosets['total'] ?? 0), ]; } /** * Get photos from a specific photoset (album) * * @param string $photosetId Photoset ID * @param int $page Page number * @param int $perPage Photos per page * @return array Photos data */ public function getPhotosetPhotos($photosetId, $page = 1, $perPage = 50) { $response = $this->request('flickr.photosets.getPhotos', [ 'photoset_id' => $photosetId, 'user_id' => $this->userId, 'page' => $page, 'per_page' => $perPage, 'extras' => 'url_sq,url_t,url_s,url_m,url_z,url_l,url_k,url_o,description,date_upload,date_taken,original_format,media,path_alias,owner_name', ]); // Get owner info from photoset response $ownername = isset($response['photoset']['ownername']) ? $response['photoset']['ownername'] : ''; $owner = isset($response['photoset']['owner']) ? $response['photoset']['owner'] : $this->userId; return $this->normalizePhotosResponse(isset($response['photoset']) ? $response['photoset'] : [], $ownername, $owner); } /** * Get info about a specific photo * * @param string $photoId Photo ID * @return array Photo info */ public function getPhotoInfo($photoId) { $response = $this->request('flickr.photos.getInfo', [ 'photo_id' => $photoId, ]); return isset($response['photo']) ? $response['photo'] : []; } /** * Get available sizes for a photo (uses OAuth if available) * * @param string $photoId Photo ID * @return array Available sizes */ public function getPhotoSizes($photoId) { // getSizes requires OAuth to return Original for private/restricted photos $response = $this->request('flickr.photos.getSizes', [ 'photo_id' => $photoId, ], true); // Force OAuth if available $sizes = []; $sizeList = isset($response['sizes']['size']) ? $response['sizes']['size'] : []; foreach ($sizeList as $size) { $sizes[$size['label']] = [ 'url' => $size['source'], 'width' => (int)$size['width'], 'height' => (int)$size['height'], ]; } return $sizes; } /** * Get original URL for a photo (requires OAuth) * * @param string $photoId Photo ID * @return string|null Original URL or null if not available */ public function getOriginalUrl($photoId) { try { $sizes = $this->getPhotoSizes($photoId); // Try Original first, then fall back to largest available if (isset($sizes['Original'])) { return $sizes['Original']['url']; } if (isset($sizes['Large 2048'])) { return $sizes['Large 2048']['url']; } if (isset($sizes['Large 1600'])) { return $sizes['Large 1600']['url']; } if (isset($sizes['Large'])) { return $sizes['Large']['url']; } return null; } catch (Exception $e) { return null; } } /** * Search user's photos * * @param string $query Search query * @param int $page Page number * @param int $perPage Photos per page * @return array Photos data */ public function searchPhotos($query, $page = 1, $perPage = 50) { $response = $this->request('flickr.photos.search', [ 'user_id' => $this->userId ? $this->userId : 'me', 'text' => $query, 'page' => $page, 'per_page' => $perPage, 'extras' => 'url_sq,url_t,url_s,url_m,url_z,url_l,url_k,url_o,description,date_upload,date_taken,original_format', ]); return $this->normalizePhotosResponse(isset($response['photos']) ? $response['photos'] : []); } /** * Find user ID by username * * @param string $username Flickr username * @return string User ID */ public function findUserByUsername($username) { $response = $this->request('flickr.people.findByUsername', [ 'username' => $username, ]); return isset($response['user']['nsid']) ? $response['user']['nsid'] : ''; } /** * Normalize photos response to consistent format */ private function normalizePhotosResponse($response, $defaultOwnerName = '', $defaultOwner = '') { $photos = []; $photoList = isset($response['photo']) ? $response['photo'] : []; foreach ($photoList as $photo) { $farm = isset($photo['farm']) ? $photo['farm'] : ''; $server = $photo['server']; $id = $photo['id']; $originalSecret = isset($photo['originalsecret']) ? $photo['originalsecret'] : $photo['secret']; $originalFormat = isset($photo['originalformat']) ? $photo['originalformat'] : 'jpg'; // Get original URL - ONLY use if API returns it // If url_o is not returned, originals are blocked by Flickr privacy settings $originalUrl = isset($photo['url_o']) ? $photo['url_o'] : null; // Get large 2048 URL (url_k) - from API or construct it // This is the best quality available when originals are blocked $large2048Url = isset($photo['url_k']) ? $photo['url_k'] : null; // Construct large2048 URL if not provided (usually works) if (!$large2048Url && $server) { $large2048Url = "https://live.staticflickr.com/{$server}/{$id}_{$photo['secret']}_k.jpg"; } // Determine media type (photo or video) $mediaType = isset($photo['media']) ? $photo['media'] : 'photo'; $isVideo = ($mediaType === 'video'); // Build page URL - use path_alias, ownername, or owner NSID $pathAlias = isset($photo['pathalias']) && $photo['pathalias'] ? $photo['pathalias'] : ''; $ownerName = isset($photo['ownername']) ? $photo['ownername'] : $defaultOwnerName; $owner = isset($photo['owner']) ? $photo['owner'] : ($defaultOwner ? $defaultOwner : $this->userId); // Prefer path_alias, then ownername, then owner NSID (URL encoded) $userPath = $pathAlias ? $pathAlias : ($ownerName ? $ownerName : rawurlencode($owner)); $pageUrl = "https://www.flickr.com/photos/{$userPath}/{$id}/"; $photos[] = [ 'id' => $id, 'secret' => $photo['secret'], 'server' => $server, 'farm' => $farm, 'title' => isset($photo['title']) ? $photo['title'] : 'Untitled', 'description' => isset($photo['description']['_content']) ? $photo['description']['_content'] : '', 'date_upload' => isset($photo['dateupload']) ? $photo['dateupload'] : '', 'date_taken' => isset($photo['datetaken']) ? $photo['datetaken'] : '', 'original_format' => $originalFormat, 'original_secret' => $originalSecret, 'media' => $mediaType, 'is_video' => $isVideo, 'urls' => [ 'square' => isset($photo['url_sq']) ? $photo['url_sq'] : null, 'thumbnail' => isset($photo['url_t']) ? $photo['url_t'] : null, 'small' => isset($photo['url_s']) ? $photo['url_s'] : null, 'medium' => isset($photo['url_m']) ? $photo['url_m'] : null, 'medium640' => isset($photo['url_z']) ? $photo['url_z'] : null, 'large' => isset($photo['url_l']) ? $photo['url_l'] : null, 'large2048' => $large2048Url, 'original' => $originalUrl, ], 'page_url' => $pageUrl, ]; } return [ 'photos' => $photos, 'page' => (int)(isset($response['page']) ? $response['page'] : 1), 'pages' => (int)(isset($response['pages']) ? $response['pages'] : 1), 'perpage' => (int)(isset($response['perpage']) ? $response['perpage'] : count($photos)), 'total' => (int)(isset($response['total']) ? $response['total'] : count($photos)), ]; } }