Compare commits

...

10 commits

10 changed files with 654 additions and 470 deletions

View file

@ -2,6 +2,8 @@ https://voussoir.net
## Mirrors
https://git.voussoir.net/voussoir/voussoir.net
https://github.com/voussoir/voussoir.net
https://gitlab.com/voussoir/voussoir.net

View file

@ -86,6 +86,8 @@ def linkchecker(do_external=True):
url = queue.popleft()
if url == 'https://voussoir.net/writing':
url = 'https://voussoir.net/writing/'
if '/slamming_and_blasting' in url:
continue
result = dotdict.DotDict()
result.exc = None
result.url = url
@ -99,7 +101,7 @@ def linkchecker(do_external=True):
if link not in seen:
queue.append(link)
seen.add(link)
elif result.head.headers['content-type'] == 'text/html' and not url.endswith('.html'):
elif 'text/html' in result.head.headers['content-type'] and not url.endswith('.html'):
log.debug('GET %s', url)
response = session.get(url)
soup = bs4.BeautifulSoup(response.text, 'html.parser')

View file

@ -426,7 +426,7 @@ body.start_eating_that_trashcan .cvitem_details
<div class="cvitem_details">
<p>trkpt is a 24/7 GPS recorder that shows me where I've been.</p>
<p><a href="https://voussoir.net/writing/obsessed_with_gpx">https://voussoir.net/writing/obsessed_with_gpx</a></p>
<p><a href="https://github.com/voussoir/trkpt">https://github.com/voussoir/trkpt</a></p>
<p><a href="https://git.voussoir.net/voussoir/trkpt">https://git.voussoir.net/voussoir/trkpt</a></p>
</div>
<img class="cvitem_logo" style='background-color: transparent !important' src="./cv/trkpt.png"/>
</div>
@ -436,7 +436,7 @@ body.start_eating_that_trashcan .cvitem_details
<div class="cvitem_details">
<p>BringRSS is an RSS client and newsreader with a web interface, made with Flask and SQLite3. RSS is a great way to keep up with your favorite forums, bloggers, podcasts, and newspapers since all the new posts come straight to you in a single place. BringRSS can send news objects to your own Python scripts, allowing for powerful automation like podcast downloading, email notifications, and other more niche features that would be outside the scope of the BringRSS application itself.</p>
<p><a href="https://bringrss.voussoir.net">Live demo</a></p>
<p><a href="https://github.com/voussoir/bringrss">https://github.com/voussoir/bringrss</a></p>
<p><a href="https://git.voussoir.net/voussoir/bringrss">https://git.voussoir.net/voussoir/bringrss</a></p>
</div>
<img class="cvitem_logo" src="./cv/bringrss.png"/>
</div>
@ -445,7 +445,7 @@ body.start_eating_that_trashcan .cvitem_details
<h3 class="cvitem_title" id="ycdl">YCDL</h3>
<div class="cvitem_details">
<p>YoutubeChannelDownloader was born out of a dissatisfaction with YouTube's own interface for keeping track of which videos I have already watched, as well as a desire to integrate with youtube-dl. YCDL makes it easy for me to watch through a channel's catalog of videos, picking which ones I'd like to download while ignoring the others. Plus, as it creates an offline database, it will retain metadata about videos even after they are removed or deleted from the original YouTube channel.</p>
<p><a href="https://github.com/voussoir/ycdl">https://github.com/voussoir/ycdl</a></p>
<p><a href="https://git.voussoir.net/voussoir/ycdl">https://git.voussoir.net/voussoir/ycdl</a></p>
</div>
<img class="cvitem_logo" src="./cv/ycdl.png"/>
</div>
@ -456,7 +456,7 @@ body.start_eating_that_trashcan .cvitem_details
<p>Etiquette is a tag-based file organization system with a web interface, built with Flask and SQLite3. Tag-based systems solve problems that a traditional folder hierarchy can't: <em>which folder should a file go in if it equally belongs in both?</em> and <em>how do I make my files searchable without littering the filenames themselves with keywords?</em></p>
<p>Etiquette is unique because the tags themselves are hierarchical. By tagging one of your vacation photos with the <code>family.parents.dad</code> tag, it will automatically appear in searches for <code>family.parents</code> and <code>family</code> as well. A traditional folder system, here called albums, is available to bundle files that always belong together without creating a bespoke tag to represent that bundle. Regardless, the files on disk are never modified.</p>
<p><a href="https://etiquette.voussoir.net">Live demo</a></p>
<p><a href="https://github.com/voussoir/etiquette">https://github.com/voussoir/etiquette</a></p>
<p><a href="https://git.voussoir.net/voussoir/etiquette">https://git.voussoir.net/voussoir/etiquette</a></p>
</div>
<img class="cvitem_logo" src="./cv/etiquette.png"/>
</div>
@ -466,7 +466,7 @@ body.start_eating_that_trashcan .cvitem_details
<div class="cvitem_details">
<p>Timesearch is a package of tools for archiving data from reddit.com. Subreddits, user posts, comments, CSS files, and community wiki files can be downloaded and easily updated.</p>
<p>Originally, it used the <code>timestamp</code> query parameter of reddit's elasticsearch, but since that feature's removal Timesearch instead queries the third-party pushshift.io database for preliminary data, then queries reddit for updated information about each item.</p>
<p><a href="https://github.com/voussoir/timesearch">https://github.com/voussoir/timesearch</a></p></div>
<p><a href="https://git.voussoir.net/voussoir/timesearch">https://git.voussoir.net/voussoir/timesearch</a></p></div>
<img class="cvitem_logo" src="./cv/timesearch.png"/>
</div>
@ -474,7 +474,7 @@ body.start_eating_that_trashcan .cvitem_details
<h3 class="cvitem_title" id="voussoirkit">voussoirkit</h3>
<div class="cvitem_details">
<p>The voussoirkit library contains code that I have found useful to include in my other projects. Everything from <code>bytestring</code> that converts integer numbers of bytes into "3.145 MiB" strings, to <code>pathclass</code> and <code>spinal</code> which provide object-oriented file and directory operations and copy routines. Some modules like <code>winglob</code> boost cross-compatibility by smoothing over differences between Windows and Unix. This way I can easily deploy new features and bug fixes to all my programs.</p>
<p><a href="https://github.com/voussoir/voussoirkit">https://github.com/voussoir/voussoirkit</a></p>
<p><a href="https://git.voussoir.net/voussoir/voussoirkit">https://git.voussoir.net/voussoir/voussoirkit</a></p>
</div>
<img class="cvitem_logo" src="./cv/voussoirkit.png"/>
</div>
@ -483,7 +483,7 @@ body.start_eating_that_trashcan .cvitem_details
<h3 class="cvitem_title" id="hnarchive">HN Archive</h3>
<div class="cvitem_details">
<p>hnarchive is a tool that downloads all submissions and comments on <a href="https://news.ycombinator.com">Hacker News</a>. HN is a forum that is mostly focused on technology and entrepreneurship. Although I am not entirely sure if all participants are <a href="https://voussoir.net/writing/cyborgs_on_hn">human</a>, it is a knowledgebase of considerable quality and in my opinion worth preserving.</p>
<p><a href="https://github.com/voussoir/hnarchive">https://github.com/voussoir/hnarchive</a></p>
<p><a href="https://git.voussoir.net/voussoir/hnarchive">https://git.voussoir.net/voussoir/hnarchive</a></p>
</div>
<img class="cvitem_logo" src="./cv/hnarchive.png"/>
</div>
@ -493,8 +493,8 @@ body.start_eating_that_trashcan .cvitem_details
<div class="cvitem_details">
<p>I use the wonderful program Sigil to edit epub files. Sigil has a python plugin system for which I have written a few modules. But, since the plugins can only operate on one book at a time while it is open in Sigil, I needed something a little different to edit epub files en masse.</p>
<p>Epubfile is a simple library for automatically processing epubs. It comes with a number of builtin routines for what I do most often: merging multiple epubs into a single file, normalizing the internal file structure, and renaming the cover image file to leverage CBXShell so I get thumbnails in Windows Explorer.</p>
<p><a href="https://github.com/voussoir/epubfile">https://github.com/voussoir/epubfile</a></p>
<p><a href="https://github.com/voussoir/sigilplugins">https://github.com/voussoir/sigilplugins</a></p>
<p><a href="https://git.voussoir.net/voussoir/epubfile">https://git.voussoir.net/voussoir/epubfile</a></p>
<p><a href="https://git.voussoir.net/voussoir/sigilplugins">https://git.voussoir.net/voussoir/sigilplugins</a></p>
</div>
<img class="cvitem_logo" src="./cv/epub.png"/>
</div>
@ -551,6 +551,7 @@ body.start_eating_that_trashcan .cvitem_details
<h3 class="cvitem_title" id="gitmirrors">Git mirrors</h3>
<div class="cvitem_details">
<p>Eggs. Baskets.</p>
<p><a href="https://git.voussoir.net">https://git.voussoir.net</a></p>
<p><a href="https://github.com/voussoir">https://github.com/voussoir</a></p>
<p><a href="https://codeberg.org/voussoir">https://codeberg.org/voussoir</a></p>
<p><a href="https://gitlab.com/voussoir">https://gitlab.com/voussoir</a></p>
@ -882,6 +883,7 @@ const SPLASHES = [
"impulse 101", // https://developer.valvesoftware.com/wiki/Impulse
"in this moment, i am euphoric", // aalewis https://knowyourmeme.com/memes/in-this-moment-i-am-euphoric
"infinity is not necessarily all-encompassing",
"information wants to be free", // https://en.wikipedia.org/wiki/Information_wants_to_be_free
"inject the memes into my bloodstream", // Filthy Frank, Meme Machine https://youtu.be/wl-LeTFM8zo
"insane in the membrane", // Cypress Hill, Insane in the Brain https://youtu.be/RijB8wnJCN0
"is a little overbearing",
@ -1153,6 +1155,7 @@ const SPLASHES = [
"the rivers and the hills, the forests and the streams", // Walking in the Air https://youtu.be/X986dthrhaQ
"the sheriff is near", // Blazing Saddles (1974) https://youtu.be/sAELs42aZt4
"the squeaky wheel gets hammered down",
"the sun's as warm as a baked potato", // Cannibal! The Musical https://www.cannibalthemusical.net/songs.shtml
"the water is extracted for use in rivers", // How it's Unmade - Oreo Cookies https://youtu.be/cJyGoGPXTj4
"the waves part and they engulf me and the water is warm", // You could stop at five or six stores https://youtu.be/YCeQLeQiRP4
"the word itself makes some men uncomfortable", // The Big Lebowski (1998) https://youtu.be/xs3OWJ53rHE

View file

@ -1,330 +0,0 @@
:root
{
--color_bodybg: #272822;
--color_codebg: rgba(255, 255, 255, 0.05);
--color_codeborder: rgba(255, 255, 255, 0.2);
--color_h1bg: #284142;
--color_htmlbg: #1b1c18;
--color_blockquotebg: rgba(0, 0, 0, 0.2);
--color_blockquoteedge: rgba(255, 255, 255, 0.2);
--color_inlinecodebg: rgba(255, 255, 255, 0.1);
--color_link: #ae81ff;
--color_maintext: #ddd;
}
*, *:before, *:after
{
box-sizing: inherit;
}
html
{
height: 100vh;
box-sizing: border-box;
background-color: var(--color_htmlbg);
color: var(--color_maintext);
font-family: Verdana, sans-serif;
font-size: 10pt;
margin: 0;
}
body
{
min-height: 100%;
width: fit-content;
margin-left: auto;
margin-right: auto;
margin-top: 0;
margin-bottom: 0;
padding: 8px;
}
body.noscrollbar::-webkit-scrollbar
{
display: none;
}
body.noscrollbar
{
scrollbar-width: none;
}
header
{
width: 100%;
max-width: 120em;
margin-left: auto;
margin-right: auto;
text-align: end;
}
header > *
{
display: inline-block;
padding: 16px;
background-color: var(--color_bodybg);
}
.album,
.photograph
{
position: relative;
margin-left: auto;
margin-right: auto;
margin-top: 8vh;
margin-bottom: 8vh;
}
.photograph
{
padding: 2vh;
background-color: var(--color_bodybg);
border-radius: 16px;
}
article .photograph:first-of-type
{
margin-top: 0;
}
article .photograph:last-of-type
{
margin-bottom: 0;
}
.photograph img
{
max-height: 92vh;
border-radius: 8px;
}
article .morelink
{
font-size: 2em;
text-align: center;
margin-top: 0;
}
@media not print
{
.photograph
{
box-shadow: #000 0px 0px 40px -10px;
}
}
@media screen and (min-width: 600px)
{
article
{
width: fit-content;
}
}
@media screen and (max-width: 600px)
{
.photograph
{
box-shadow: none;
}
}
@media not all and (pointer: fine)
{
#keyboardhint,
#scrollbartoggle
{
display: none;
}
}
h1, h2, h3, h4, h5
{
margin-bottom: 0;
padding: 8px;
}
h1:first-child, h2:first-child, h3:first-child, h4:first-child, h5:first-child
{
margin-top: 0;
}
h2, h3, h4, h5
{
border-bottom: 1px solid var(--color_maintext);
/*background-color: var(--color_h1bg);*/
}
h1 + p
{
margin-top: 0;
}
p:last-child
{
margin-bottom: 0;
}
h1 {font-size: 2.00em;} h1 * {font-size: inherit;}
h2 {font-size: 1.75em;} h2 * {font-size: inherit;}
h3 {font-size: 1.50em;} h3 * {font-size: inherit;}
h4 {font-size: 1.25em;} h4 * {font-size: inherit;}
h5 {font-size: 1.00em;} h5 * {font-size: inherit;}
.header_anchor_link {display: none; font-size: 1.0em; text-decoration: none}
h1:hover > .header_anchor_link {display: initial;}
h2:hover > .header_anchor_link {display: initial;}
h3:hover > .header_anchor_link {display: initial;}
h4:hover > .header_anchor_link {display: initial;}
h5:hover > .header_anchor_link {display: initial;}
a
{
color: var(--color_link);
cursor: pointer;
}
article *
{
max-width: 100%;
word-wrap: break-word;
}
#table_of_contents
{
border: 1px solid var(--color_blockquoteedge);
padding-top: 8px;
padding-bottom: 8px;
border-radius: 8px;
}
blockquote
{
background-color: var(--color_blockquotebg);
margin-inline-start: 0;
margin-inline-end: 0;
border-left: 4px solid var(--color_blockquoteedge);
padding: 8px;
padding-inline-start: 20px;
padding-inline-end: 20px;
}
table
{
border-collapse: collapse;
font-size: 1em;
}
table, table th, table td
{
border: 1px solid var(--color_maintext);
}
table th, table td
{
padding: 4px;
}
ol ol, ul ul, ol ul, ul ol
{
padding-inline-start: 20px;
}
*:not(pre) > code
{
background-color: var(--color_inlinecodebg);
border-radius: 4px;
line-height: 1.5;
padding-left: 4px;
padding-right: 4px;
}
pre
{
padding: 8px;
border: 1px solid var(--color_codeborder);
background-color: var(--color_codebg);
overflow-x: auto;
}
code,
pre,
.highlight *
{
font-family: monospace;
}
/*
Thank you richleland for pre-building this Monokai style.
https://github.com/richleland/pygments-css
*/
:root
{
--color_monokai_bg: #272822;
--color_monokai_purple: #ae81ff;
--color_monokai_green: #a6e22e;
--color_monokai_pink: #f92672;
--color_monokai_white: #f8f8f2;
--color_monokai_orange: #fd971f;
--color_monokai_yellow: #e6db74;
--color_monokai_blue: #66d9ef;
}
.highlight .hll { background-color: #49483e }
.highlight { background-color: var(--color_monokai_bg); color: var(--color_monokai_white) }
.highlight .c { color: #75715e } /* Comment */
.highlight .err { color: #960050; background-color: #1e0010 } /* Error */
.highlight .k { color: var(--color_monokai_pink) } /* Keyword */
.highlight .l { color: var(--color_monokai_purple) } /* Literal */
.highlight .n { color: var(--color_monokai_white) } /* Name */
.highlight .o { color: var(--color_monokai_pink) } /* Operator */
.highlight .p { color: var(--color_monokai_white) } /* Punctuation */
.highlight .ch { color: #75715e } /* Comment.Hashbang */
.highlight .cm { color: #75715e } /* Comment.Multiline */
.highlight .cp { color: #75715e } /* Comment.Preproc */
.highlight .cpf { color: #75715e } /* Comment.PreprocFile */
.highlight .c1 { color: #75715e } /* Comment.Single */
.highlight .cs { color: #75715e } /* Comment.Special */
.highlight .gd { color: var(--color_monokai_pink) } /* Generic.Deleted */
.highlight .ge { font-style: italic } /* Generic.Emph */
.highlight .gi { color: var(--color_monokai_green) } /* Generic.Inserted */
.highlight .gs { font-weight: bold } /* Generic.Strong */
.highlight .gu { color: #75715e } /* Generic.Subheading */
.highlight .kc { color: var(--color_monokai_purple) } /* Keyword.Constant */
.highlight .kd { color: var(--color_monokai_blue) } /* Keyword.Declaration */
.highlight .kn { color: var(--color_monokai_pink) } /* Keyword.Namespace */
.highlight .kp { color: var(--color_monokai_blue) } /* Keyword.Pseudo */
.highlight .kr { color: var(--color_monokai_blue) } /* Keyword.Reserved */
.highlight .kt { color: var(--color_monokai_blue) } /* Keyword.Type */
.highlight .ld { color: var(--color_monokai_yellow) } /* Literal.Date */
.highlight .m { color: var(--color_monokai_purple) } /* Literal.Number */
.highlight .s { color: var(--color_monokai_yellow) } /* Literal.String */
.highlight .na { color: var(--color_monokai_white) } /* Name.Attribute */
.highlight .narg {color: var(--color_monokai_orange) } /* Custom Name.Argument */
.highlight .nb { color: var(--color_monokai_blue) } /* Name.Builtin */
.highlight .nc { color: var(--color_monokai_white) } /* Name.Class */
.highlight .no { color: var(--color_monokai_blue) } /* Name.Constant */
.highlight .nd { color: var(--color_monokai_green) } /* Name.Decorator */
.highlight .ni { color: var(--color_monokai_white) } /* Name.Entity */
.highlight .ne { color: var(--color_monokai_blue) } /* Name.Exception */
.highlight .nf { color: var(--color_monokai_green) } /* Name.Function */
.highlight .nl { color: var(--color_monokai_white) } /* Name.Label */
.highlight .nn { color: var(--color_monokai_white) } /* Name.Namespace */
.highlight .nx { color: var(--color_monokai_green) } /* Name.Other */
.highlight .py { color: var(--color_monokai_white) } /* Name.Property */
.highlight .nt { color: var(--color_monokai_pink) } /* Name.Tag */
.highlight .nv { color: var(--color_monokai_white) } /* Name.Variable */
.highlight .ow { color: var(--color_monokai_pink) } /* Operator.Word */
.highlight .w { color: var(--color_monokai_white) } /* Text.Whitespace */
.highlight .mb { color: var(--color_monokai_purple) } /* Literal.Number.Bin */
.highlight .mf { color: var(--color_monokai_purple) } /* Literal.Number.Float */
.highlight .mh { color: var(--color_monokai_purple) } /* Literal.Number.Hex */
.highlight .mi { color: var(--color_monokai_purple) } /* Literal.Number.Integer */
.highlight .mo { color: var(--color_monokai_purple) } /* Literal.Number.Oct */
.highlight .sa { color: var(--color_monokai_white) } /* Literal.String.Affix */
.highlight .sb { color: var(--color_monokai_yellow) } /* Literal.String.Backtick */
.highlight .sc { color: var(--color_monokai_yellow) } /* Literal.String.Char */
.highlight .dl { color: var(--color_monokai_yellow) } /* Literal.String.Delimiter */
.highlight .sd { color: var(--color_monokai_yellow) } /* Literal.String.Doc */
.highlight .s2 { color: var(--color_monokai_yellow) } /* Literal.String.Double */
.highlight .se { color: var(--color_monokai_purple) } /* Literal.String.Escape */
.highlight .sh { color: var(--color_monokai_yellow) } /* Literal.String.Heredoc */
.highlight .si { color: var(--color_monokai_yellow) } /* Literal.String.Interpol */
.highlight .sx { color: var(--color_monokai_yellow) } /* Literal.String.Other */
.highlight .sr { color: var(--color_monokai_yellow) } /* Literal.String.Regex */
.highlight .s1 { color: var(--color_monokai_yellow) } /* Literal.String.Single */
.highlight .ss { color: var(--color_monokai_yellow) } /* Literal.String.Symbol */
.highlight .bp { color: var(--color_monokai_white) } /* Name.Builtin.Pseudo */
.highlight .fm { color: var(--color_monokai_blue) } /* Name.Function.Magic */
.highlight .vc { color: var(--color_monokai_white) } /* Name.Variable.Class */
.highlight .vg { color: var(--color_monokai_white) } /* Name.Variable.Global */
.highlight .vi { color: var(--color_monokai_white) } /* Name.Variable.Instance */
.highlight .vm { color: var(--color_monokai_white) } /* Name.Variable.Magic */
.highlight .il { color: var(--color_monokai_purple) } /* Literal.Number.Integer.Long */

View file

@ -1,18 +1,43 @@
import boto3
import io
import jinja2
import PIL.Image
import sys
import urllib.parse
import etiquette
import r2_credentials
from voussoirkit import dotdict
from voussoirkit import imagetools
from voussoirkit import pathclass
from voussoirkit import spinal
from voussoirkit import vlogging
log = vlogging.get_logger(__name__, 'photography_generate')
pdb = etiquette.photodb.PhotoDB('D:\\Documents\\Photos\\_etiquette')
s3 = boto3.resource('s3',
endpoint_url = f'https://{r2_credentials.get_account_id()}.r2.cloudflarestorage.com',
aws_access_key_id = r2_credentials.get_access_key(),
aws_secret_access_key = r2_credentials.get_access_secret(),
)
bucket = s3.Bucket('voussoir')
S3_EXISTING_FILES = set(item.key for item in bucket.objects.filter(Prefix="photography/"))
PUBLISH_TAGNAME = 'voussoir_net_publish'
HEADLINER_TAGNAME = 'voussoir_net_headliner'
PHOTOGRAPHY_ROOTDIR = pathclass.Path(__file__).parent
ATOM_FILE = PHOTOGRAPHY_ROOTDIR.with_child('photography.atom')
DOMAIN_ROOTDIR = PHOTOGRAPHY_ROOTDIR.parent
CSS_CONTENT = PHOTOGRAPHY_ROOTDIR.with_child('dark.css').read('r', encoding='utf-8')
DOMAIN_WEBROOT = ('file:///' + DOMAIN_ROOTDIR.absolute_path) if '--test' in sys.argv else 'https://voussoir.net'
DOMAIN_WEBROOT = DOMAIN_WEBROOT.replace('\\', '/')
DOMAIN_WEBROOT = 'https://voussoir.net'
PHOTOGRAPHY_WEBROOT = 'https://voussoir.net/photography'
S3_WEBROOT = 'https://files.voussoir.net'
SIZE_SMALL = 1440
SIZE_TINY = 360
def webpath(path, anchor=None):
path = path.relative_to(DOMAIN_ROOTDIR, simple=True).replace('\\', '/').lstrip('/')
@ -22,92 +47,129 @@ def webpath(path, anchor=None):
return path
class Photo:
def __init__(self, filepath):
self.filepath = filepath
self.thumbnail = make_thumbnail(filepath)
self.article_id = filepath.replace_extension('').basename
self.anchor = f'#{self.article_id}'
self.published = imagetools.get_exif_datetime(filepath)
def __init__(self, etq_photo, etq_album=None):
self.etq_photo = etq_photo
self.article_id = self.etq_photo.real_path.replace_extension('').basename
if etq_album is None:
parent_key = 'photography'
else:
parent_key = f'photography/{etq_album.title}'
self.s3_key = f'{parent_key}/{self.etq_photo.real_path.basename}'
self.small_key = f'{parent_key}/small_{self.etq_photo.real_path.basename}'
self.tiny_key = f'{parent_key}/tiny_{self.etq_photo.real_path.basename}'
self.color_class = 'monochrome' if self.etq_photo.has_tag('monochrome') else ''
self.s3_exists = self.s3_key in S3_EXISTING_FILES;
self.img_url = f'{S3_WEBROOT}/{self.s3_key}'
self.small_url = f'{S3_WEBROOT}/{self.small_key}'
self.tiny_url = f'{S3_WEBROOT}/{self.tiny_key}'
self.anchor_url = f'{DOMAIN_WEBROOT}/{parent_key}#{self.article_id}'
self.published = imagetools.get_exif_datetime(self.etq_photo.real_path)
def prepare(self):
if not self.s3_exists:
self.s3_upload()
def make_thumbnail(self, size):
image = PIL.Image.open(self.etq_photo.real_path.absolute_path)
icc = image.info.get('icc_profile')
(image_width, image_height) = image.size
exif = image.getexif()
(width, height) = imagetools.fit_into_bounds(image_width, image_height, size, size)
image = image.resize((width, height), PIL.Image.LANCZOS)
bio = io.BytesIO()
image.save(bio, format='jpeg', quality=75, exif=exif, icc_profile=icc)
bio.seek(0)
return bio
def s3_upload(self):
log.info('Uploading %s as %s', self.etq_photo.real_path.absolute_path, self.s3_key)
bucket.upload_fileobj(self.make_thumbnail(SIZE_SMALL), self.small_key)
bucket.upload_fileobj(self.make_thumbnail(SIZE_TINY), self.tiny_key)
bucket.upload_fileobj(self.etq_photo.real_path.open('rb'), self.s3_key)
self.s3_exists = True
def render_web(self, index=None, totalcount=None):
if totalcount is not None:
number_tag = f'<span class="number_tag">#{index}/{totalcount}</a>'
else:
number_tag = ''
def render_web(self, relative_directory=None):
return f'''
<article id="{self.article_id}" class="photograph">
<a href="{webpath(self.filepath)}" target="_blank"><img src="{webpath(self.thumbnail)}" loading="lazy"/></a>
<article id="{self.article_id}" class="photograph {self.color_class}">
<a href="{self.img_url}" target="_blank"><img src="{self.small_url}" loading="lazy"/></a>
{number_tag}
</article>
'''
def render_atom(self):
href = webpath(PHOTOGRAPHY_ROOTDIR, anchor=self.anchor)
imgsrc = webpath(self.thumbnail)
return f'''
<id>{self.article_id}</id>
<title>{self.article_id}</title>
<link rel="alternate" type="text/html" href="{href}"/>
<link rel="alternate" type="text/html" href="{self.anchor_url}"/>
<updated>{self.published.isoformat()}</updated>
<content type="html">
<![CDATA[
<a href="{href}"><img src="{imgsrc}"/></a>
<a href="{self.img_url}"><img src="{self.small_url}"/></a>
]]>
</content>
'''
class Album:
def __init__(self, path):
self.path = path
self.article_id = path.basename
self.link = webpath(path)
self.published = imagetools.get_exif_datetime(sorted(path.glob_files('*.jpg'))[0])
self.photos = list(spinal.walk(
self.path,
glob_filenames={'*.jpg'},
exclude_filenames={'*_small*'},
recurse=False,
yield_directories=False,
))
self.photos.sort(key=lambda file: file.basename)
self.photos = [Photo(file) for file in self.photos]
def __init__(self, etq_album):
self.etq_album = etq_album
self.article_id = self.etq_album.title
self.photos = list(self.etq_album.get_photos())
self.photos = [p for p in self.photos if p.has_tag(PUBLISH_TAGNAME)]
self.photos = [Photo(etq_photo=photo, etq_album=self.etq_album) for photo in self.photos]
self.photos.sort(key=lambda p: p.published)
# self.link = webpath(path)
self.web_url = f'{PHOTOGRAPHY_WEBROOT}/{self.article_id}'
self.published = self.photos[0].published
def render_web(self):
firsts = self.photos[:5]
remaining = self.photos[5:]
if remaining:
next_after_more = remaining[0]
else:
next_after_more = None
def prepare(self):
for photo in self.photos:
photo.prepare()
def render_web(self, index=None, totalcount=None):
headliners = [p for p in self.photos if p.etq_photo.has_tag(HEADLINER_TAGNAME)]
return jinja2.Template('''
<article id="{{article_id}}" class="album">
<h1><a href="{{album_path}}">{{directory.basename}}</a></h1>
{% for photo in firsts %}
<h1><a href="{{web_url}}">{{article_id}}</a></h1>
<div class="albumphotos">
{% for photo in headliners %}
{{photo.render_web()}}
{% endfor %}
{% if remaining > 0 %}
<p class="morelink"><a href="{{album_path}}{{next_after_more.anchor}}">{{remaining}} more</a></p>
{% endif %}
<div class="album_tinies">
{% for photo in photos %}
<a class="tiny_thumbnail {{photo.color_class}}" href="{{photo.anchor_url}}"><img src="{{photo.tiny_url}}" loading="lazy"/></a>
{% endfor %}
</div>
</div>
</article>
''').render(
article_id=self.article_id,
directory=self.path,
album_path=webpath(self.path),
next_after_more=next_after_more,
firsts=firsts,
remaining=len(remaining),
web_url=self.web_url,
photos=self.photos,
headliners=headliners,
)
def render_atom(self):
photos = []
for photo in self.photos:
href = webpath(photo.filepath)
imgsrc = webpath(photo.thumbnail)
line = f'<article><a href="{href}"><img src="{imgsrc}"/></a>'.replace('\\', '/')
line = f'<article><a href="{photo.anchor_url}"><img src="{photo.small_url}" loading="lazy"/></a>'.replace('\\', '/')
photos.append(line)
photos = '\n'.join(photos)
return f'''
<id>{self.article_id}</id>
<title>{self.article_id}</title>
<link rel="alternate" type="text/html" href="{self.link}"/>
<link rel="alternate" type="text/html" href="{self.web_url}"/>
<updated>{self.published.isoformat()}</updated>
<content type="html">
<![CDATA[
@ -126,30 +188,12 @@ def write(path, content):
print(path.absolute_path)
path.write('w', content, encoding='utf-8')
def write_directory_index(directory):
rss_link = webpath(ATOM_FILE) if directory == PHOTOGRAPHY_ROOTDIR else None
back_link = webpath(PHOTOGRAPHY_ROOTDIR) if directory != PHOTOGRAPHY_ROOTDIR else None
sort_reverse = directory == PHOTOGRAPHY_ROOTDIR
def make_webpage(items, is_root, doctitle):
rss_link = f'{PHOTOGRAPHY_WEBROOT}/{ATOM_FILE.basename}' if is_root else None
back_link = None if is_root else PHOTOGRAPHY_WEBROOT
sort_reverse = is_root
items = list(spinal.walk(
directory,
glob_filenames={'*.jpg'},
exclude_filenames={'*_small*'},
recurse=False,
yield_directories=False,
)) + list(directory.listdir_directories())
items2 = []
for item in items:
if item.is_file:
items2.append(Photo(item))
else:
items2.append(Album(item))
items = items2
items.sort(key=lambda item: item.published, reverse=sort_reverse)
page = jinja2.Template('''
html = jinja2.Template('''
<!DOCTYPE html>
<html>
<head>
@ -158,10 +202,229 @@ def write_directory_index(directory):
{% if rss_link %}
<link rel="alternate" type="application/atom+xml" href="{{rss_link}}"/>
{% endif %}
<title>{{directory.basename}}</title>
<title>{{doctitle}}</title>
<style>
{{css_content}}
:root
{
--color_bodybg: #272822;
--color_htmlbg: #1b1c18;
--color_link: #ae81ff;
--color_maintext: #ddd;
--img_borderradius: 16px;
--img_borderradius_tiny: 4px;
--img_sepia: 0%;
--img_huerotate: 0deg;
--img_saturate: 100%;
--img_blur: 0px;
--img_mixblendmode: normal;
}
*, *:before, *:after
{
box-sizing: inherit;
margin: 0;
}
.hidden
{
display: none !important;
}
html
{
height: 100vh;
box-sizing: border-box;
background-color: var(--color_htmlbg);
color: var(--color_maintext);
font-family: Verdana, sans-serif;
font-size: 10pt;
}
body
{
width: 100em;
max-width: 100%;
margin-left: auto;
margin-right: auto;
margin-top: 0;
margin-bottom: 0;
padding: 8px;
padding-bottom:8vh;
display: grid;
grid-auto-flow: row;
grid-row-gap: 12vh;
}
body.noscrollbar::-webkit-scrollbar
{
display: none;
}
body.noscrollbar
{
scrollbar-width: none;
}
a
{
color: var(--color_link);
cursor: pointer;
}
h1
{
text-align: center;
}
article *
{
max-width: 100%;
word-wrap: break-word;
}
header
{
width: 100%;
max-width: 120em;
margin-left: auto;
margin-right: auto;
text-align: end;
}
header > *
{
display: inline-block;
padding: 16px;
background-color: var(--color_bodybg);
}
.album,
.photograph,
.album_tinies
{
position: relative;
margin-left: auto;
margin-right: auto;
}
.album
{
width: 100%;
}
.album .albumphotos
{
width: 100%;
display: grid;
grid-auto-flow: row;
grid-row-gap: 12vh;
}
.photograph
{
width: fit-content;
}
.photograph img
{
display: block;
max-height: 92vh;
border-radius: var(--img_borderradius);
border: 1.25vh solid var(--color_bodybg);
filter: hue-rotate(var(--img_huerotate)) saturate(var(--img_saturate)) blur(var(--img_blur));
mix-blend-mode: var(--img_mixblendmode);
}
.photograph.monochrome img,
.tiny_thumbnail.monochrome img
{
filter: sepia(var(--img_sepia)) hue-rotate(var(--img_huerotate)) saturate(var(--img_saturate)) blur(var(--img_blur));
}
.photograph .number_tag
{
position: absolute;
bottom: 8px;
right: 8px;
text-align: center;
background-color: white;
padding: 1px;
color: black;
text-decoration: none;
font-family: sans-serif;
border-radius: 4px;
font-weight: bold;
opacity: 50%;
}
.album .album_tinies
{
max-width: 80em;
text-align: justify;
}
.tiny_thumbnail
{
vertical-align: middle;
display:inline-block;
margin: 8px;
}
.tiny_thumbnail img
{
aspect-ratio: auto;
border-radius: var(--img_borderradius_tiny);
outline: 4px solid var(--color_bodybg);
filter: hue-rotate(var(--img_huerotate)) saturate(var(--img_saturate)) blur(var(--img_blur));
mix-blend-mode: var(--img_mixblendmode);
}
@media not print
{
.photograph img
{
box-shadow: #000 0px 0px 40px -10px;
}
}
@media screen and (min-width: 600px)
{
.tiny_thumbnail img
{
height: 128px;
}
}
@media screen and (max-width: 600px)
{
.tiny_thumbnail img
{
height: 64px;
}
.photograph
{
box-shadow: none;
}
}
@media not all and (pointer: fine)
{
#keyboardhint,
#scrollbartoggle
{
display: none;
}
}
h1 {font-size: 2.00em;} h1 * {font-size: inherit;}
h2 {font-size: 1.75em;} h2 * {font-size: inherit;}
h3 {font-size: 1.50em;} h3 * {font-size: inherit;}
h4 {font-size: 1.25em;} h4 * {font-size: inherit;}
h5 {font-size: 1.00em;} h5 * {font-size: inherit;}
#a_new_perspective
{
background-color: var(--color_bodybg);
position: fixed;
left: 8px;
padding: 8px;
border-radius: 8px;
}
</style>
</head>
@ -178,9 +441,47 @@ def write_directory_index(directory):
{%- endif -%}
</header>
{% if not is_root %}
<h1>{{doctitle}}</h1>
{% endif %}
{% for item in items %}
{{item.render_web()}}
{{item.render_web(index=loop.index, totalcount=none if is_root else (items|length))}}
{% endfor %}
<footer>
<p>Ethan Dalool</p>
<p>Contact me: photography@voussoir.net</p>
</footer>
<p><button id="new_perspective_button" onclick="return new_perspective_button_onclick(event);">👁 Try a different perspective</button></p>
<form id="a_new_perspective" class="hidden">
<div><label>Background color: <input type="color" value="#1b1c18" oninput="return backgroundcolor_onchange(event);"/></label></div>
<div><label>Border radius: <input type="range" min="0" value="16" max="500" oninput="return border_radius_onchange(event);"/></label> <span id="border_radius_value">16px</span></div>
<div><label>Saturation: <input type="range" min="0" value="100" max="500" oninput="return saturate_onchange(event);"/></label> <span id="saturate_value">100%</span></div>
<div><label>Hue rotate: <input type="range" min="0" value="0" max="360" oninput="return hue_rotate_onchange(event);"/></label> <span id="hue_rotate_value">0deg</span></div>
<div><label>Blur: <input type="range" min="0" value="0" max="50" oninput="return blur_onchange(event);"/></label> <span id="blur_value">0%</span></div>
<div><label>Mix blend mode: <select onchange="return mixblendmode_onchange(event);">
<option selected>normal</option>
<option>multiply</option>
<option>screen</option>
<option>overlay</option>
<option>darken</option>
<option>lighten</option>
<option>color-dodge</option>
<option>color-burn</option>
<option>hard-light</option>
<option>soft-light</option>
<option>difference</option>
<option>exclusion</option>
<option>hue</option>
<option>saturation</option>
<option>color</option>
<option>luminosity</option>
</select></label></div>
</form>
</body>
<script type="text/javascript">
@ -212,70 +513,93 @@ def write_directory_index(directory):
}
}
function get_center_img()
const SCROLL_STOPS = Array.from(document.querySelectorAll("article.photograph img, .album_tinies"));
function get_center_stop()
{
let center_x = window.innerWidth / 2;
let center_y = window.innerHeight / 2;
while (true)
let center_y = window.pageYOffset + (window.innerHeight / 2);
let final;
for (const stop of SCROLL_STOPS)
{
const element = document.elementFromPoint(center_x, center_y);
if (element.tagName === "IMG")
const stop_top = stop.getBoundingClientRect().top + window.pageYOffset;
const stop_bottom = stop_top + stop.offsetHeight;
console.log("-----");
console.log(stop);
console.log(`${center_y} versus ${stop_top}--${stop_bottom}`);
console.log("-----");
if (center_y > stop_top)
{
return element;
final = stop;
}
center_y -= 20;
if (center_y <= 0)
else
{
return null;
break;
}
}
if (final)
{
return final;
}
}
function next_img(img)
{
const images = Array.from(document.images);
const this_index = images.indexOf(img);
if (this_index === images.length-1)
const this_index = SCROLL_STOPS.indexOf(img);
if (this_index === SCROLL_STOPS.length-1)
{
return img;
}
return images[this_index + 1];
return SCROLL_STOPS[this_index + 1];
}
function previous_img(img)
{
const images = Array.from(document.images);
const this_index = images.indexOf(img);
const this_index = SCROLL_STOPS.indexOf(img);
if (this_index === 0)
{
return img;
}
return images[this_index - 1];
return SCROLL_STOPS[this_index - 1];
}
function scroll_step()
{
const distance = desired_scroll_position - document.body.scrollTop;
const jump = (distance * 0.25) + (document.body.scrollTop < desired_scroll_position ? 1 : -1);
document.body.scrollTop = document.body.scrollTop + jump;
console.log(`${document.body.scrollTop} ${desired_scroll_position}`);
const new_distance = desired_scroll_position - document.body.scrollTop;
const distance = desired_scroll_position - document.documentElement.scrollTop;
const jump = (distance * 0.25) + (document.documentElement.scrollTop < desired_scroll_position ? 1 : -1);
document.documentElement.scrollTop = document.documentElement.scrollTop + jump;
//console.log(`${document.documentElement.scrollTop} ${desired_scroll_position}`);
const new_distance = desired_scroll_position - document.documentElement.scrollTop;
if (Math.abs(new_distance / distance) < 0.97)
{
window.requestAnimationFrame(scroll_step);
}
}
function scroll_to_img(img)
function scroll_to_stop(stop)
{
const img_centerline = img.getBoundingClientRect().top + img.ownerDocument.defaultView.pageYOffset + (img.offsetHeight / 2);
// document.body.scrollTop = img_centerline - (window.innerHeight / 2);
if (stop.offsetHeight > window.innerHeight)
{
desired_scroll_position = stop.getBoundingClientRect().top + window.pageYOffset;
}
else
{
const img_centerline = stop.getBoundingClientRect().top + window.pageYOffset + (stop.offsetHeight / 2);
// document.documentElement.scrollTop = img_centerline - (window.innerHeight / 2);
desired_scroll_position = Math.round(img_centerline - (window.innerHeight / 2));
}
scroll_step();
}
function scroll_to_next_img()
{
scroll_to_img(next_img(get_center_img()));
const current_stop = get_center_stop();
if (current_stop)
{
scroll_to_stop(next_img(current_stop));
}
}
function scroll_to_previous_img()
{
scroll_to_img(previous_img(get_center_img()));
const current_stop = get_center_stop();
if (current_stop)
{
scroll_to_stop(previous_img(current_stop));
}
}
function arrowkey_listener(event)
{
@ -304,6 +628,78 @@ def write_directory_index(directory):
clearTimeout(hide_cursor_timeout);
hide_cursor_timeout = setTimeout(hide_cursor, 3000);
}
function new_perspective_button_onclick(event)
{
const p = event.target.parentElement;
document.getElementById("a_new_perspective").classList.remove("hidden");
p.parentElement.removeChild(p);
}
function backgroundcolor_onchange(event)
{
document.documentElement.style.setProperty("--color_htmlbg", event.target.value);
}
function border_radius_onchange(event)
{
if (event.target.value == "500")
{
const value = "100%";
document.documentElement.style.setProperty("--img_borderradius", value);
document.documentElement.style.setProperty("--img_borderradius_tiny", value);
document.getElementById("border_radius_value").textContent = value;
}
else
{
const value = event.target.value + "px";
document.documentElement.style.setProperty("--img_borderradius", value);
document.documentElement.style.setProperty("--img_borderradius_tiny", (event.target.value / 4) + "px");
document.getElementById("border_radius_value").textContent = value;
}
}
function saturate_onchange(event)
{
const value = event.target.value + "%";
document.documentElement.style.setProperty("--img_saturate", value);
if (event.target.value == "0")
{
document.getElementById("saturate_value").textContent = "Artistic";
}
else
{
document.getElementById("saturate_value").textContent = value;
}
}
function hue_rotate_onchange(event)
{
const value = event.target.value + "deg";
if (event.target.value === "0")
{
document.documentElement.style.setProperty("--img_sepia", "0");
}
else
{
document.documentElement.style.setProperty("--img_sepia", "100%");
}
document.documentElement.style.setProperty("--img_huerotate", value);
document.getElementById("hue_rotate_value").textContent = value;
}
function blur_onchange(event)
{
const value = event.target.value + "px";
document.documentElement.style.setProperty("--img_blur", value);
document.getElementById("blur_value").textContent = value;
}
function mixblendmode_onchange(event)
{
document.documentElement.style.setProperty("--img_mixblendmode", event.target.value);
}
function on_pageload()
{
document.documentElement.addEventListener("keydown", arrowkey_listener);
@ -315,23 +711,20 @@ def write_directory_index(directory):
</script>
</html>
''').render(
css_content=CSS_CONTENT,
directory=directory,
is_root=is_root,
doctitle=doctitle,
rss_link=rss_link,
back_link=back_link,
items=items,
)
write(directory.with_child('index.html'), page)
if rss_link:
write_atom(items)
return html
def write_atom(items):
atom = jinja2.Template('''
<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
<title>voussoir.net/photography</title>
<link href="webpath(PHOTOGRAPHY_ROOTDIR)}"/>
<link href="https://voussoir.net/photography"/>
<id>voussoir.net/photography</id>
{% for item in items %}
@ -343,21 +736,41 @@ def write_atom(items):
'''.strip()).render(items=items)
write(ATOM_FILE, atom)
def make_thumbnail(photo):
small_name = photo.replace_extension('').basename + '_small'
small_name = photo.parent.with_child(small_name).add_extension(photo.extension)
if small_name.is_file:
return small_name
image = PIL.Image.open(photo.absolute_path)
icc = image.info.get('icc_profile')
(image_width, image_height) = image.size
exif = image.getexif()
(width, height) = imagetools.fit_into_bounds(image_width, image_height, 1440, 1440)
image = image.resize((width, height), PIL.Image.LANCZOS)
image.save(small_name.absolute_path, quality=75, exif=exif, icc_profile=icc)
print(small_name)
return small_name
# write_directory_index(PHOTOGRAPHY_ROOTDIR)
# for directory in PHOTOGRAPHY_ROOTDIR.walk_directories():
# write_directory_index(directory)
write_directory_index(PHOTOGRAPHY_ROOTDIR)
for directory in PHOTOGRAPHY_ROOTDIR.walk_directories():
write_directory_index(directory)
@vlogging.main_decorator
def main(argv):
singlephotos = list(pdb.search(tag_mays=[PUBLISH_TAGNAME], has_albums=False, yield_albums=False, yield_photos=True).results)
singlephotos += list(pdb.search(tag_mays=['voussoir_net_publish_single'], yield_albums=False, yield_photos=True).results)
singlephotos = [Photo(p) for p in singlephotos]
singlephotos.sort(key=lambda i: i.published, reverse=True)
albums = list(pdb.search(tag_musts=[PUBLISH_TAGNAME], tag_forbids=['voussoir_net_publish_single'], has_albums=True, yield_albums=True, yield_photos=False).results)
albums = [Album(a) for a in albums]
albums.sort(key=lambda i: i.published, reverse=True)
items = singlephotos + albums
items.sort(key=lambda i: i.published, reverse=True)
for item in items:
item.prepare()
log.info('Writing homepage')
homepage_html = make_webpage(items, is_root=True, doctitle='photography')
homepage_file = PHOTOGRAPHY_ROOTDIR.with_child('photography.html')
homepage_file.write('w', homepage_html)
for album in albums:
album_html = make_webpage(album.photos, is_root=False, doctitle=album.article_id)
album_file = PHOTOGRAPHY_ROOTDIR.with_child(album.article_id).replace_extension('html')
log.info('Writing %s', album_file.absolute_path)
album_file.write('w', album_html)
write_atom(items)
return 0
if __name__ == '__main__':
raise SystemExit(main(sys.argv[1:]))

View file

@ -203,8 +203,79 @@ pre
-->
<article>
<p><b>Reply-To</b>: grant@squadhelp.co</p>
<p><b>From</b>: Fiona Hill &lt;bettyraymond201@gmail.com&gt;</p>
<p><b>Reply-To</b>: fionahill142@gmail.com</p>
<p><b>To</b>: fionahill142@gmail.com</p>
<p><b>Bcc</b>: writing@voussoir.net</p>
<p><b>Subject</b>: </p>
<p><b>Date</b>: Fri, 8 Sep 2023 15:29:07 +0000</p>
<details>
<summary>Headers</summary>
<pre>
X-Spam-Status: No, score=-2.44 required=5 tests=DKIM_SIGNED,DKIM_VALID,DKIM_VALID_AU,FREEMAIL_FROM,FREEMAIL_REPLYTO,FREEMAIL_REPLYTO_END_DIGIT,RCVD_IN_MSPIKE_H2,SPF_HELO_NONE,PURELYMAIL_IP_REPUTATION autolearn=ham autolearn_force=no version=4.0.0
X-Pm-Spam-PURELYMAIL-IP-REPUTATION: -2.64;EffectiveIp=209.85.219.41,SpamProbability=0.02
Return-Path: &lt;bettyraymond201@gmail.com&gt;
Delivered-To: ethan@voussoir.net
X-Pm-Known-Alias: writing@voussoir.net
X-Pm-Original-To: writing@voussoir.net
Authentication-Results: purelymail.com; spf=pass (domain of _spf.google.com designates 209.85.219.41 as permitted sender) smtp.mailfrom=gmail.com; dkim=pass header.i=gmail.com; dmarc=pass (p=none) header.from=bettyraymond201@gmail.com
Received: from mail-qv1-f41.google.com (EHLO mail-qv1-f41.google.com) ([209.85.219.41])
by smtp.purelymail.com (Purelymail SMTP) with ESMTPS id -114360871
for &lt;writing@voussoir.net&gt;
(version=TLSv1.3 cipher=TLS_AES_256_GCM_SHA384);
Fri, 08 Sep 2023 15:29:08 +0000 (UTC)
Received: by mail-qv1-f41.google.com with SMTP id 6a1803df08f44-64f3ad95ec0so13119666d6.1
for &lt;writing@voussoir.net&gt;; Fri, 08 Sep 2023 08:29:08 -0700 (PDT)
DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed;
d=gmail.com; s=20221208; t=1694186948; x=1694791748; darn=voussoir.net;
h=to:subject:message-id:date:from:reply-to:mime-version:from:to:cc
:subject:date:message-id:reply-to;
bh=Ba3gj8+xBPQLJTahTfzW6RbWQ/XPgESxkCi2B66PSQg=;
b=YasTMC07fWjdPrJEgfH9/EbuJwftHxPKdoeKPhwuG0DlU3lg1Zo/j8NtchSnEt1Gdp
5OqyuPcnmVW3BZodZUZsR6R1lmlmAnbt/2lsDUrHGednKwfjwmxjekCWURhU4cxPatDl
DkQxA/su7gAHB2Gzb+E1qiWwqOmm7Lc/ettNaamilMQUHQ0MKdgCMQd9kDy49km6mY/1
R6Nvap5kc1rY46b02VxitvacfSiRqR3BSyWqJSSUvtjXc1o/WcfSA50YXW2xOjlQgEG9
t/ckwsDY7uHYmdRHUaZGokiEvdD5O0f53Rp6byCxJublcIa180ShWngjPZWwFslltr0I
ex1g==
X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed;
d=1e100.net; s=20230601; t=1694186948; x=1694791748;
h=to:subject:message-id:date:from:reply-to:mime-version
:x-gm-message-state:from:to:cc:subject:date:message-id:reply-to;
bh=Ba3gj8+xBPQLJTahTfzW6RbWQ/XPgESxkCi2B66PSQg=;
b=CBxDaKVaxeHutPqbgxvlqmwwoZ0jsMgDFsBmr8zgYCF+wiA1IbaioYudVPkENjzNNo
0t2H0B9rRo2KmaBzwxDX2pt6dvtpCJ80M1lN3eFlpSmbZqigglv7AfyVEsFtfaLwAPxv
MurPpOErCWMoWYNhI5TAYUhaWs95WLQaEn125TJFWmufhEnVoEd2U3RMnACcn+B7dNsT
FUuise8xuOJSJhA+MYxrQnchDwDkCIkruhkErMt9/IWjnZ6JIeKTdtVT/2NkGOEo50o5
CBIp2fbgi8BWchGuq9rxUxtXteVuJhpsOgXtLJu9UfRj2RGgNhtl+9IDHNSF1hfTysqo
Oh/w==
X-Gm-Message-State: AOJu0YyInMyJvkYc6g+p66NCNSu/cKD11y6pidrfNcAMnIBlTT50oy0v
Ud1GUYOI+8wMUMpHNZ/7nzUAgs2vZhLU9MiwodY=
X-Google-Smtp-Source: AGHT+IEZJmW6LWDaz7ky7Vtqc9VL15HnDlWpbje8Ob/RkQ+ioqTvE3WaJRy4/QkqQfqQjVn/BbhhjU/DI2QearkIFHA=
X-Received: by 2002:a0c:d610:0:b0:647:1a66:181a with SMTP id
c16-20020a0cd610000000b006471a66181amr2420913qvj.50.1694186947840; Fri, 08
Sep 2023 08:29:07 -0700 (PDT)
MIME-Version: 1.0
Received: by 2002:a0c:ab5e:0:b0:64d:3e37:9bff with HTTP; Fri, 8 Sep 2023
08:29:07 -0700 (PDT)
Reply-To: fionahill142@gmail.com
From: Fiona Hill &lt;bettyraymond201@gmail.com&gt;
Date: Fri, 8 Sep 2023 15:29:07 +0000
Message-ID: &lt;CALZothvq=+PPgJAZxrSNGZa1hx4DcnMTYqxg4ZiEwuntu1TKLA@mail.gmail.com&gt;
Subject:
To: fionahill142@gmail.com
Content-Type: text/plain; charset=&quot;UTF-8&quot;
Bcc: writing@voussoir.net
</pre>
</details>
<hr/>
<p>Hello</p>
</article>
<article>
<p><b>From</b>: Grant Polachek &lt;pola-grant@squadhelp.co&gt;</p>
<p><b>Reply-To</b>: grant@squadhelp.co</p>
<p><b>To</b>: contact@voussoir.net</p>
<p><b>Subject</b>: Re: Want an exclusive guest article for voussoir.net?</p>
<p><b>Date</b>: Tue, 05 Sep 2023 11:59:33 +0000</p>

View file

@ -525,7 +525,7 @@ Spam is built upon the abuse of systems by definition. Unlike sponsors, who make
2. are highly trafficked, because spammers need to hit as many people as possible before getting caught.
3. provide otherwise legitimate value to your life, so that you will not simply leave the space after the spammers invade.
Your email inbox and your telephone are spaces for communicating with people you know, and spammers abuse it to send you junk. Web forums are spaces for conversation, and spammers abuse them by making fake posts. Grocery stores are spaces for buying food, and spammers abuse the traffic flow to hand out fliers. [Parking lots](/writing/not_just_bikes) are part of our transit system, and spammers leave advertisements under your wipers because you're not there to stop them. The front door of your house is for yourself and your guests, and spammers abuse it by turning it into ad space.
Your [email inbox](/spam) and your telephone are spaces for communicating with people you know, and spammers abuse it to send you junk. Web forums are spaces for conversation, and spammers abuse them by making fake posts. Grocery stores are spaces for buying food, and spammers abuse the traffic flow to hand out fliers. [Parking lots](/writing/not_just_bikes) are part of our transit system, and spammers leave advertisements under your wipers because you're not there to stop them. The front door of your house is for yourself and your guests, and spammers abuse it by turning it into ad space.
The problem with spam is not just that it's annoying, but that it converts your entire life into an arms race. Spam is hostile. It is mean. Spam takes every aspect of your life and turns it into an opportunity to make a buck off of you. No matter how hard you work to avoid advertisements and carefully pick the publishers you want to pay for, spammers can override your decisions and advertise to you anyway. I am not kidding when I compare spam to terrorism, though I am exaggerating. Spam positions you against enemies you didn't know you had. Enemies who come from afar to your place of residence to disrespect your time, attention, and belongings, and sneak away. Unlike a TV which can be turned off or a magazine which can be closed, spam offers no means to opt-out. Spammers will hit you and run without showing their face. What are you going to do about it, not have an email address? Not have a phone number?

View file

@ -3,6 +3,20 @@ Cyborgs on HN
This page collects comments which make unnecessary or tenuous analogies to computers, programming, dollar-sign $variables, sed's/replace/syntax/g, mathematics, AI/machine learning, and cryptography in discussions that aren't about those things.
> If you search "watch $MOVIE free" on google you're going to get netflix, Hulu, prime, Disney etc as the first results regardless of whether those sites even have it in their library.
-
> I hope these journalists are ashamed of themselves and I hope they are at least sacked, with their careers relegated to selling poutine all day.
> > s/selling/eating/ ?
-
> There is always plenty wrong with each new release but the comments and jokes about ${"LATEST_RELEASE"} of ${"SOFTWARE"} being unilaterally bad just make me think the person can't deal with change instead of the software actually being bad.
-
> I'm scratching my head here. The old gas/diesel dispensing stations have solved this problem of restricting people taking all of the fuel in the pump with a disruptive financial technology called blo\^H\^H\^H credit card.
-

View file

@ -162,6 +162,11 @@ table th, table td
padding: 4px;
}
hr
{
border-color: var(--color_codeborder);
}
ol ol, ul ul, ol ul, ul ol
{
padding-inline-start: 20px;

View file

@ -145,6 +145,10 @@ You're overthinking it!
Feedback wanted.
---
**Update**: I have not received any feedback, and I've taken some more good pictures. [Scales tipped](https://voussoir.net/photography).
![](photo1.jpg)
![](photo2.jpg)