pfaltgall/pfaltgall

482 lines
12 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/>.
*/
$SCRIPTNAME='pfaltgall';
$SCRIPTVERSION='0.2';
require 'lib/ckratelimit.php';
require 'lib/httpjson.php';
$configfp=null;
$outfp=null;
$conf=[
'host'=>null,
'token'=>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, display each one using almost all the available screen space and
will let you jump right from the start to any point in the timeline. It will
also show each posts text content, its date, and each image description
(alt-text), if present.
See my example gallery here: https://rame.altervista.org/foto-pixelfed
In order to create the html gallery file, you just need to login to your
Pixelfed account 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
---
For example:
---
host=pixelfed.social
token=as7f8a7s0d89f7as97df09a8s7d90f81jkl2h34lkj12h3jkl4
---
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} goofy@pixelfed.social.conf index.html».
This will create an html file that will be ready to be put where you want
(youll also be able to see it locally, obviously).
[[[ 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 (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'];
$imgurls=[];
$imgs='';
$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) {
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'])) {
$imgurl=$attachment['url'];
$imgurls[]=$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=\"{$acc['avatar_static']}\"{$altdesc}></a></td></tr><caption class=\"imgcaptcel\">{$desc}{$icnt}{$date}</caption></table></div>\n";
$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";
$title="{$acc['username']}@{$conf['host']}";
if ($acc['display_name']!='') $title=htmlspecialchars($acc['display_name'],ENT_QUOTES|ENT_HTML5)." ({$title})";
$html='<!DOCTYPE HTML>
<html lang="en">
<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.3em;
}
p.firstp, p.center {
text-indent: 0;
}
p.center {
text-align: center;
text-wrap: balance;
padding: 1em 0 1em 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;
}
#tools {
/*display: none;*/
width: 100%;
position: fixed;
left: 0;
bottom: 0;
background-color: rgb(51 51 51 / .85);;
padding: 4px 6px 4px 6px;
color: white;
font-size: 10pt;
z-index: 1;
}
@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 imgurls=["'.implode('", "',$imgurls).'"];
let phimgurl="'.$acc['avatar_static'].'";//placeholder image url
function prel() {
var md=document.getElementById("maindiv"),
ph=window.innerHeight,
th=md.scrollHeight,
pages=Math.round(th/ph),
sy=Math.round(md.scrollTop),
page=Math.round(pages-(th-sy)/ph)+1,
img,
u;
//console.log(ph+" "+th+" "+pages+" "+sy+" "+page);
if (page>1) {//current
img=document.getElementById("img"+(page-2));
if (img.src==phimgurl) {
u=imgurls[page-2];
img.src=u;
img.loading="eager";
}
}
if (page<pages) {//next
img=document.getElementById("img"+(page-1));
if (img.src==phimgurl) {
u=imgurls[page-1];
img.src=u;
img.loading="eager";
}
}
if (page>2) {//previous
img=document.getElementById("img"+(page-3));
if (img.src==phimgurl) {
u=imgurls[page-3];
img.src=u;
img.loading="eager";
}
}
}
</script>
</head>
<body onload="prel();">
<div id="maindiv" onscrollend="prel();">
<div class="page">
<div class="profile">
<p class="center"><img src="'.$acc['avatar_static'].'" class="avatar"></p>
<p class="center"><a href="'.$acc['url'].'">'.$title.'</a></p>
<p class="center">'.nl2br($acc['note']).'</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";
}
}
?>