Added photo voting

This commit is contained in:
Max Nuding 2022-05-05 17:12:01 +02:00
parent 4ba6085a06
commit 52bc3105fd
Signed by: phlaym
GPG Key ID: A06651BAB6777237
9 changed files with 484 additions and 4 deletions

103
results-gallery.php Normal file
View File

@ -0,0 +1,103 @@
<?php
session_start();
/** require autoloading to manage namespaces */
require __DIR__ . '/vendor/autoload.php';
use PhotoPrismUpload\API\PhotoPrism;
use PhotoPrismUpload\Entities\Album;
use PhotoPrismUpload\Entities\Photo;
?>
<html>
<head>
<title>Collagen Abstimmung</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta charset="utf-8">
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<link href="vote.css" rel="stylesheet">
</head>
<body>
<?php
$footer = '</body></html>';
/** @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.'</body></html>');
}
/** @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 . '</body></html>');
}
$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;
});
?>
<?php
foreach ($photos as $photo) {
$thumb = $photo->getThumbnailUrl();?>
<div class="photowrapper">
<img src="<?= $api->api_url.$thumb?>" id="<?= $photo->uid?>">
</div>
<?php
}
} catch (\Exception $e) {
die('Fehler: ' . $footer . $e->getMessage() . '</body></html>');
}
?>

View File

@ -33,7 +33,6 @@ class LoggerFactory
{
$l = new Logger($name);
$l->setHandlers(self::$handlers);
$l->info('Initialized');
return $l;
}
}

View File

@ -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;
}
}

63
src/Entities/Photo.php Normal file
View File

@ -0,0 +1,63 @@
<?php
namespace PhotoPrismUpload\Entities;
use PhotoPrismUpload\API\LoggerFactory;
/** A PhotoPrism Photo */
class Photo
{
/** @var string $thumbnail_token Token for fetching the thumbnail*/
public string $thumbnail_token = '';
/** @var string $uid Unique Id of the photo */
public string $uid = '';
/** @var string $id Id of the photo */
public int $id = 0;
/** @var string $type Type of the photo (image, video, etc) */
public string $type = '';
public string $fileUID;
public string $fileRoot;
public string $fileName;
public string $hash;
public int $width;
public int $height;
public array $files;
/**
* Creates a new photo from the api response
*
* @param array $response Photoprism API response containing a photo object
*
* @return void
*/
public function __construct(
array $response
) {
$this->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}";
}
}

110
vote-gallery.php Normal file
View File

@ -0,0 +1,110 @@
<?php
session_start();
/** require autoloading to manage namespaces */
require __DIR__ . '/vendor/autoload.php';
use PhotoPrismUpload\API\PhotoPrism;
use PhotoPrismUpload\Entities\Album;
?>
<html>
<head>
<title>Collagen Abstimmung</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta charset="utf-8">
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<script src="vote.js"></script>
<link href="vote.css" rel="stylesheet">
</head>
<body>
<?php
$footer = '</body></html><footer style="position: fixed;bottom: 0;left: 0;">'
.'<a href="/git/phlaym/photoprismupload">Ich bin Open Source</a></footer>';
/** @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.'</body></html>');
}
/** @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 . '</body></html>');
}
$photos = $api->getAlbumPhotos($album, $count, $offset);
?>
<div>
<input type="text" placeholder="Name" name="name" id="nameInput" style="margin: 0 0 0 8px">
<button onclick="loadVotes()">OK</button>
<a class="<?=$link_class?>" href="vote-gallery.php?token=<?=$token?>&count=<?=$count?>&page=1"><<</a>
<a class="<?=$link_class?>" href="vote-gallery.php?token=<?=$token?>&count=<?=$count?>&page=<?=$page-1?>"><</a>
Seite <?=$page?>
<a class="pageLink" href="vote-gallery.php?token=<?=$token?>&count=<?=$count?>&page=<?=$page+1?>">></a>
<select>
<?php
foreach ($page_sizes as $current_page_size) {
$selected_string = $current_page_size === $count ? 'selected' : '';
echo '<option '.$selected_string.'>'.$current_page_size.'</option>';
}
?>
</select> pro Seite
</div>
<?php
foreach ($photos as $photo) {
$thumb = $photo->getThumbnailUrl();?>
<div class="photowrapper">
<img src="<?= $api->api_url.$thumb?>" id="<?= $photo->uid?>">
<div class="votewrapper">
<button value="1" onclick="vote(this)" class="voteButton" data-uid="<?= $photo->uid?>" disabled>
👍 Dafür
</button>
<button value="0" onclick="vote(this)" class="voteButton" data-uid="<?= $photo->uid?>" disabled>
😶 Neutral
</button>
<button value="-1" onclick="vote(this)" class="voteButton" data-uid="<?= $photo->uid?>" disabled>
👎 Dagegen
</button>
</div>
</div>
<?php
}
} catch (\Exception $e) {
die('Fehler: ' . $footer . $e->getMessage() . '</body></html>');
}
?>
<div>
<a class="<?=$link_class?>" href="vote-gallery.php?token=<?=$token?>&count=<?=$count?>&page=1"><<</a>
<a class="<?=$link_class?>" href="vote-gallery.php?token=<?=$token?>&count=<?=$count?>&page=<?=$page-1?>"><</a>
Seite <?=$page?>
<a class="pageLink" href="vote-gallery.php?token=<?=$token?>&count=<?=$count?>&page=<?=$page+1?>">></a>
</div>

52
vote.css Normal file
View File

@ -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);
}

41
vote.js Normal file
View File

@ -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();
}
});

30
vote.php Normal file
View File

@ -0,0 +1,30 @@
<?php
$votes_file = 'votes.json';
if (isset($_GET['name'])) {
$name = $_GET['name'];
$contents = file_get_contents($votes_file);
if ($contents === false) {
$contents = '{}';
}
$contents = json_decode($contents, true);
$data = $contents[$name] ?? [];
header('Content-Type: application/json; charset=utf-8');
die(json_encode($data));
} elseif (isset($_POST['name']) && isset($_POST['uid']) && isset($_POST['vote'])) {
$name = $_POST['name'];
$contents = file_get_contents($votes_file);
if ($contents === false) {
$contents = '{}';
}
$contents = json_decode($contents, true);
$data = $contents[$name] ?? [];
$data[$_POST['uid']] = $_POST['vote'];
$contents[$name] = $data;
file_put_contents($votes_file, json_encode($contents));
header('HTTP/1.1 204 No Content');
die();
} else {
header('HTTP/1.1 400 Bad Request');
die();
}

1
votes.json Normal file
View File

@ -0,0 +1 @@
{"":{"pra15px12nephjox":"1"},"Max":{"pra15px12nephjox":"1","pra15pf3bal4owcz":"-1","pra15oq302x3jkuz":"1","pra15nj2z2ludgom":"1","pr354wv3w00i7czq":"1","pr354x42wpbkkg7m":"1","pr354z62mo0pncoz":"1","pr9mmge3mws6j0rw":"1","pr354yv3323z8b3z":"1","pr354wnz5nl70u4v":"1","pr354we11aqlpp4w":"1","pr354v03myo2bo7b":"1","pr354v63s8rslcv9":"1","pr354vp2ac5w1475":"1","pr354vs27id73evj":"1","pr354vwa8rws201t":"1","pra15okcu3t5ob18":"-1","pra15no38usadrog":"1","pr354x99sy2hweha":"-1","pr354wz2t3pdxu96":"1"},"Tanja":{"pra15px12nephjox":"-1","pra15pu3vm33tk8q":"-1","pra15pn12aqtap4o":"0","pra15pf3bal4owcz":"-1","pra15p92aj8kop1p":"0","pra15p43fyu1terq":"1","pra15ox3hmtsdz3l":"-1","pra15oq302x3jkuz":"1","pra15okcu3t5ob18":"0","pra15od10vgdxzrb":"1","pra15o6zv5tgetvk":"-1","pra15nz2zm6knku7":"-1","pra15nuvxk0zmeh9":"-1","pra15no38usadrog":"-1","pra15nj2z2ludgom":"1","pra15nd3ov8vazx7":"1","pra15n3y8fe0petq":"-1","pra15mw24h20rl1a":"1","pr9mmge3mws6j0rw":"1","pr354z62mo0pncoz":"-1","pr354z11al5w7r7g":"-1","pr354yv3323z8b3z":"-1","pr354yn28trvqtyy":"-1","pr354yi4avhp502d":"-1","pr354yc26ml4vbae":"-1","pr354y8edf9t34mj":"1","pr354y14maa7029f":"1","pr354xy2bhnte2gq":"-1","pr354xvtxuexra59":"1","pr354xl3gvk4amup":"-1","pr354xc1yyqjiciw":"-1","pr354x99sy2hweha":"-1","pr354x42wpbkkg7m":"1","pr354wz2t3pdxu96":"1","pr354wv3w00i7czq":"1","pr354wr25rtuux68":"1","pr354wnz5nl70u4v":"1","pr354wj2ph8qjcwg":"1","pr354we11aqlpp4w":"-1","pr354w93pprqutov":"-1","pr354w62c5g8t1po":"1","pr354vz2dnwjdm1r":"1","pr354vwa8rws201t":"-1","pr354vs27id73evj":"-1","pr354vp2ac5w1475":"1","pr354v63s8rslcv9":"0","pr354v03myo2bo7b":"0","pr354uovlc3coazv":"-1"},"Colin":{"pra15px12nephjox":"1","pra15pu3vm33tk8q":"-1","pra15pn12aqtap4o":"1","pra15pf3bal4owcz":"1","pra15p92aj8kop1p":"1","pra15p43fyu1terq":"0","pra15ox3hmtsdz3l":"0","pra15oq302x3jkuz":"1","pra15okcu3t5ob18":"0","pra15od10vgdxzrb":"1","pra15o6zv5tgetvk":"0","pra15nz2zm6knku7":"1","pra15nuvxk0zmeh9":"-1","pra15no38usadrog":"0","pra15nj2z2ludgom":"1","pra15nd3ov8vazx7":"1","pra15n3y8fe0petq":"-1","pra15mw24h20rl1a":"1","pr9mmge3mws6j0rw":"0","pr354z62mo0pncoz":"1","pr354z11al5w7r7g":"1","pr354yv3323z8b3z":"1","pr354yn28trvqtyy":"1","pr354yi4avhp502d":"1","pr354yc26ml4vbae":"1","pr354y8edf9t34mj":"1","pr354y14maa7029f":"1","pr354xy2bhnte2gq":"-1","pr354xvtxuexra59":"1","pr354xl3gvk4amup":"-1","pr354xc1yyqjiciw":"-1","pr354x99sy2hweha":"-1","pr354x42wpbkkg7m":"1","pr354wz2t3pdxu96":"0","pr354wv3w00i7czq":"1","pr354wr25rtuux68":"1","pr354wnz5nl70u4v":"0","pr354wj2ph8qjcwg":"0","pr354we11aqlpp4w":"-1","pr354w93pprqutov":"-1","pr354w62c5g8t1po":"0","pr354vz2dnwjdm1r":"1","pr354vwa8rws201t":"0","pr354vs27id73evj":"1","pr354vp2ac5w1475":"1","pr354v63s8rslcv9":"1","pr354v03myo2bo7b":"1","pr354uovlc3coazv":"1"},"Claraaa":{"pra15px12nephjox":"1","pra15pu3vm33tk8q":"0","pra15pn12aqtap4o":"1","pra15pf3bal4owcz":"1","pra15p92aj8kop1p":"1","pra15p43fyu1terq":"1","pra15ox3hmtsdz3l":"1","pra15oq302x3jkuz":"1","pra15okcu3t5ob18":"0","pra15od10vgdxzrb":"1","pra15o6zv5tgetvk":"-1","pra15nz2zm6knku7":"1","pra15nuvxk0zmeh9":"0","pra15no38usadrog":"0","pra15nj2z2ludgom":"1","pra15nd3ov8vazx7":"1","pra15n3y8fe0petq":"-1","pra15mw24h20rl1a":"0","pr9mmge3mws6j0rw":"0","pr354z62mo0pncoz":"1","pr354z11al5w7r7g":"1","pr354yv3323z8b3z":"1","pr354yn28trvqtyy":"0","pr354yi4avhp502d":"0","pr354yc26ml4vbae":"1","pr354y8edf9t34mj":"0","pr354y14maa7029f":"1","pr354xy2bhnte2gq":"0","pr354xvtxuexra59":"1","pr354xl3gvk4amup":"0","pr354xc1yyqjiciw":"0","pr354x99sy2hweha":"0","pr354x42wpbkkg7m":"1","pr354wv3w00i7czq":"1","pr354wz2t3pdxu96":"1","pr354wr25rtuux68":"1","pr354wnz5nl70u4v":"1"}}