pfaltgall/pfaltgall

637 lines
17 KiB
PHP
Executable file
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/php
<?php
/*
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
// todo: add a "last updated on" header
$SCRIPTNAME='pfaltgall';
$SCRIPTVERSION='0.3.1';
$SCRIPTURL='https://git.lattuga.net/Jones/pfaltgall';
require 'lib/ckratelimit.php';
require 'lib/httpjson.php';
$configfp=null;
$outfp=null;
$conf=[
'host'=>null,
'token'=>null,
'lang'=>null
];
$help=
"[[[ SYNOPSIS ]]]
{$SCRIPTNAME} [options] <configuration file path> <output file path>
[[[ DESCRIPTION ]]]
This is {$SCRIPTNAME} v{$SCRIPTVERSION}, a CLI PHP script that can generate an html file
with a gallery from your Pixelfed profile. The html gallery file will load
images dynamically from your Pixelfed instance or its CDN, display each one
using almost all the available screen space and let you jump right from the
start to any point in the timeline.
It will also show each posts text content, its date, and provide each image
with its description (alt-text), if its present on Pixelfed.
Here is example gallery: https://rame.altervista.org/foto-pixelfed
In order to create the html gallery file, you just need to login to your
Pixelfed account from the official web frontend and get an app token
(Settings -> Applications -> Create new token), then create a configuration
file for {$SCRIPTNAME} like this (dont write the «---» lines):
---
host=your_instance_host
token=your_token
lang=your_html_page_language_code
---
For example:
---
host=pixelfed.social
token=as7f8a7s0d89f7as97df09a8s7d90f81jkl2h34lkj12h3jkl4
lang=it
---
Then run {$SCRIPTNAME} with the path of the configuration file you have created
and the path of an output file as arguments (if the output file exists, it
will be overwritten), e.g.: «{$SCRIPTNAME} pixelfed.social.conf index.html».
This will create an «index.html» file that will be ready to be put where you
want (youll also be able to see it locally, obviously). There is a sample
bash script that you can adapt to run {$SCRIPTNAME} and automatically upload
it where you want.
[[[ OPTIONS ]]]
-h, --help
Show this help text and exit.
[[[ DISCLAIMER AND LICENSE ]]]
This program comes with ABSOLUTELY NO WARRANTY; for details see the source.
This is free software, and you are welcome to redistribute it under certain
conditions; see <http://www.gnu.org/licenses/> for details.\n";
for ($i=1; $i<$argc; $i++) {
if ($argv[$i]=='-h' || $argv[$i]=='--help') {
echo $help;
exit(0);
} elseif ($argv[$i]=='--make-readme') {
file_put_contents(__DIR__.'/README.md',"```text\n{$help}\n```\n");
exit(0);
} elseif (is_null($configfp)) {
$configfp=$argv[$i];
} elseif (is_null($outfp)) {
$outfp=$argv[$i];
} else {
eecho("Error: «{$argv[$i]}» is not a valid option and configuration file and output file have already been set to «{$configfp}» and «{$outfp}» (use «-h» or «--help» to read help).\n");
exit(1);
}
}
$errors=[];
if (is_null($configfp))
$errors[]="you have not specified a config file";
if (is_null($outfp))
$errors[]="you have not specified an output file";
if (count($errors)>0) {
eecho("Errors:\n");
foreach ($errors as $val)
eecho(" - {$val}\n");
eecho("Use «-h» or «--help» to read help.\n");
exit(1);
}
$fconf=@parse_ini_file($configfp);
if ($fconf===false) {
eecho("Error: {$SCRIPTNAME} could not open configuration file «{$configfp}».\n");
exit(1);
}
$errors=[];
if (!array_key_exists('host',$fconf))
$errors[]="no «host» defined";
if (!array_key_exists('token',$fconf))
$errors[]="no «token» defined";
if (!array_key_exists('lang',$fconf))
$errors[]="no «lang» defined";
if (count($errors)>0) {
eecho("Error: {$SCRIPTNAME} has found errors in «{$configfp}» configuration file:\n");
foreach ($errors as $val)
eecho(" - {$val}\n");
eecho("Use «-h» or «--help» to read help.\n");
exit(1);
}
foreach ($conf as $key=>$val)
if (array_key_exists($key,$fconf))
$conf[$key]=$fconf[$key];
//print_r($conf);
$acc=httpjson("https://{$conf['host']}/api/v1/accounts/verify_credentials",null,null,null,null,$conf['token']);
//print_r($res);
if (!$acc['ok']) {
eecho("Error: {$SCRIPTNAME} could not retrieve the account associated with the given token ({$acc['errors']}).\n");
exit(2);
}
ckrl($acc['headers']);
$acc=$acc['content'];
$imgsurls=[];
$imgs='';
$thumbsurls=[];
$thumbs='';
$i=0;
$ic=0;
do {
$i++;
echo "\rRetrieving chunk {$i}";
$endpoint="https://{$conf['host']}/api/v1/accounts/{$acc['id']}/statuses?limit=40&only_media=1&exclude_replies=1&exclude_reblogs=1";
if (isset($max_id)) $endpoint.="&max_id={$max_id}";
$res=httpjson($endpoint,null,null,null,null,$conf['token']);
//print_r($res);
if (!$res['ok']) {
eecho("\rError: {$SCRIPTNAME} could not retrieve chunk {$i} of statuses ({$res['errors']}).\n");
exit(2);
}
$count=count($res['content']);
if ($count>0) {
foreach ($res['content'] as $status) {
//print_r($status);die();
if (isset($status['created_at']) && preg_match('#^\s+$#',$status['created_at'])!==1) {
$date=strtotime($status['created_at']);
$date=' <span class="grey">['.date('Y/m/d',$date).']</span>';
} else {
$date='';
}
if (isset($status['content']) && preg_match('#^\s+$#',$status['content'])!==1) {
$desc=strip_tags($status['content']);
$desc=preg_replace('/#\w+/','',$desc);
$desc=trim($desc);
} else {
$desc='';
}
if (isset($status['media_attachments']) && is_array($status['media_attachments'])) {
$ca=count($status['media_attachments']);
$ia=0;
foreach ($status['media_attachments'] as $attachment) {
if (isset($attachment['url'])) {
$thumburl=$attachment['preview_url'];
$thumbsurls[]=$thumburl;
$imgurl=$attachment['url'];
$imgsurls[]=$imgurl;
$ia++;
if (isset($attachment['description']) && preg_match('#^\s+$#',$attachment['description'])!==1)
$altdesc=' alt="'.htmlspecialchars(trim($attachment['description']),ENT_QUOTES|ENT_HTML5).'"';
else
$altdesc='';
if ($ca>1)
$icnt=" ({$ia}/{$ca})";
else
$icnt='';
/* $imgs.="<div class=\"page\"><table class=\"imgtab\"><tr><td class=\"imgcel\"><a href=\"{$imgurl}\" name=\"img{$ic}\"><img class=\"img\" decoding=\"async\" loading=\"lazy\" id=\"img{$ic}\" src=\"{$imgurl}\"{$altdesc}></a></td></tr><caption class=\"imgcaptcel\">{$desc}{$icnt}{$date}</caption></table></div>\n";
$thumbs.="<div class=\"thumbd\" id=\"thumbdiv{$ic}\" onclick=\"goto({$ic});\"><img class=\"thumb\" id=\"thumb{$ic}\" decoding=\"async\" loading=\"lazy\" src=\"{$thumburl}\"{$altdesc}></div>";*/
$imgs.="<div class=\"page\"><table class=\"imgtab\"><tr><td class=\"imgcel\"><a href=\"{$imgurl}\" name=\"img{$ic}\"><img class=\"img\" decoding=\"async\" loading=\"eager\" isset=\"0\" id=\"img{$ic}\" src=\"{$acc['avatar_static']}\"{$altdesc}></a></td></tr><caption class=\"imgcaptcel\">{$desc}{$icnt}{$date}</caption></table></div>\n";
$thumbs.="<div class=\"thumbd\" id=\"thumbdiv{$ic}\" onclick=\"goto({$ic});\"><img class=\"thumb\" id=\"thumb{$ic}\" decoding=\"async\" loading=\"eager\" isset=\"0\" src=\"{$acc['avatar_static']}\"{$altdesc}></div>";
$ic++;
}
}
}
}
$max_id=$res['content'][$count-1]['id'];
//echo "count: {$count}; max_id: {$max_id}\n";
if ($count<40)
break;
}
ckrl($res['headers']);
} while ($count>0);
echo "\n";
$accadd="{$acc['username']}@{$conf['host']}";
$title=$accadd;
$profhead="<a href=\"{$acc['url']}\">{$accadd}</a>";
if ($acc['display_name']!='') {
$accdispname=htmlspecialchars($acc['display_name'],ENT_QUOTES|ENT_HTML5);
$title="{$accdispname} ({$title})";
$profhead="<strong>{$accdispname}</strong><br>({$profhead})";
}
$html='<!DOCTYPE HTML>
<html lang="'.$conf['lang'].'">
<head>
<title>'.$title.'</title>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<meta name="description" content="Album">
<meta name="viewport" content="width=device-width, height=device-height, initial-scale=1.0, minimum-scale=1.0, user-scalable=yes">
<meta property="og:image" content="'.$acc['avatar_static'].'">
<link rel="icon" type="image/png" href="'.$acc['avatar_static'].'">
<!--
<meta property="og:image" content="imgs/ogimage.jpg">
<link rel="icon" type="image/png" href="imgs/icon-16.png" sizes="16x16">
<link rel="icon" type="image/png" href="imgs/icon-24.png" sizes="24x24">
<link rel="icon" type="image/png" href="imgs/icon-32.png" sizes="32x32">
<link rel="icon" type="image/png" href="imgs/icon-64.png" sizes="64x64">
<link rel="icon" type="image/png" href="imgs/icon-128.png" sizes="128x128">
<link rel="apple-touch-icon-precomposed" href="imgs/icon-180.png">
<link rel="icon" type="image/png" href="imgs/icon-192.png" sizes="192x192">
<link rel="icon" type="image/png" href="imgs/icon-256.png" sizes="256x256">
<link rel="icon" type="image/png" href="imgs/icon-512.png" sizes="512x512">
-->
<!-- <link rel="stylesheet" type="text/css" href="gallery.css"> -->
<style>
* {
box-sizing: border-box;
}
html {
scroll-behavior: smooth;
height: 100vh;
}
body {
font-family: "sans";
font-size: 12pt;
background-color: black;
color: white;
margin: 0;
height: 100vh;
}
a {
color: #87decd;
}
a:focus {
outline: none;
}
p {
margin: 0;
padding: 0;
text-indent: 3mm;
line-height: 1.5em;
}
p.firstp, p.center {
text-indent: 0;
}
p.center {
text-align: center;
text-wrap: balance;
padding: 0 0 1.5em 0;
}
.profile {
width: 640px;
}
.avatar {
border-radius: 12px;
}
.grey {
color: #888;
}
hr {
display: block;
border: none;
height: 1px;
background-color: #666666;
margin: 3mm 0 3mm 0;
}
#maindiv {
width: 100%;
height: 100%;
overflow: auto;
scroll-snap-type: y mandatory;
scroll-padding: 0;
}
.page {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
scroll-snap-stop: always;
scroll-snap-align: start;
scroll-margin: 0;
/*border: 1px solid yellow;*/
}
.imgtab, .imgtab tr, .imgtab td {
margin: 0;
padding: 0;
border: none;
border-collapse: collapse;
table-layout: fixed;
scroll-snap-align: none;
}
.img {
display: block;
position: relative;
max-width: 92vw;
max-height: 92vh;
border: 8px solid white;
border-bottom: none;
scroll-snap-align: none;
}
.imgcaptcel {
background-color: white;
color: black;
padding: 4px 8px 4px 8px;
font-size: 10pt;
caption-side: bottom;
text-align: left;
line-height: 1.2;
scroll-snap-align: none;
}
.textdiv {
width: 20cm;
max-width: 92%;
background-color: #333333;
border-radius: 8px;
padding: 3mm;
scroll-snap-align: none;
}
#notif {
display: none;
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background-color: rgb(51 51 51 / .85);
padding: 4px 6px 4px 6px;
color: white;
border-radius: 6px;
font-size: 10pt;
z-index: 1;
cursor: pointer;
}
.link {
margin-top: 2px;
float: right;
color: #333333;
cursor: pointer;
}
#toolbar {
display: block;
width: 100%;
height: 48px;
position: fixed;
right: 0;
bottom: 0;
/*background-color: rgb(0 0 0 / .85);*/
background-color: #444444;
z-index: 1;
overflow: auto;
cursor: pointer;
text-align: center;
-webkit-user-select: none; /* Safari */
-moz-user-select: none; /* Firefox */
-ms-user-select: none; /* IE10+/Edge */
user-select: none; /* Standard */
}
#browser {
display: none;/*default: block*/
text-align: center;
width: 100%;
height: 100%;
position: fixed;
left: 0;
top: 0;
background-color: rgb(0 0 0 / .85);
padding: 3px;
z-index: 2;
overflow: auto;
/*border: 1px solid yellow;*/
}
.thumbd {
display: inline-flex;
justify-content: center;
width: 320px;
height: 320px;
background-color: white;
margin: 3px;
border: 3px solid white;
border-radius: 12px;
overflow: clip;
cursor: pointer;
}
.thumb {
display: block;
object-fit: contain;
max-width: 100%;
max-height: 100%;
/*width: auto;
height: auto;*/
}
@media only screen and (max-width:15cm) {
.img {
max-width: 100vw;
max-height: 98vh;
border: 2px solid white;
}
.imgcaptcel {
padding: 1px 2px 1px 2px;
font-size: 8pt;
}
.textdiv {
font-size: 9pt;
}
.link {
margin-top: 0;
}
}
</style>
<script type="text/javascript">
let totimgs='.$ic.';
let thumbsurls=["'.implode('", "',$thumbsurls).'"];
let imgsurls=["'.implode('", "',$imgsurls).'"];
window.onresize=setToolBar;
function prel() {
let ph=window.innerHeight;
if (window.innerWidth < window.innerHeight)
ph-=48;
let md=document.getElementById("maindiv"),
th=md.scrollHeight,
pages=th/ph,
sy=md.scrollTop,
page=Math.floor(pages-(th-sy)/ph)+1,
img;
// console.log("ph: "+ph+"; th: "+th+"; sy: "+sy+"; pages: "+pages+"; page: "+page);
if (page>1) {//current
img=document.getElementById("img"+(page-2));
if (img.getAttribute("isset")=="0") {
img.src=imgsurls[page-2];
img.loading="eager";
img.setAttribute=("isset","1");
}
}
if (page+1<pages) {//next
img=document.getElementById("img"+(page-1));
if (img.getAttribute("isset")=="0") {
img.src=imgsurls[page-1];
img.loading="eager";
img.setAttribute=("isset","1");
}
}
if (page>2) {//previous
img=document.getElementById("img"+(page-3));
if (img.getAttribute("isset")=="0") {
img.src=imgsurls[page-3];
img.loading="eager";
img.setAttribute=("isset","1");
}
}
if (page+2<pages) {//next-next
img=document.getElementById("img"+page);
if (img.getAttribute("isset")=="0") {
img.src=imgsurls[page];
img.loading="eager";
img.setAttribute=("isset","1");
}
}
if (page>3) {//previous-previous
img=document.getElementById("img"+(page-4));
if (img.getAttribute("isset")=="0") {
img.src=imgsurls[page-4];
img.loading="eager";
img.setAttribute=("isset","1");
}
}
}
function isInViewport(el) {
let rect=el.getBoundingClientRect(),
wh=(window.innerHeight || document.documentElement.clientHeight),
ww=(window.innerWidth || document.documentElement.clientWidth);
return (
(rect.top>=0 && rect.top<=wh && rect.left>=0 && rect.left<=ww) ||
(rect.bottom>=0 && rect.bottom<=wh && rect.right>=0 && rect.right<=ww)
);
}
function prelb() {
let i, timg;
for (i=0; i<totimgs; i++) {
timg=document.getElementById("thumb"+i);
if (timg.getAttribute("isset")=="0" && isInViewport(timg)) {
console.log(i);
timg.src=thumbsurls[i];
timg.setAttribute("isset","1");
}
}
}
function setToolBar() {
var md=document.getElementById("maindiv"),
td=document.getElementById("toolbar"),
bd=document.getElementById("browser"),
dw=window.innerWidth,
dh=window.innerHeight;
//console.log("dw: "+dw+"; dh: "+dh);
if (dw >= dh) {
dw-=48;
md.style.width=dw+"px";
md.style.height="100%";
bd.style.width=dw+"px";
bd.style.height="100%";
td.setAttribute("style","width:48px;height:100%;padding-left:15px;padding-top:0;writing-mode:vertical-lr;text-orientation:upright;");
} else {
dh-=48;
md.style.width="100%";
md.style.height=dh+"px";
bd.style.width="100%";
bd.style.height=dh+"px";
td.setAttribute("style","width:100%;height:48px;padding-left:0;padding-top:13px;writing-mode:horizontal-tb;text-orientation:mixed;");
}
}
let bshows=false;
function shbrowser() {
bd=document.getElementById("browser");
if (bshows) {
bd.style.display="none";
bshows=false;
} else {
bd.style.display="block";
bshows=true;
prelb();
}
}
function goto(i) {
document.location.href="#img"+i;
document.getElementById("browser").style.display="none";
bshows=false;
}
</script>
</head>
<body onload="setToolBar();prel();">
<div id="browser" onscroll="prelb();">
'.$thumbs."\n".'
</div>
<div id="toolbar" onclick="shbrowser();">Miniature</div>
<div id="maindiv" onscroll="prel();">
<div class="page">
<div class="profile">
<p class="center"><img src="'.$acc['avatar_static'].'" class="avatar"></p>
<p class="center">'.$profhead.'</p>
<p class="center">'.nl2br($acc['note']).'</p>
<p class="center"><span class="grey">Made with <a href="'.$SCRIPTURL.'">'.$SCRIPTNAME.'</a> v'.$SCRIPTVERSION.'</span></p>
</div>
</div>
'.$imgs.'
</div>
</body>
</html>
';
if (@file_put_contents($outfp,$html)===false) {
eecho("Error: {$SCRIPTNAME} could not save html into «{$outfp}».\n");
exit(2);
}
exit(0);
function eecho($text) {
fwrite(STDERR,$text);
}
function ckrl($headers) {
$rl=ckratelimit($headers);
if ($rl['ok'] && $rl['remaining']==0) {
echo "\rInfo: reached rate limit, sleeping for {$rl['sleep']} second(s)... ";
sleep($rl['sleep']);
echo "\n";
}
}
?>