Compare commits

...

3 commits

3 changed files with 188 additions and 29 deletions

View file

@ -585,6 +585,7 @@ const SPLASHES = [
// pretty jank nowadays.
"&evil=true", // https://en.m.wikipedia.org/wiki/Evil_bit
"(ↄ) all rights reversed",
"**i move away from the mic to breathe in", // Tay Zonday, Chocolate Rain https://youtu.be/EwTZ2xpQwpA?t=21
"... johnson?", // The Big Lebowski (1998) https://youtu.be/xs3OWJ53rHE
"0118 999 881 999 119 7253", // The IT Crowd https://youtu.be/ab8GtuPdrUQ
"09 f9 11 02 9d 74 e3 5b d8 41 56 c5 63 56 88 c0", // https://en.wikipedia.org/wiki/AACS_encryption_key_controversy
@ -663,6 +664,7 @@ const SPLASHES = [
"at the tone, the time will be...",
"automatic caution door",
"aziz, light!", // The Fifth Element (1997) https://youtu.be/CnvBpLWTQig
"backed by the full faith and credit of the us government",
"banana slips on man", // Banana slips on man https://youtu.be/aKn0HddzuWM
"barfed all over the benevolent order of antelopes", // Stand By Me (1986)
"be a winner", // Star Time (1992)
@ -683,6 +685,7 @@ const SPLASHES = [
"breaks userspace",
"brittle as an aged human being", // King Gizzard and the Lizard Wizard, Crumbling Castle https://youtu.be/qZwr511qGoY
"buddy, you don't look hip", // Taxi Driver (1976)
"bug fixes and performance improvements",
"built a tower of stone with our flesh and bone", // Rainbow, Stargazer (1976)
"but doctor, i am pagliacci",
"but enough about you, let's talk about me",
@ -727,6 +730,7 @@ const SPLASHES = [
"contributes to openstreetmap", // https://openstreetmap.org
"controls the spice", // Dune (1965)
"cool guy has a chill day", // Cool Guy has a Chill Day https://youtu.be/4txVqr1eNwc
"could give a speech on humility",
"could have sworn it was a lovers' quarrel", // The Whitest Kids U'Know, Happier and With Your Mouth More Open https://youtu.be/ABxH-NTF0SM R.I.P. Trevor
"could i interest you in everything, all of the time?", // Bo Burnham, Welcome to the Internet https://youtu.be/k1BneeJTDcU
"could you kick up the 4d3d3d3?", // Tim and Eric, Celery Man https://youtu.be/maAFcEU6atk
@ -770,6 +774,7 @@ const SPLASHES = [
"don't be evil", // Since Google is done with this motto I guess I'll use it for a while
"don't bernie me", // The Incredibles (2004)
"don't call it a comeback", // LL Cool J, Mama Said Knock You Out https://youtu.be/vimZj8HW0Kg
"don't create the torment nexus", // https://en.wikipedia.org/wiki/Torment_Nexus
"don't dead open inside", // https://knowyourmeme.com/memes/dont-dead-open-inside
"don't even think about going out, coraline jones!", // Coraline (2009)
"don't forget to like, share, and subscribe",
@ -786,6 +791,7 @@ const SPLASHES = [
"drank the kool-aid",
"drinking orange juice out of a champagne glass", // Fresh Prince of Bel Air https://youtu.be/AVbQo3IOC_A
"drive stick with that kung fu grip", // Bloodhound Gang, Uhn-Tiss Uhn-Tiss Uhn-Tiss (2005)
"dry your filament",
"du hast mich gefragt, und ich hab nichts gesagt", // Rammstein, Du Hast
"each one of these stickers adds five horsepower", // Sh_t Civic Owners Say https://youtu.be/f9x74SlY1ik
"ecco qui papa el mio belvedere", // https://en.wikipedia.org/wiki/File:The_Papal_Belvedere.jpg
@ -796,6 +802,7 @@ const SPLASHES = [
"endorses ublock origin", // https://github.com/gorhill/uBlock
"enemies foreign and domestic", // https://en.wikipedia.org/wiki/United_States_Uniformed_Services_Oath_of_Office
"enjoys collecting unspent ordnance",
"enlarged to show detail",
"ergo propter hoc",
"error: low on mayonnaise", // Worst Computer Ever https://youtu.be/HFXsMfcExi4
"est arrivé près de chez vous", // C'est arrivé près de chez vous / Man Bites Dog (1992)
@ -1065,6 +1072,7 @@ const SPLASHES = [
"not to scale",
"not unlike means like",
"nothin' but .net",
"now it's time to reiterate california", // What Red Hot Chili Peppers Sounds Like https://youtu.be/VE5JMEu5hZA
"nuapurista kuulu se polokan tahti jalakani pohjii kutkutti", // Ievan Polkka https://youtu.be/7yh9i0PAjck
"null",
"nur noch konkret reden, gib mir ein ja oder nein", // Peter Fox, Alles Neu https://youtu.be/DD0A2plMSVA
@ -1092,7 +1100,7 @@ const SPLASHES = [
"pasta, water, getting hotter", // Adventure Time S03E10 What Was Missing? (2011)
"pebcak", // https://en.wikipedia.org/wiki/User_error
"picture you gettin' down in a picture tube", // Gorillaz, Clint Eastwood https://youtu.be/1V_xRb0x9aw
"pie iesu domine, dona eis requiem",
"pie iesu domine, dona eis requiem", // Monty Python and the Holy Grail (1975) https://en.wikipedia.org/wiki/Pie_Jesu
"pillowy mounds of mashed potatoes", // https://groovypotatoes.ytmnd.com/
"pioneers used to ride these babies for miles", // SpongeBob S01E10 Pizza Delivery (1999)
"plato, aristotle, socrates? morons", // The Princess Bride (1987) https://youtu.be/BUg2cp23rGE
@ -1143,6 +1151,7 @@ const SPLASHES = [
"seeks your validation",
"seems like a one-ply kind of thought", // Joel Haver, Toilet Paper Bears https://youtu.be/IMKW-ifxikE
"selling knives and insurance from door to door", // Rhett & Link, Epic Rap Battle https://youtu.be/7ov1DDjHt8c
"sells to willing buyers at current fair market value", // Margin Call (2011)
"senator, we run ads", // https://youtu.be/GGTWUOxkfGQ
"seven lines all strictly perpendicular", // The Expert https://youtu.be/BKorP55Aqvg
"shall compare thee to a summer's day",
@ -1190,6 +1199,7 @@ const SPLASHES = [
"the door is a jar",
"the end recontextualizes the means",
"the eye is the window of the soul", // Obviously a general phrase but I'll attribute it to Videodrome (1983)
"the files are inside the computer", // Zoolander (2001)
"the fry bites back, my man", // OH MY DAYUM https://youtu.be/DcJFdCmN98s
"the goop is minimal", // randytaylor69, the moistening. https://youtu.be/KVmOGvQhWYE
"the happiest place on earth",
@ -1199,7 +1209,7 @@ const SPLASHES = [
"the menace must remain phantom", // The (Totally) Phantom Menace https://youtu.be/J0mUVY9fLlw
"the moment you've all been waiting for",
"the museum is the mausoleum is the men's room", // David Foster Wallace, E unibus pluram (1993)
"the of and to a in is i that it for you was with on as have but be they", // Vsauce, The Zip Mystery https://youtu.be/fCn8zs912OE
"the of and to a in is i that it for you was with on as have but be they", // Vsauce, The Zipf Mystery https://youtu.be/fCn8zs912OE
"the only way to be is to become",
"the only winning move is not to play",
"the opposite of apposite",
@ -1329,6 +1339,7 @@ const SPLASHES = [
"yet another tragedy in a long string of misfortunes", // Dream Theater, Metropolis Pt. 2 Scenes From a Memory, Finally Free
"yo, ding dong man, ding dong, ding dong yo", // Weird Al Yankovic, Fat (1988)
"you and me and sister ain't got nothin' to hide", // Scatman John, Scatman's World https://youtu.be/Ic2Cjw7kydI
"you are a sad, strange little man", // Toy Story (1996)
"you are here",
"you are now aware of your breathing",
"you can actually see nothing as long as it's next to something", // The Action Lab https://youtu.be/DAQB9RQP6pE&t=2m57s
@ -1342,6 +1353,7 @@ const SPLASHES = [
"you have arrived",
"you just lost your brain priveleges", // SpongeBob S03E03 Welcome to the ChumBucket
"you just lost yourself a customer", // Simpsons https://youtu.be/bwcJNsoY50E
"you know the germans always make good stuff", // Shamwow commercial https://youtu.be/F3lrhPeK6gU
"you know this boogie is for real", // Jamiroquai, Canned Heat
"you know, for kids", // The Hudsucker Proxy (1994)
"you may call me by my locally designated name", // Borderlands
@ -1351,6 +1363,7 @@ const SPLASHES = [
"you must have enough fuel units", // Tai Lopez https://youtu.be/Cv1RJTHf5fk
"you need to vibrate higher", // Death Grips, Culture Shock (2011)
"you really think people are gonna give you your mail?", // Accursed Farms videochat February 2023 https://youtu.be/5HZwr1V3y4A?t=2h18m15s
"you would be perfect for somebody else",
"you wouldn't get this from any other guy", // Rick Astley, Never Gonna Give You Up https://youtu.be/dQw4w9WgXcQ
"you'd better go to a seller that sells weaker potions", // Potion Seller https://youtu.be/R_FQU4KzN7A
"you'll thank me in ten years",

View file

@ -267,6 +267,10 @@ class Video:
def __init__(self, etq_photo, etq_album=None):
self.etq_photo = etq_photo
self.article_id = self.etq_photo.real_path.replace_extension('').basename
self.stream_file = self.etq_photo.real_path.replace_extension('stream').add_extension('mp4')
if not self.stream_file.is_file:
self.stream_file = etq_photo.real_path
if etq_album is None:
parent_key = 'photography'
@ -276,11 +280,18 @@ class Video:
self.s3_key = f'{parent_key}/{self.etq_photo.real_path.basename}'
self.small_key = f'{parent_key}/small_{self.etq_photo.real_path.replace_extension("jpg").basename}'
self.tiny_key = f'{parent_key}/tiny_{self.etq_photo.real_path.replace_extension("jpg").basename}'
self.stream_key = f'{parent_key}/{self.stream_file.basename}'
self.color_class = 'monochrome' if self.etq_photo.has_tag('monochrome') else ''
self.s3_exists = self.s3_key in S3_EXISTING_FILES;
self.big_exists = self.s3_key in S3_EXISTING_FILES;
self.stream_exists = self.stream_key in S3_EXISTING_FILES;
self.small_exists = self.small_key in S3_EXISTING_FILES;
self.tiny_exists = self.tiny_key in S3_EXISTING_FILES;
self.big_url = f'{S3_WEBROOT}/{self.s3_key}'
self.stream_url = f'{S3_WEBROOT}/{self.stream_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}'
@ -294,27 +305,35 @@ class Video:
self.exposure_time = 0
def make_thumbnail(self, size) -> io.BytesIO:
probe = kkroening_ffmpeg.probe(self.etq_photo.real_path.absolute_path)
video_stream = next(stream for stream in probe['streams'] if stream['codec_type'] == 'video')
video_width = int(video_stream['width'])
video_height = int(video_stream['height'])
if self.etq_photo.has_thumbnail:
bio = io.BytesIO(self.etq_photo.get_thumbnail())
image = PIL.Image.open(bio)
(width, height) = imagetools.fit_into_bounds(image.size[0], image.size[1], size, size)
image = image.resize((width, height), PIL.Image.LANCZOS)
bio = io.BytesIO()
image.save(bio, format='jpeg', quality=75)
bio.seek(0)
else:
probe = kkroening_ffmpeg.probe(self.etq_photo.real_path.absolute_path)
video_stream = next(stream for stream in probe['streams'] if stream['codec_type'] == 'video')
video_width = int(video_stream['width'])
video_height = int(video_stream['height'])
command = kkroening_ffmpeg.input(self.etq_photo.real_path.absolute_path, ss=10)
# command = command.filter('scale', size[0], size[1])
command = command.output('pipe:', vcodec='bmp', format='image2pipe', vframes=1)
(out, trash) = command.run(capture_stdout=True, capture_stderr=True)
bio = io.BytesIO(out)
image = PIL.Image.open(bio)
(width, height) = imagetools.fit_into_bounds(video_width, video_height, size, size)
image = image.resize((width, height), PIL.Image.LANCZOS)
bio = io.BytesIO(out)
image.save(bio, format='jpeg', quality=75)
bio.seek(0)
command = kkroening_ffmpeg.input(self.etq_photo.real_path.absolute_path, ss=10)
# command = command.filter('scale', size[0], size[1])
command = command.output('pipe:', vcodec='bmp', format='image2pipe', vframes=1)
(out, trash) = command.run(capture_stdout=True, capture_stderr=True)
bio = io.BytesIO(out)
image = PIL.Image.open(bio)
(width, height) = imagetools.fit_into_bounds(video_width, video_height, size, size)
image = image.resize((width, height), PIL.Image.LANCZOS)
bio = io.BytesIO(out)
image.save(bio, format='jpeg', quality=75)
bio.seek(0)
return bio
def prepare(self):
if not self.s3_exists:
self.s3_upload()
self.s3_upload()
def render_atom(self):
return f'''
@ -349,15 +368,25 @@ class Video:
return f'''
<article id="{self.article_id}" class="videograph {self.color_class}">
{download_tag}
<video controls preload="none" poster="{self.small_url}" src="{self.big_url}"></video>
<video controls preload="none" poster="{self.small_url}" src="{self.stream_url}"></video>
</article>
'''
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)
if (not self.small_exists) or '2025-06-28' in self.etq_photo.real_path.basename:
bucket.upload_fileobj(self.make_thumbnail(SIZE_SMALL), self.small_key)
if (not self.small_exists) or '2025-06-28' in self.etq_photo.real_path.basename:
bucket.upload_fileobj(self.make_thumbnail(SIZE_TINY), self.tiny_key)
if self.stream_key != self.s3_key and not self.stream_exists:
bucket.upload_fileobj(self.stream_file.open('rb'), self.stream_key)
if not self.big_exists:
bucket.upload_fileobj(self.etq_photo.real_path.open('rb'), self.s3_key)
self.s3_exists = True
def make_atom(items):
@ -378,9 +407,9 @@ def make_atom(items):
atom = textwrap.dedent(atom)
return atom
def make_webpage(items, is_root, doctitle):
def make_webpage(this_id, 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
back_link = None if is_root else (PHOTOGRAPHY_WEBROOT + '#' + this_id)
sort_reverse = is_root
html = jinja2.Template('''
@ -489,7 +518,8 @@ def make_webpage(items, is_root, doctitle):
margin-right: auto;
text-align: end;
}
header > *
header > *,
.back_link
{
display: inline-block;
padding: 16px;
@ -667,7 +697,7 @@ def make_webpage(items, is_root, doctitle):
{% endif %}
{%- if back_link -%}
<a href="{{back_link}}">Back</a>
<a class="back_link" href="{{back_link}}">Back</a>
{%- endif -%}
</header>
@ -684,6 +714,9 @@ def make_webpage(items, is_root, doctitle):
<p>Contact me: photography@voussoir.net</p>
<p>These photos took {{items|sum(attribute='exposure_time')|round(4)}} seconds to make.</p>
<p><button id="new_perspective_button" onclick="return new_perspective_button_onclick(event);">👁 Try a different perspective</button></p>
{%- if back_link -%}
<p><a class="back_link" href="{{back_link}}">Back to homepage</a></p>
{%- endif -%}
</footer>
<form id="a_new_perspective" class="hidden">
@ -966,12 +999,12 @@ def main(argv):
item.prepare()
log.info('Writing homepage')
homepage_html = make_webpage(items, is_root=True, doctitle='photography')
homepage_html = make_webpage(None, 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_html = make_webpage(album.article_id, 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)

View file

@ -0,0 +1,113 @@
[tag:today_i_did_this]
28-400
======
A couple of years ago, I bought this bottle of shampoo. I have been reusing it and refilling it ever since.
I particularly like the translucent, frosted plastic more than a crystal clear bottle or an opaque one. I think it's a pretty effect.
[![](thumbs/2026-01-04_21-49-07.jpg)](https://files.voussoir.net/writing/28_400/2026-01-04_21-49-07.jpg)
In fact, I've bought several pieces from a range of home goods that follow a white + frosted color scheme to keep things cohesive.
[![](thumbs/2026-01-04_23-26-56.jpg)](https://files.voussoir.net/writing/28_400/2026-01-04_23-26-56.jpg)
And several of the ikea pepprig spray bottles which I like for their shape.
[![](thumbs/2026-01-04_23-29-17.jpg)](https://files.voussoir.net/writing/28_400/2026-01-04_23-29-17.jpg)
[![](thumbs/2026-01-04_23-30-49.jpg)](https://files.voussoir.net/writing/28_400/2026-01-04_23-30-49.jpg)
At some point during my ownership of the blue shampoo bottle, I realized that I wanted to get two more so that I could have my shampoo, conditioner, and bodywash all in matching frosted bottles. But that brand has switched to clear bottles and I missed my chance. When I would go to the store I would check if any nice bottles might catch my attention, but I couldn't find a good match or a new bottle worth switching to. And since this isn't that important I just continued living with the mismatched bottles at home cuz whatever.
Recently though, I really got determined to buy some matching shower bottles. I wondered if I should join the bourgeoisie and buy actual frosted glass instead of plastic, despite the obvious questionability of keeping glass in the shower.
When searching for glass pump bottles online, it quickly became apparent that the [Boston Round](<https://en.wikipedia.org/wiki/Boston_round_(bottle)>) style of bottle is the most common for this purpose. My blue shampoo bottle holds 32 ounces, though considering that a 32 oz Boston round might not sit on the ledges of my shower as well as the oblong plastic bottle, I would be willing to accept a 16 oz instead.
My dreams and lacerations were not to be, however, since I absolutely could not find a suitable product except in large wholesale quantities. Search after search after search on multiple websites with all combinations of keywords simply could not produce for me a small set of 16 or 32 oz frosted glass Boston rounds.
Instead, all I could find were:
- Frosted glass less than 16 oz, which will simply be way too small for shower use.
[![](thumbs/2026-01-04_23-48-25.png)](https://files.voussoir.net/writing/28_400/2026-01-04_23-48-25.png)
- 32 oz frosted glass in too large a quantity
[![](thumbs/2026-01-06_19-30-52.png)](https://files.voussoir.net/writing/28_400/2026-01-06_19-30-52.png)
- Bottles in other colors, mostly clear, amber, or an opaque coating.
[![](thumbs/2026-01-06_19-33-11.png)](https://files.voussoir.net/writing/28_400/2026-01-06_19-33-11.png)
Don't get me wrong, these cobalt blues are very pretty as well, and I'm nearly tempted to buy some. But I don't actually have any use for more such glassware.
[![](thumbs/2026-01-05_00-11-56.png)](https://files.voussoir.net/writing/28_400/2026-01-05_00-11-56.png)
Having nearly given up on finding bottles to buy, I decided to try putting my shower products in the hand soap bottles that I already own. I have a few spares, and at 18 oz they're a decent capacity. But, the experience was poor: the dosage of those pumps is too low, requiring two or three presses to get a normal amount of shampoo; and the straws too narrow to deal with conditioner, often sending up nothing at all. So I aborted that idea and went back to searching.
Since I've gone down one corner of the rabbit hole that is glassware, and noticing that I'd probably have to buy the pump heads separately from the bottles themselves, I learned that the standard threading used on Boston rounds is most commonly of the 28-400 or 28-410 variety.
[![](thumbs/neck_styles.png)](https://www.paramountglobal.com/knowledge/bottle-neck-thread-finish/)
[![](thumbs/neck_infographic.jpg)](https://www.paramountglobal.com/knowledge/bottle-neck-thread-finish/)
There's actually a second, contemporary storyline that intersects here, which is that the pump heads on my hand and dish soap don't seem to be the most durable. One of them had already cracked pretty badly and started leaking (not pictured here, I threw it away -- keeping the bottle itself of course -- before deciding to write this article), and another has now started to crack as well.
[![](thumbs/2026-01-04_21-58-28.jpg)](https://files.voussoir.net/writing/28_400/2026-01-04_21-58-28.jpg)
[![](thumbs/2026-01-05_00-45-42.jpg)](https://files.voussoir.net/writing/28_400/2026-01-05_00-45-42.jpg)
These hand soap bottles cost me $5 each, so if the pumps crack I'd like to replace just the pumps and not the bottles. Do these also use a 28-400 thread?
I still don't need 500 of them, but you know what I mean.
[![](thumbs/2026-01-05_00-21-36.png)](https://files.voussoir.net/writing/28_400/2026-01-05_00-21-36.png)
Hmm, looks like 28mm to me. Sorry, this color is difficult to photograph.
[![](thumbs/2026-01-04_22-00-03.jpg)](https://files.voussoir.net/writing/28_400/2026-01-04_22-00-03.jpg)
I found that someone had already made an [STL file for a 28-400 pump](https://www.thingiverse.com/thing:6622235), so I printed up one of those to check if it fits.
[![](thumbs/2026-01-04_22-05-41.jpg)](https://files.voussoir.net/writing/28_400/2026-01-04_22-05-41.jpg)
Yes!
[![](thumbs/2026-01-04_22-07-54.jpg)](https://files.voussoir.net/writing/28_400/2026-01-04_22-07-54.jpg)
What else does it fit?
[![](thumbs/2026-01-04_22-10-31.jpg)](https://files.voussoir.net/writing/28_400/2026-01-04_22-10-31.jpg)
What *else* does it fit???
[![](thumbs/2026-01-05_01-03-01.jpg)](https://files.voussoir.net/writing/28_400/2026-01-05_01-03-01.jpg)
Does that mean...?
It does!
[![](thumbs/2026-01-04_22-25-36.jpg)](https://files.voussoir.net/writing/28_400/2026-01-04_22-25-36.jpg)
It means the pepprig are now my shower bottles. At 20 oz and a conveniently small footprint, they are a good size for the shower. Using the pumps that came from the original shower products, the dosage is correct, and nicely color matched with the contents. Though not matching the white + frosted theme, I like this too.
Here they are in situ.
[![](thumbs/2026-01-04_22-31-50.jpg)](https://files.voussoir.net/writing/28_400/2026-01-04_22-31-50.jpg)
[![](thumbs/2026-01-04_22-32-17.jpg)](https://files.voussoir.net/writing/28_400/2026-01-04_22-32-17.jpg)
This made me curious what else might be 28-400 compatible, with a few... cursed results.
[![](thumbs/2026-01-04_22-53-30.jpg)](https://files.voussoir.net/writing/28_400/2026-01-04_22-53-30.jpg)
[![](thumbs/2026-01-05_06-10-00.jpg)](https://files.voussoir.net/writing/28_400/2026-01-05_06-10-00.jpg)
The lesson today is:
- appreciate standardized interfaces whenever you find them, and
- try putting random stuff together
Have fun.