482 lines
12 KiB
PHP
Executable file
482 lines
12 KiB
PHP
Executable file
#!/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 post’s 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 (don’t 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
|
||
(you’ll 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";
|
||
}
|
||
}
|
||
|
||
?>
|