pfaltgall 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503
  1. #!/usr/bin/php
  2. <?php
  3. /*
  4. This program is free software: you can redistribute it and/or modify
  5. it under the terms of the GNU General Public License as published by
  6. the Free Software Foundation, either version 3 of the License, or
  7. (at your option) any later version.
  8. This program is distributed in the hope that it will be useful,
  9. but WITHOUT ANY WARRANTY; without even the implied warranty of
  10. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  11. GNU General Public License for more details.
  12. You should have received a copy of the GNU General Public License
  13. along with this program. If not, see <http://www.gnu.org/licenses/>.
  14. */
  15. // todo: add a "last updated on" header
  16. $SCRIPTNAME='pfaltgall';
  17. $SCRIPTVERSION='0.2.1';
  18. $SCRIPTURL='https://git.lattuga.net/Jones/pfaltgall';
  19. require 'lib/ckratelimit.php';
  20. require 'lib/httpjson.php';
  21. $configfp=null;
  22. $outfp=null;
  23. $conf=[
  24. 'host'=>null,
  25. 'token'=>null,
  26. ];
  27. $help=
  28. "[[[ SYNOPSIS ]]]
  29. {$SCRIPTNAME} [options] <configuration file path> <output file path>
  30. [[[ DESCRIPTION ]]]
  31. This is {$SCRIPTNAME} v{$SCRIPTVERSION}, a CLI PHP script that can generate an html file
  32. with a gallery from your Pixelfed profile. The html gallery file will load
  33. images dynamically, display each one using almost all the available screen
  34. space and let you jump right from the start to any point in the timeline.
  35. It will also show each post’s text content, its date, and provide each image
  36. with its description (alt-text), if present.
  37. See my example gallery here: https://rame.altervista.org/foto-pixelfed
  38. In order to create the html gallery file, you just need to login to your
  39. Pixelfed account and get an app token (Settings -> Applications -> Create new
  40. token), then create a configuration file for {$SCRIPTNAME} like this (don’t write
  41. the «---» lines):
  42. ---
  43. host=your_instance_host
  44. token=your_token
  45. ---
  46. For example:
  47. ---
  48. host=pixelfed.social
  49. token=as7f8a7s0d89f7as97df09a8s7d90f81jkl2h34lkj12h3jkl4
  50. ---
  51. Then run {$SCRIPTNAME} with the path of the configuration file you have created
  52. and the path of an output file as arguments (if the output file exists, it
  53. will be overwritten), e.g.: «{$SCRIPTNAME} goofy@pixelfed.social.conf index.html».
  54. This will create an html file that will be ready to be put where you want
  55. (you’ll also be able to see it locally, obviously).
  56. [[[ OPTIONS ]]]
  57. -h, --help
  58. Show this help text and exit.
  59. [[[ DISCLAIMER AND LICENSE ]]]
  60. This program comes with ABSOLUTELY NO WARRANTY; for details see the source.
  61. This is free software, and you are welcome to redistribute it under certain
  62. conditions; see <http://www.gnu.org/licenses/> for details.\n";
  63. for ($i=1; $i<$argc; $i++) {
  64. if ($argv[$i]=='-h' || $argv[$i]=='--help') {
  65. echo $help;
  66. exit(0);
  67. } elseif ($argv[$i]=='--make-readme') {
  68. file_put_contents(__DIR__.'/README.md',"```text\n{$help}\n```\n");
  69. exit(0);
  70. } elseif (is_null($configfp)) {
  71. $configfp=$argv[$i];
  72. } elseif (is_null($outfp)) {
  73. $outfp=$argv[$i];
  74. } else {
  75. 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");
  76. exit(1);
  77. }
  78. }
  79. $errors=[];
  80. if (is_null($configfp))
  81. $errors[]="you have not specified a config file";
  82. if (is_null($outfp))
  83. $errors[]="you have not specified an output file";
  84. if (count($errors)>0) {
  85. eecho("Errors:\n");
  86. foreach ($errors as $val)
  87. eecho(" - {$val}\n");
  88. eecho("Use «-h» or «--help» to read help.\n");
  89. exit(1);
  90. }
  91. $fconf=@parse_ini_file($configfp);
  92. if ($fconf===false) {
  93. eecho("Error: {$SCRIPTNAME} could not open configuration file «{$configfp}».\n");
  94. exit(1);
  95. }
  96. $errors=[];
  97. if (!array_key_exists('host',$fconf))
  98. $errors[]="no «host» defined";
  99. if (!array_key_exists('token',$fconf))
  100. $errors[]="no «token» defined";
  101. if (count($errors)>0) {
  102. eecho("Error: {$SCRIPTNAME} has found errors in «{$configfp}» configuration file:\n");
  103. foreach ($errors as $val)
  104. eecho(" - {$val}\n");
  105. eecho("Use «-h» or «--help» to read help.\n");
  106. exit(1);
  107. }
  108. foreach ($conf as $key=>$val)
  109. if (array_key_exists($key,$fconf))
  110. $conf[$key]=$fconf[$key];
  111. //print_r($conf);
  112. $acc=httpjson("https://{$conf['host']}/api/v1/accounts/verify_credentials",null,null,null,null,$conf['token']);
  113. //print_r($res);
  114. if (!$acc['ok']) {
  115. eecho("Error: {$SCRIPTNAME} could not retrieve the account associated with the given token ({$acc['errors']}).\n");
  116. exit(2);
  117. }
  118. ckrl($acc['headers']);
  119. $acc=$acc['content'];
  120. $imgurls=[];
  121. $imgs='';
  122. $i=0;
  123. $ic=0;
  124. do {
  125. $i++;
  126. echo "\rRetrieving chunk {$i}";
  127. $endpoint="https://{$conf['host']}/api/v1/accounts/{$acc['id']}/statuses?limit=40&only_media=1&exclude_replies=1&exclude_reblogs=1";
  128. if (isset($max_id)) $endpoint.="&max_id={$max_id}";
  129. $res=httpjson($endpoint,null,null,null,null,$conf['token']);
  130. //print_r($res);
  131. if (!$res['ok']) {
  132. eecho("\rError: {$SCRIPTNAME} could not retrieve chunk {$i} of statuses ({$res['errors']}).\n");
  133. exit(2);
  134. }
  135. $count=count($res['content']);
  136. if ($count>0) {
  137. foreach ($res['content'] as $status) {
  138. if (isset($status['created_at']) && preg_match('#^\s+$#',$status['created_at'])!==1) {
  139. $date=strtotime($status['created_at']);
  140. $date=' <span class="grey">['.date('Y/m/d',$date).']</span>';
  141. } else {
  142. $date='';
  143. }
  144. if (isset($status['content']) && preg_match('#^\s+$#',$status['content'])!==1) {
  145. $desc=strip_tags($status['content']);
  146. $desc=preg_replace('/#\w+/','',$desc);
  147. $desc=trim($desc);
  148. } else {
  149. $desc='';
  150. }
  151. if (isset($status['media_attachments']) && is_array($status['media_attachments'])) {
  152. $ca=count($status['media_attachments']);
  153. $ia=0;
  154. foreach ($status['media_attachments'] as $attachment) {
  155. if (isset($attachment['url'])) {
  156. $imgurl=$attachment['url'];
  157. $imgurls[]=$imgurl;
  158. $ia++;
  159. if (isset($attachment['description']) && preg_match('#^\s+$#',$attachment['description'])!==1)
  160. $altdesc=' alt="'.htmlspecialchars(trim($attachment['description']),ENT_QUOTES|ENT_HTML5).'"';
  161. else
  162. $altdesc='';
  163. if ($ca>1)
  164. $icnt=" ({$ia}/{$ca})";
  165. else
  166. $icnt='';
  167. $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";
  168. $ic++;
  169. }
  170. }
  171. }
  172. }
  173. $max_id=$res['content'][$count-1]['id'];
  174. //echo "count: {$count}; max_id: {$max_id}\n";
  175. if ($count<40)
  176. break;
  177. }
  178. ckrl($res['headers']);
  179. } while ($count>0);
  180. echo "\n";
  181. $accadd="{$acc['username']}@{$conf['host']}";
  182. $title=$accadd;
  183. $profhead="<a href=\"{$acc['url']}\">{$accadd}</a>";
  184. if ($acc['display_name']!='') {
  185. $accdispname=htmlspecialchars($acc['display_name'],ENT_QUOTES|ENT_HTML5);
  186. $title="{$accdispname} ({$title})";
  187. $profhead="<strong>{$accdispname}</strong><br>({$profhead})";
  188. }
  189. $html='<!DOCTYPE HTML>
  190. <html lang="en">
  191. <head>
  192. <title>'.$title.'</title>
  193. <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
  194. <meta name="description" content="Album">
  195. <meta name="viewport" content="width=device-width, height=device-height, initial-scale=1.0, minimum-scale=1.0, user-scalable=yes">
  196. <meta property="og:image" content="'.$acc['avatar_static'].'">
  197. <link rel="icon" type="image/png" href="'.$acc['avatar_static'].'">
  198. <!--
  199. <meta property="og:image" content="imgs/ogimage.jpg">
  200. <link rel="icon" type="image/png" href="imgs/icon-16.png" sizes="16x16">
  201. <link rel="icon" type="image/png" href="imgs/icon-24.png" sizes="24x24">
  202. <link rel="icon" type="image/png" href="imgs/icon-32.png" sizes="32x32">
  203. <link rel="icon" type="image/png" href="imgs/icon-64.png" sizes="64x64">
  204. <link rel="icon" type="image/png" href="imgs/icon-128.png" sizes="128x128">
  205. <link rel="apple-touch-icon-precomposed" href="imgs/icon-180.png">
  206. <link rel="icon" type="image/png" href="imgs/icon-192.png" sizes="192x192">
  207. <link rel="icon" type="image/png" href="imgs/icon-256.png" sizes="256x256">
  208. <link rel="icon" type="image/png" href="imgs/icon-512.png" sizes="512x512">
  209. -->
  210. <!-- <link rel="stylesheet" type="text/css" href="gallery.css"> -->
  211. <style>
  212. * {
  213. box-sizing: border-box;
  214. }
  215. html {
  216. scroll-behavior: smooth;
  217. height: 100vh;
  218. }
  219. body {
  220. font-family: "sans";
  221. font-size: 12pt;
  222. background-color: black;
  223. color: white;
  224. margin: 0;
  225. height: 100vh;
  226. }
  227. a {
  228. color: #87decd;
  229. }
  230. a:focus {
  231. outline: none;
  232. }
  233. p {
  234. margin: 0;
  235. padding: 0;
  236. text-indent: 3mm;
  237. line-height: 1.5em;
  238. }
  239. p.firstp, p.center {
  240. text-indent: 0;
  241. }
  242. p.center {
  243. text-align: center;
  244. text-wrap: balance;
  245. padding: 0 0 1.5em 0;
  246. }
  247. .profile {
  248. width: 640px;
  249. }
  250. .avatar {
  251. border-radius: 12px;
  252. }
  253. .grey {
  254. color: #888;
  255. }
  256. hr {
  257. display: block;
  258. border: none;
  259. height: 1px;
  260. background-color: #666666;
  261. margin: 3mm 0 3mm 0;
  262. }
  263. #maindiv {
  264. width: 100%;
  265. height: 100%;
  266. overflow: auto;
  267. scroll-snap-type: y mandatory;
  268. scroll-padding: 0;
  269. }
  270. .page {
  271. display: flex;
  272. align-items: center;
  273. justify-content: center;
  274. width: 100%;
  275. height: 100%;
  276. scroll-snap-stop: always;
  277. scroll-snap-align: start;
  278. scroll-margin: 0;
  279. /*border: 1px solid yellow;*/
  280. }
  281. .imgtab, .imgtab tr, .imgtab td {
  282. margin: 0;
  283. padding: 0;
  284. border: none;
  285. border-collapse: collapse;
  286. table-layout: fixed;
  287. scroll-snap-align: none;
  288. }
  289. .img {
  290. display: block;
  291. position: relative;
  292. max-width: 92vw;
  293. max-height: 92vh;
  294. border: 8px solid white;
  295. border-bottom: none;
  296. scroll-snap-align: none;
  297. }
  298. .imgcaptcel {
  299. background-color: white;
  300. color: black;
  301. padding: 4px 8px 4px 8px;
  302. font-size: 10pt;
  303. caption-side: bottom;
  304. text-align: left;
  305. line-height: 1.2;
  306. scroll-snap-align: none;
  307. }
  308. .textdiv {
  309. width: 20cm;
  310. max-width: 92%;
  311. background-color: #333333;
  312. border-radius: 8px;
  313. padding: 3mm;
  314. scroll-snap-align: none;
  315. }
  316. #notif {
  317. display: none;
  318. position: fixed;
  319. top: 50%;
  320. left: 50%;
  321. transform: translate(-50%, -50%);
  322. background-color: rgb(51 51 51 / .85);
  323. padding: 4px 6px 4px 6px;
  324. color: white;
  325. border-radius: 6px;
  326. font-size: 10pt;
  327. z-index: 1;
  328. cursor: pointer;
  329. }
  330. .link {
  331. margin-top: 2px;
  332. float: right;
  333. color: #333333;
  334. cursor: pointer;
  335. }
  336. #tools {
  337. /*display: none;*/
  338. width: 100%;
  339. position: fixed;
  340. left: 0;
  341. bottom: 0;
  342. background-color: rgb(51 51 51 / .85);;
  343. padding: 4px 6px 4px 6px;
  344. color: white;
  345. font-size: 10pt;
  346. z-index: 1;
  347. }
  348. @media only screen and (max-width:15cm) {
  349. .img {
  350. max-width: 100vw;
  351. max-height: 98vh;
  352. border: 2px solid white;
  353. }
  354. .imgcaptcel {
  355. padding: 1px 2px 1px 2px;
  356. font-size: 8pt;
  357. }
  358. .textdiv {
  359. font-size: 9pt;
  360. }
  361. .link {
  362. margin-top: 0;
  363. }
  364. }
  365. </style>
  366. <script type="text/javascript">
  367. let imgurls=["'.implode('", "',$imgurls).'"];
  368. let phimgurl="'.$acc['avatar_static'].'";//placeholder image url
  369. function prel() {
  370. var md=document.getElementById("maindiv"),
  371. ph=window.innerHeight,
  372. th=md.scrollHeight,
  373. pages=Math.round(th/ph),
  374. sy=Math.round(md.scrollTop),
  375. page=Math.round(pages-(th-sy)/ph)+1,
  376. img;
  377. //console.log(ph+" "+th+" "+pages+" "+sy+" "+page);
  378. if (page>1) {//current
  379. img=document.getElementById("img"+(page-2));
  380. if (img.src==phimgurl) {
  381. img.src=imgurls[page-2];
  382. img.loading="eager";
  383. }
  384. }
  385. if (page<pages) {//next
  386. img=document.getElementById("img"+(page-1));
  387. if (img.src==phimgurl) {
  388. img.src=imgurls[page-1];
  389. img.loading="eager";
  390. }
  391. }
  392. if (page>2) {//previous
  393. img=document.getElementById("img"+(page-3));
  394. if (img.src==phimgurl) {
  395. img.src=imgurls[page-3];
  396. img.loading="eager";
  397. }
  398. }
  399. if (page+1<pages) {//next-next
  400. img=document.getElementById("img"+page);
  401. if (img.src==phimgurl) {
  402. img.src=imgurls[page];
  403. img.loading="eager";
  404. }
  405. }
  406. if (page>3) {//previous-previous
  407. img=document.getElementById("img"+(page-4));
  408. if (img.src==phimgurl) {
  409. img.src=imgurls[page-4];
  410. img.loading="eager";
  411. }
  412. }
  413. }
  414. </script>
  415. </head>
  416. <body onload="prel();">
  417. <div id="maindiv" onscrollend="prel();">
  418. <div class="page">
  419. <div class="profile">
  420. <p class="center"><img src="'.$acc['avatar_static'].'" class="avatar"></p>
  421. <p class="center">'.$profhead.'</p>
  422. <p class="center">'.nl2br($acc['note']).'</p>
  423. <p class="center"><span class="grey">Made with <a href="'.$SCRIPTURL.'">'.$SCRIPTNAME.'</a> v'.$SCRIPTVERSION.'</span></p>
  424. </div>
  425. </div>
  426. '.$imgs.'
  427. </div>
  428. </body>
  429. </html>
  430. ';
  431. if (@file_put_contents($outfp,$html)===false) {
  432. eecho("Error: {$SCRIPTNAME} could not save html into «{$outfp}».\n");
  433. exit(2);
  434. }
  435. exit(0);
  436. function eecho($text) {
  437. fwrite(STDERR,$text);
  438. }
  439. function ckrl($headers) {
  440. $rl=ckratelimit($headers);
  441. if ($rl['ok'] && $rl['remaining']==0) {
  442. echo "\rInfo: reached rate limit, sleeping for {$rl['sleep']} second(s)... ";
  443. sleep($rl['sleep']);
  444. echo "\n";
  445. }
  446. }
  447. ?>