From 52bc3105fd08c2be475af39e8bd11775c6b8b759 Mon Sep 17 00:00:00 2001 From: Max Nuding Date: Thu, 5 May 2022 17:12:01 +0200 Subject: [PATCH] Added photo voting --- results-gallery.php | 103 +++++++++++++++++++++++++++++++++++ src/API/LoggerFactory.php | 1 - src/API/PhotoPrism.php | 87 ++++++++++++++++++++++++++++-- src/Entities/Photo.php | 63 ++++++++++++++++++++++ vote-gallery.php | 110 ++++++++++++++++++++++++++++++++++++++ vote.css | 52 ++++++++++++++++++ vote.js | 41 ++++++++++++++ vote.php | 30 +++++++++++ votes.json | 1 + 9 files changed, 484 insertions(+), 4 deletions(-) create mode 100644 results-gallery.php create mode 100644 src/Entities/Photo.php create mode 100644 vote-gallery.php create mode 100644 vote.css create mode 100644 vote.js create mode 100644 vote.php create mode 100644 votes.json diff --git a/results-gallery.php b/results-gallery.php new file mode 100644 index 0000000..a21ec9f --- /dev/null +++ b/results-gallery.php @@ -0,0 +1,103 @@ + + + + Collagen Abstimmung + + + + + + +'; + +/** @var array $config configuration options */ +$config = require(__DIR__ . '/config.php'); + +/** @var PhotoPrism $api API object to interface with PhotoPrism */ +$api = new PhotoPrism($config); + +/** @var Album[] $albums List of PhotoPrism albums */ +$albums = []; +try { + $api->login(); +} catch (\Exception $e) { + die('Fehler: ' . $e->getMessage().$footer.''); +} + +/** @var string $token Tokens for which album(s) are visible in the dropdown */ +$token = $_GET['token']; + +/** @var int $page Page number of photos to load */ +$page = 1; + +/** @var int $count Number of pictures to load */ +$count = 200; + +/** @var int $offset Tokens for which album(s) are visible in the dropdown */ +$offset = ($page - 1) * $count; + +/** @var string $album_url URL path to the selected album */ +$album_url = '/'; + +$votes_file = 'votes.json'; + +$contents = file_get_contents($votes_file); +if ($contents === false) { + $contents = '{}'; +} +$contents = json_decode($contents, true); + +$vote_counts = []; +foreach ($contents as $votes) { + foreach ($votes as $photo_uid => $vote_value) { + if (!array_key_exists($photo_uid, $vote_counts)) { + $vote_counts[$photo_uid] = 0; + } + $vote_counts[$photo_uid] += intval($vote_value); + } +} + +$page_sizes = array_values(array_unique([10, 20, 50, 100, $count])); +sort($page_sizes); +$link_class = $page === 1 ? 'pageLink disabled' : 'pageLink'; + +try { + $album = $api->getAlbumByToken($token); + if ($album === null) { + die('Album nicht gefunden' . $footer . ''); + } + $photos = $api->getAlbumPhotos($album, $count, $offset); + $photos = array_filter($photos, function (Photo $a) use ($vote_counts) { + $vote_a = $vote_counts[$a->uid] ?? 0; + return $vote_a >= 0; + }); + usort($photos, function (Photo $a, Photo $b) use ($vote_counts) { + $vote_a = $vote_counts[$a->uid] ?? 0; + $vote_b = $vote_counts[$b->uid] ?? 0; + return $vote_b - $vote_a; + }); + ?> + getThumbnailUrl();?> +
+ +
+ getMessage() . ''); +} +?> diff --git a/src/API/LoggerFactory.php b/src/API/LoggerFactory.php index c47fcad..c9e84b8 100644 --- a/src/API/LoggerFactory.php +++ b/src/API/LoggerFactory.php @@ -33,7 +33,6 @@ class LoggerFactory { $l = new Logger($name); $l->setHandlers(self::$handlers); - $l->info('Initialized'); return $l; } } diff --git a/src/API/PhotoPrism.php b/src/API/PhotoPrism.php index 9309738..fba1467 100644 --- a/src/API/PhotoPrism.php +++ b/src/API/PhotoPrism.php @@ -8,6 +8,7 @@ use Psr\Log\LoggerInterface; use PhotoPrismUpload\Exceptions\NetworkException; use PhotoPrismUpload\Exceptions\AuthenticationException; use PhotoPrismUpload\Entities\Album; +use PhotoPrismUpload\Entities\Photo; /** * The main API class to interface with PhotoPrism @@ -18,11 +19,17 @@ class PhotoPrism public string $base_url = 'https://photos.phlaym.net'; /** @var string $api_url API URL of the PhotoPrism instance */ - protected string $api_url = ''; + public string $api_url = ''; /** @var string|null $session_id Session id of the currently logged in user */ protected ?string $session_id = null; + /** @var string|null $download_token Token for downloading files */ + protected ?string $download_token = null; + + /** @var string|null $preview_token Token for thumbnails files */ + protected ?string $preview_token = null; + /** @var array $config Configuration options */ protected array $config; @@ -51,6 +58,8 @@ class PhotoPrism $this->logger = LoggerFactory::create('PhotoPrismUpload'); if (isset($_SESSION['pp_sessionid'])) { $this->session_id = $_SESSION['pp_sessionid']; + $this->preview_token = $_SESSION['pp_preview_token']; + $this->download_token = $_SESSION['pp_download_token']; } } @@ -67,6 +76,10 @@ class PhotoPrism private function parseHeaders(string $response): array { $response = explode("\r\n\r\n", $response, 2); + if (count($response) === 3) { + $dropped = array_shift($response); + $this->logger->warning('Dropping start of response:' . $dropped); + } $headers = $response[0]; if ($headers === 'HTTP/1.1 100 Continue') { $response = explode("\r\n\r\n", $response[1], 2); @@ -150,6 +163,7 @@ class PhotoPrism curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_HEADER, true); curl_setopt($ch, CURLINFO_HEADER_OUT, true); + curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); if ($method !== 'get' && !empty($query_data)) { curl_setopt($ch, CURLOPT_POSTFIELDS, $query_data); } @@ -209,8 +223,12 @@ class PhotoPrism throw new AuthenticationException($response['error']); } $this->session_id = $response['id']; + $this->download_token = $response['config']['downloadToken']; + $this->preview_token = $response['config']['previewToken']; $_SESSION['pp_sessionid'] = $this->session_id; - $this->logger->debug('Session ID: ' . $this->session_id); + $_SESSION['pp_preview_token'] = $this->preview_token; + $_SESSION['pp_download_token'] = $this->download_token; + $this->logger->debug('Session ID: ' . $this->session_id . ', preview token: ' . $this->preview_token); } /** @@ -248,7 +266,7 @@ class PhotoPrism * Fetches albums from PhotoPrism which can be viewed with the provided tokens * @throws NetworkException on failure - * @param string[] $tokens -A list of tokens by which the albms are filtered + * @param string[] $tokens -A list of tokens by which the albums are filtered * @param int $count -Maximum amount of albums to fetch * @param int $offset -Number of albums to skip * @return Album[] @@ -272,6 +290,25 @@ class PhotoPrism return $visibleAlbums; } + /** + * Fetches an album from PhotoPrism which can be viewed with the provided token + + * @throws NetworkException on failure + * @param string $token -A token by which the albums are filtered + * @return Album|null + */ + public function getAlbumByToken(string $token): ?Album + { + $this->logger->debug('getAlbumByToken'); + $albums = $this->getAlbumsByTokens([$token]); + if (empty($albums)) { + $this->logger->debug('Could not find album for token' . $token); + return null; + } + $this->logger->debug('getAlbumByToken done'); + return $albums[0]; + } + /** * Fetches the secret token of an album @@ -326,4 +363,48 @@ class PhotoPrism $res = $this->makeRequest('POST', $import_url, $import_data); $this->logger->info('Import result: ' . $res); } + + /** + * Retrieves the photos of an album + + * @throws NetworkException on failure + * @param Album $album - The album to which the photos should be loaded + * @param int $count - How many photos should be loaded + * @param int $offset - Offset for paging + * @return Photo[] - Photos in the album + */ + public function getAlbumPhotos(Album $album, int $count = 60, int $offset = 0): array + { + $this->logger->debug('getAlbumPhotos'); + $data = [ + 'album' => $album->uid, + 'count' => $count, + 'offset' => $offset + ]; + $res = $this->makeRequest('GET', '/photos', $data, 'text/plain'); + $response = json_decode($res, true); + if (!empty($response['error'])) { + throw new NetworkException($response['error']); + } + $this->logger->debug('getAlbumPhotos response'); + $photos = array_map( + function ($entry) { + $photo = new Photo($entry); + $photo->thumbnail_token = $this->preview_token; + return $photo; + }, + $response + ); + $this->logger->info('Got photos:' . json_encode($photos)); + $photos = array_filter($photos, function ($obj) { + static $idList = array(); + if (in_array($obj->uid, $idList)) { + return false; + } + $idList []= $obj->uid; + return true; + }); + $this->logger->info('unique photos:' . json_encode($photos)); + return $photos; + } } diff --git a/src/Entities/Photo.php b/src/Entities/Photo.php new file mode 100644 index 0000000..eb5f848 --- /dev/null +++ b/src/Entities/Photo.php @@ -0,0 +1,63 @@ +uid = $response['UID']; + $this->id = intval($response['ID']); + $this->type = $response['Type']; + $this->fileUID = $response['FileUID']; + $this->fileRoot = $response['FileRoot']; + $this->fileName = $response['FileName']; + $this->hash = $response['Hash']; + $this->width = intval($response['Width']); + $this->height = intval($response['Height']); + $this->logger = LoggerFactory::create('PhotoPrismUpload.Photo'); + } + + /** + * Gets the thumbnail URL path for this image. + * Starts with a leading / + * + * @return string + */ + public function getThumbnailUrl(string $size = 'tile_500'): string + { + return "/t/{$this->hash}/{$this->thumbnail_token}/{$size}"; + } +} diff --git a/vote-gallery.php b/vote-gallery.php new file mode 100644 index 0000000..eae9c22 --- /dev/null +++ b/vote-gallery.php @@ -0,0 +1,110 @@ + + + + Collagen Abstimmung + + + + + + + +'; + +/** @var array $config configuration options */ +$config = require(__DIR__ . '/config.php'); + +/** @var PhotoPrism $api API object to interface with PhotoPrism */ +$api = new PhotoPrism($config); + +/** @var Album[] $albums List of PhotoPrism albums */ +$albums = []; +try { + $api->login(); +} catch (\Exception $e) { + die('Fehler: ' . $e->getMessage().$footer.''); +} + +/** @var string $token Tokens for which album(s) are visible in the dropdown */ +$token = $_GET['token']; + +/** @var int $page Page number of photos to load */ +$page = intval($_GET['page'] ?? 1); + +/** @var int $count Number of pictures to load */ +$count = intval($_GET['count'] ?? 50); + +/** @var int $offset Tokens for which album(s) are visible in the dropdown */ +$offset = ($page - 1) * $count; + +/** @var string $album_url URL path to the selected album */ +$album_url = '/'; + +$page_sizes = array_values(array_unique([10, 20, 50, 100, $count])); +sort($page_sizes); +$link_class = $page === 1 ? 'pageLink disabled' : 'pageLink'; + +try { + $album = $api->getAlbumByToken($token); + if ($album === null) { + die('Album nicht gefunden' . $footer . ''); + } + $photos = $api->getAlbumPhotos($album, $count, $offset); + ?> +
+ + + << + < + Seite + > + pro Seite +
+ getThumbnailUrl();?> +
+ +
+ + + +
+
+ getMessage() . ''); +} +?> +
+ << + < + Seite + > +
diff --git a/vote.css b/vote.css new file mode 100644 index 0000000..17e23db --- /dev/null +++ b/vote.css @@ -0,0 +1,52 @@ +:root { + --main-bg-color: white; + --main-text-color: #333333; +} + +@media (prefers-color-scheme: dark) { + :root { + --main-bg-color: #333333; + --main-text-color: white; + } +} + +body { + background-color: var(--main-bg-color); + color: var(--main-text-color); + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Oxygen-Sans, Ubuntu, + Cantarell, "Fira Sans", "Droid Sans", "Helvetica Neue", Arial, sans-serif; + +} + +button { + padding: 4px; +} + +img { + display: block; +} + +.photowrapper { + margin: 8px; + display: inline-block; +} + +.votewrapper { + margin: auto; + display: flex; + justify-content: center; +} + +a.disabled, a.disabled:visited { + pointer-events: none; + color: rgb(163, 163, 163); + cursor: not-allowed; +} + +.pageLink { + color: orange; +} + +.pageLink:visited { + color: rgb(195, 0, 255); +} diff --git a/vote.js b/vote.js new file mode 100644 index 0000000..90d8189 --- /dev/null +++ b/vote.js @@ -0,0 +1,41 @@ +async function vote(button) { + console.log(button); + const name = document.getElementById('nameInput').value; + const data = new FormData(); + data.append("name", name); + data.append("vote", button.value); + data.append("uid", button.dataset.uid); + const response = await fetch('vote.php', { method: "POST", body: data }); + button.disabled = true; + button.parentElement + .querySelectorAll('.voteButton') + .forEach(b => { + b.disabled = b.value === button.value; + }); +} + +async function loadVotes() { + const name = document.getElementById('nameInput').value; + localStorage.setItem('name', name); + const response = await fetch('vote.php?' + new URLSearchParams({ + name: name + })); + const result = await response.json(); + console.log(result); + document + .querySelectorAll('.voteButton') + .forEach(b => { + b.disabled = b.dataset.uid in result && b.value == result[b.dataset.uid]; + }); +} + +window.addEventListener('DOMContentLoaded', async (event) => { + console.log('DOMContentLoaded'); + const loadedName = localStorage.getItem('name'); + console.log(loadedName); + if (loadedName) { + const nameInput = document.getElementById('nameInput'); + nameInput.value = loadedName; + await loadVotes(); + } +}); diff --git a/vote.php b/vote.php new file mode 100644 index 0000000..5671b7d --- /dev/null +++ b/vote.php @@ -0,0 +1,30 @@ +