<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>sui generis (Lyjia's blog)</title>
    <description>sui generis (Lyjia's blog)</description>
    <link>https://lyjia.us/</link>
    <item>
      <title>Best Practices For Transferring Vinyl Records to Digital Files</title>
      <description>&lt;div class="trix-content"&gt;
  &lt;div&gt;&lt;action-text-attachment sgid="eyJfcmFpbHMiOnsiZGF0YSI6ImdpZDovL3BsYXlncm91bmQvQWN0aXZlU3RvcmFnZTo6QmxvYi80OD9leHBpcmVzX2luIiwicHVyIjoiYXR0YWNoYWJsZSJ9fQ==--6f34e2291f23a1d9461de5230035527d4087b846" content-type="image/jpeg" url="https://lyjia.us/rails/active_storage/blobs/redirect/eyJfcmFpbHMiOnsiZGF0YSI6NDgsInB1ciI6ImJsb2JfaWQifX0=--86a6cc55e50fdb00d9b80d2ecf9536bd4b4d6b73/IMG_1367.jpg" filename="IMG_1367.jpg" filesize="96707" width="547" height="600" previewable="true" presentation="gallery" caption="Old Faithful..."&gt;&lt;figure class="attachment attachment--preview attachment--jpg"&gt;
    &lt;img src="https://lyjia.us/rails/active_storage/representations/redirect/eyJfcmFpbHMiOnsiZGF0YSI6NDgsInB1ciI6ImJsb2JfaWQifX0=--86a6cc55e50fdb00d9b80d2ecf9536bd4b4d6b73/eyJfcmFpbHMiOnsiZGF0YSI6eyJmb3JtYXQiOiJqcGciLCJyZXNpemVfdG9fbGltaXQiOlsxMDI0LDc2OF19LCJwdXIiOiJ2YXJpYXRpb24ifX0=--4582af97109930a13c086ad18f8d949801b376ef/IMG_1367.jpg"&gt;

  &lt;figcaption class="attachment__caption"&gt;
      Old Faithful...
  &lt;/figcaption&gt;
&lt;/figure&gt;&lt;/action-text-attachment&gt;&lt;br&gt;&lt;br&gt;If you, like me, have a sizable vinyl record collection, then at some point you may want to make digital copies of them ("vinyl rips") so you can play them out or enjoy them on your phone or computer. I've done this a fair amount of times -- probably about a hundred records -- and let me tell you: there is a lot that can go wrong and affect the quality of your transfers!&lt;br&gt;&lt;br&gt;A friend of mine recently asked for some tips on vinyl ripping his own collection, so the following started as a comment on his Facebook post. However, after posting, I realized that some of this knowledge may not be very widely known, so I thought I would polish it up and make a blog post of it!&lt;br&gt;&lt;br&gt;This is not a step-by-step guide for beginners; I am assuming you already have your turntable, software, and audio setup worked out. (I am also kind of assuming you are an electronic music DJ, but this advice should work for anyone.) So if you want your hand held, look elsewhere! This post is more about the little things you want to think about before you hit Record:&lt;br&gt;&lt;br&gt;&lt;/div&gt;&lt;ol&gt;&lt;li&gt;&lt;strong&gt;Check your pitch calibration&lt;/strong&gt; using the dots on the side of the platter and the red light. You want to make sure that the '0' on the pitch control corresponds with exactly 33 or 45 RPM. This is especially important for accurate BPM readings, beatgridding, and beatmatching. If you don't know how to do this, &lt;a href="https://www.youtube.com/watch?v=kFERz1J13p8"&gt;here is a good guide&lt;/a&gt;.&lt;br&gt;&lt;br&gt;&lt;/li&gt;&lt;li&gt;It helps to &lt;strong&gt;have two turntables&lt;/strong&gt;: one for recording (deck 1), and one for prepping the next disc you want to record (deck 2).&lt;br&gt;&lt;br&gt;&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Do a quick cleaning on the disc right before you record to reduce dust. &lt;/strong&gt;A wet cleaning would be best; I usually do this on deck 2 while deck 1 records. You can put a fan on it so it dries fast. This way all you will have to do on the disc you are recording is a quick dusting or dry clean, mainly to catch dust from the slipmat (if you use those) or the rubber mat.&amp;nbsp;&lt;br&gt;&lt;br&gt;&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Use a fresh needle;&lt;/strong&gt; worn needles &lt;em&gt;will&lt;/em&gt; impact sound quality!&lt;br&gt;&lt;br&gt;&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Use a good preamp&lt;/strong&gt;. it will make a &lt;em&gt;huge&lt;/em&gt; difference! The preamps on my Pioneer XDJ controller are complete &lt;em&gt;ass&lt;/em&gt;. Preamps are usually kind of expensive, but I've had good luck with some of the cheap Chinese units you can get off Amazon. (&lt;a href="https://www.amazon.com/Fosi-Audio-Preamplifier-Phonograph-Pre-Amplifier/dp/B07XNTHHBP"&gt;Fosi Audio makes a pretty good hybrid tube/solid state preamp that isn't too expensive.&lt;/a&gt;)&lt;br&gt;&lt;br&gt;&lt;/li&gt;&lt;li&gt;If you're using a digital recorder, it can be annoying to match up generically-named audio files to their original disc. So, on a notepad, or in your phone's notes, &lt;strong&gt;write down the file name on the recorder next to the record name or Discogs ID&lt;/strong&gt;. Then, it will be easy to match which file goes to each disc, and then you can add other metadata later. (And be sure to hit the 'track cut' button, or hit stop and then record again!)&lt;br&gt;&lt;br&gt;&lt;/li&gt;&lt;li&gt;&lt;strong&gt;For electronic music:&lt;/strong&gt; even with the Technics direct drive, even if you calibrate it with the red light and the dots, there is very slight pitch wobble and BPM won't usually be a perfect round number. I believe this is a symptom of analog being analog both on your turntable and on the record cutter that produced the original mother that your disc is stamped from. This makes accurate BPM readings and setting up beatgrids on DJ software a much bigger pain in the ass than you might be used to with digital files. Be careful playing these out with SYNC enabled!&lt;br&gt;&lt;br&gt;&lt;/li&gt;&lt;li&gt;If possible, you should &lt;strong&gt;record in 32-bit float format&lt;/strong&gt;. This way it is practically impossible to clip the recording! Then you can set audio levels in post and bounce the final cut to 16-bit. 32-bit float has such a wide dynamic range that clipping is never going to happen. (That said, I don't think this benefits sound quality very much if at all; but there are those who would argue that you should record in at least 24-bit. My suspicion is that this is an audiophile's old wives' tale. My own research indicates that vinyl's dynamic range is equivalent to something closer to 12-bit digital audio.)&lt;br&gt;&lt;br&gt;&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Recording in anything above 44.1khz is probably overkill &lt;/strong&gt;-- but if you want a higher sample rate do an&lt;strong&gt; integer multiple&lt;/strong&gt; like 88.2khz. This way, you can minimize the chances of downsampling adding weird quantization artifacts. Again, there are those who would argue you should record in the highest sample rate you can, but in my opinion this more &lt;a href="https://www.sweetwater.com/insync/should-i-record-at-the-high-sample-rates/"&gt;audiophile cargo-culting&lt;/a&gt;. (The exception here is if you plan on doing weird stuff to the resulting audio file, like pitching it down, sampling, or running it through an FX chain. In those cases a higher sample rate would be better.)&lt;br&gt;&lt;br&gt;&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Set your needle weight&lt;/strong&gt; to something a little higher than usual, so that you can minimize skips.&lt;br&gt;&lt;br&gt;&lt;/li&gt;&lt;li&gt;Make sure the needle is as close to &lt;strong&gt;perpendicular&lt;/strong&gt; to the disc as possible; a slanted needle will produce stereo audio levels that are slightly higher in one channel than the other. If possible you will want to verify this with level meters before getting started.&lt;/li&gt;&lt;/ol&gt;&lt;div&gt;&lt;br&gt;And one bonus tip: &lt;strong&gt;handle your needles and the tonearm very carefully!&lt;/strong&gt; Don't be like me and drunkenly clip the cartridge with your arm, sending the needle skidding across the record like a crashed motorcycle. The tonearm+cartridge+needle assembly is very delicately balanced and shocks like that can throw something out-of-whack!&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br&gt;Finally, I don't have any recommendations for needles or cartridges, other than the venerable Shure M44g, if you can get your hands on one. I'm still pissed that Shure stopped making them. When I run out of spares I will be a very sad panda. (Though I have heard good things about &lt;a href="https://www.jico-stylus.com/cartridge-number/m44g-m44-7/"&gt;JICO's clones&lt;/a&gt;.)&lt;/div&gt;

&lt;/div&gt;
</description>
      <pubDate>Thu, 02 Apr 2026 23:54:24 +0000</pubDate>
      <link>https://lyjia.us/blog/p/vinyl-ripping-tips</link>
      <guid>https://lyjia.us/blog/p/vinyl-ripping-tips</guid>
    </item>
    <item>
      <title>Run Alembic Migrations Outside of a Transaction Block</title>
      <description>&lt;div class="trix-content"&gt;
  &lt;div&gt;&lt;action-text-attachment sgid="eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaEpJamRuYVdRNkx5OXdiR0Y1WjNKdmRXNWtMMEZqZEdsMlpWTjBiM0poWjJVNk9rSnNiMkl2TkRFL1pYaHdhWEpsYzE5cGJnWTZCa1ZVIiwiZXhwIjpudWxsLCJwdXIiOiJhdHRhY2hhYmxlIn19--78441a88626d1f77151ab2c50b4215971a1718f8" content-type="image/png" url="https://www.lyjia.us/rails/active_storage/blobs/redirect/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBMZz09IiwiZXhwIjpudWxsLCJwdXIiOiJibG9iX2lkIn19--2d6e0da5d58aeab8f26c54088dc8b6a88fa2b699/d0ef0afd-1a67-465b-8f69-8abce2d15d8d.png" filename="d0ef0afd-1a67-465b-8f69-8abce2d15d8d.png" filesize="1588561" width="1024" height="1024" previewable="true" presentation="gallery" caption="Sitting around the campfire"&gt;&lt;figure class="attachment attachment--preview attachment--png"&gt;
    &lt;img src="https://lyjia.us/rails/active_storage/representations/redirect/eyJfcmFpbHMiOnsiZGF0YSI6NDEsInB1ciI6ImJsb2JfaWQifX0=--649eca2ce633395c076fa0d476ee23f629158a60/eyJfcmFpbHMiOnsiZGF0YSI6eyJmb3JtYXQiOiJwbmciLCJyZXNpemVfdG9fbGltaXQiOlsxMDI0LDc2OF19LCJwdXIiOiJ2YXJpYXRpb24ifX0=--199294c92961cfde7a884b276194a0c797db67ef/d0ef0afd-1a67-465b-8f69-8abce2d15d8d.png"&gt;

  &lt;figcaption class="attachment__caption"&gt;
      Sitting around the campfire
  &lt;/figcaption&gt;
&lt;/figure&gt;&lt;/action-text-attachment&gt;&lt;br&gt;I recently ran into the following error when adding some indices to a &lt;a href="https://www.postgresql.org/"&gt;PostgreSQL&lt;/a&gt; database from within an &lt;a href="https://pypi.org/project/alembic/"&gt;Alembic&lt;/a&gt; migration:&lt;br&gt;&lt;br&gt;&lt;/div&gt;&lt;pre&gt;CREATE INDEX CONCURRENTLY cannot run inside a transaction block&lt;/pre&gt;&lt;div&gt;&lt;br&gt;Which makes sense, particularly if you're creating the table with the index in the same migration (transaction block) -- how can `CREATE INDEX` know anything about a table that doesn't exist yet?&lt;br&gt;&lt;br&gt;There are a variety of SQL schema transforms (a.k.a. &lt;a href="https://en.wikipedia.org/wiki/Data_definition_language"&gt;DDL&lt;/a&gt; statements) that work this way. Alembic migrations are always run &lt;em&gt;inside&lt;/em&gt; a transaction, and breaking out a given statement is not immediately obvious. Luckily, Alembic provides a way for us to end the transaction early and execute statements on their own ("autocommit"):&lt;br&gt;&lt;br&gt;&lt;/div&gt;&lt;pre&gt;def upgrade():
    with op.get_context().autocommit_block():
        op.execute("ALTER TYPE mood ADD VALUE 'soso'")&lt;/pre&gt;&lt;div&gt;&lt;br&gt;It's important to note that any statements before the autocommit block &lt;strong&gt;will be committed!&lt;/strong&gt; So it is probably best to do this last, at the end of your migration, in order to keep things simple.&lt;br&gt;&lt;br&gt;And while this blog post speaks of PostgreSQL, this should apply to any database that SQLAlchemy supports!&lt;br&gt;&lt;br&gt;More info: &lt;a href="https://alembic.sqlalchemy.org/en/latest/api/runtime.html#alembic.runtime.migration.MigrationContext.autocommit_block"&gt;Alembic Documentation&lt;/a&gt;&lt;br&gt;H/T &lt;a href="https://stackoverflow.com/questions/53641912/how-to-disable-ddl-transaction-in-an-alembic-migration"&gt;Stack Overflow&lt;/a&gt;&lt;/div&gt;

&lt;/div&gt;
</description>
      <pubDate>Thu, 12 Jun 2025 19:55:10 +0000</pubDate>
      <link>https://lyjia.us/blog/p/alembic-migrations-outside-transaction-block</link>
      <guid>https://lyjia.us/blog/p/alembic-migrations-outside-transaction-block</guid>
    </item>
    <item>
      <title>It's Technological / The Dream Now Available on Streaming!</title>
      <description>&lt;div class="trix-content"&gt;
  &lt;div&gt;&lt;action-text-attachment sgid="eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaEpJamRuYVdRNkx5OXdiR0Y1WjNKdmRXNWtMMEZqZEdsMlpWTjBiM0poWjJVNk9rSnNiMkl2TXprL1pYaHdhWEpsYzE5cGJnWTZCa1ZVIiwiZXhwIjpudWxsLCJwdXIiOiJhdHRhY2hhYmxlIn19--a19cfeb5f2e4c2d02d8b5026fd96b22b6d0e47a7" content-type="image/png" url="https://www.lyjia.us/rails/active_storage/blobs/redirect/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBMQT09IiwiZXhwIjpudWxsLCJwdXIiOiJibG9iX2lkIn19--a1639c641948db1bb648765e1d971bad92574349/Its%20Technological%20-%20The%20Dream%20Cover_1000px.png" filename="Its Technological - The Dream Cover_1000px.png" filesize="1326638" width="1000" height="1000" previewable="true" presentation="gallery" caption="It's Technological / The Dream"&gt;&lt;figure class="attachment attachment--preview attachment--png"&gt;
    &lt;img src="https://lyjia.us/rails/active_storage/representations/redirect/eyJfcmFpbHMiOnsiZGF0YSI6MzksInB1ciI6ImJsb2JfaWQifX0=--541d77ccc2ab896d0e0e31620663aa69177ccbd0/eyJfcmFpbHMiOnsiZGF0YSI6eyJmb3JtYXQiOiJwbmciLCJyZXNpemVfdG9fbGltaXQiOlsxMDI0LDc2OF19LCJwdXIiOiJ2YXJpYXRpb24ifX0=--199294c92961cfde7a884b276194a0c797db67ef/Its%20Technological%20-%20The%20Dream%20Cover_1000px.png"&gt;

  &lt;figcaption class="attachment__caption"&gt;
      It's Technological / The Dream
  &lt;/figcaption&gt;
&lt;/figure&gt;&lt;/action-text-attachment&gt;&lt;br&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;Selected tracks from my debut album, &lt;a href="https://lyjia.bandcamp.com/album/listen-ep"&gt;&lt;strong&gt;Listen E.P.&lt;/strong&gt;&lt;/a&gt;, are now available for your enjoyment on streaming platforms worldwide! &lt;a href="https://open.spotify.com/album/5LHdEEdBFdzoZhwhv59s3S?si=rlSp6jwWQi20839wNEz_Jg"&gt;&lt;strong&gt;It's Technological / The Dream&lt;/strong&gt;&lt;/a&gt; can be found on Spotify, Apple Music, and many others! Give em a listen today!&lt;/div&gt;

&lt;/div&gt;
</description>
      <pubDate>Fri, 20 Dec 2024 17:44:07 +0000</pubDate>
      <link>https://lyjia.us/blog/p/its-technological-the-dream-now-available-on-streaming</link>
      <guid>https://lyjia.us/blog/p/its-technological-the-dream-now-available-on-streaming</guid>
    </item>
    <item>
      <title>How to Set an Empty Dictionary/Hash as Default Value for a PostgreSQL HSTORE Column</title>
      <description>&lt;div class="trix-content"&gt;
  &lt;div&gt;Do you have an HSTORE column on your &lt;a href="https://www.postgresql.org/"&gt;PostgreSQL&lt;/a&gt; database that you don't want to be `null` but need to have a default value? The syntax for this is a little irregular; so I'm posting it here for my own reference and yours:&lt;br&gt;&lt;br&gt;&lt;/div&gt;&lt;pre&gt;my_column HSTORE DEFAULT '' NOT NULL&lt;/pre&gt;&lt;div&gt;&lt;br&gt;is the line in your `CREATE TABLE`command that you want. &lt;br&gt;&lt;br&gt;In &lt;a href="https://rubyonrails.org/"&gt;&lt;strong&gt;Ruby on Rails&lt;/strong&gt;&lt;/a&gt;, using an &lt;a href="https://guides.rubyonrails.org/active_record_basics.html"&gt;ActiveRecord&lt;/a&gt; migration, you would use:&lt;br&gt;&lt;br&gt;&lt;/div&gt;&lt;pre&gt;t.hstore :my_column, default: {}&lt;/pre&gt;&lt;div&gt;&lt;br&gt;In &lt;a href="https://www.python.org/"&gt;&lt;strong&gt;Python&lt;/strong&gt;&lt;/a&gt;&lt;strong&gt;,&lt;/strong&gt; using an &lt;a href="https://alembic.sqlalchemy.org/en/latest/"&gt;Alembic&lt;/a&gt; migration, you would use:&lt;br&gt;&lt;br&gt;&lt;/div&gt;&lt;pre&gt;sa.Column('my_column', HSTORE(), nullable=False, server_default=sa.text("''")),&lt;/pre&gt;&lt;div&gt;&lt;br&gt;Additionally, if you want your SQLAlchemy model object to initialize this column with said empty dictionary (instead of `None`), &lt;a href="https://stackoverflow.com/questions/28331046/why-does-sqlalchemy-initialize-hstore-field-as-null"&gt;per this StackOverflow post&lt;/a&gt; you need take a couple of extra steps in your model:&lt;br&gt;&lt;br&gt;&lt;/div&gt;&lt;pre&gt;from sqlalchemy.dialects.postgresql import HSTORE
from sqlalchemy.ext.mutable import MutableDict

class Item(db.Model):
    my_column = db.Column(MutableDict.as_mutable(HSTORE), nullable=False, default={}, server_default='')

    def __init__(self, **kwargs):
        kwargs.setdefault('my_column', {})
        super(Item, self).__init__(**kwargs)&lt;/pre&gt;&lt;div&gt;&lt;br&gt;&lt;br&gt;&lt;action-text-attachment sgid="eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaEpJamRuYVdRNkx5OXdiR0Y1WjNKdmRXNWtMMEZqZEdsMlpWTjBiM0poWjJVNk9rSnNiMkl2TXpjL1pYaHdhWEpsYzE5cGJnWTZCa1ZVIiwiZXhwIjpudWxsLCJwdXIiOiJhdHRhY2hhYmxlIn19--346ae8bf9fd9245a2376d73ccc76db530dadcb57" content-type="image/gif" url="https://www.lyjia.us/rails/active_storage/blobs/redirect/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBLZz09IiwiZXhwIjpudWxsLCJwdXIiOiJibG9iX2lkIn19--15620b0b0739c4a46aa4ec5cc5e0e4a1674a48c4/f9e.gif" filename="f9e.gif" filesize="403819" width="305" height="185" previewable="true" presentation="gallery" caption="The More You Know!"&gt;&lt;figure class="attachment attachment--preview attachment--gif"&gt;
    &lt;img src="https://lyjia.us/rails/active_storage/representations/redirect/eyJfcmFpbHMiOnsiZGF0YSI6MzcsInB1ciI6ImJsb2JfaWQifX0=--77b305a2fee6e6b4fa9c3c92ddaf54c8133e921f/eyJfcmFpbHMiOnsiZGF0YSI6eyJmb3JtYXQiOiJnaWYiLCJyZXNpemVfdG9fbGltaXQiOlsxMDI0LDc2OF19LCJwdXIiOiJ2YXJpYXRpb24ifX0=--e1f11a4d1d1e1eb0c15d8a737f6b72ea13c219d7/f9e.gif"&gt;

  &lt;figcaption class="attachment__caption"&gt;
      The More You Know!
  &lt;/figcaption&gt;
&lt;/figure&gt;&lt;/action-text-attachment&gt;&lt;/div&gt;

&lt;/div&gt;
</description>
      <pubDate>Tue, 30 Jul 2024 17:51:59 +0000</pubDate>
      <link>https://lyjia.us/blog/p/how-to-set-empty-hash-for-postgres-hstore</link>
      <guid>https://lyjia.us/blog/p/how-to-set-empty-hash-for-postgres-hstore</guid>
    </item>
    <item>
      <title>Lyjia's Directory of Free, Open Collections of Historically Significant Art</title>
      <description>&lt;div class="trix-content"&gt;
  &lt;div&gt;&lt;action-text-attachment sgid="BAh7CEkiCGdpZAY6BkVUSSI3Z2lkOi8vcGxheWdyb3VuZC9BY3RpdmVTdG9yYWdlOjpCbG9iLzM1P2V4cGlyZXNfaW4GOwBUSSIMcHVycG9zZQY7AFRJIg9hdHRhY2hhYmxlBjsAVEkiD2V4cGlyZXNfYXQGOwBUMA==--83dc8941de47916084571a57e93f619770fa58e8" content-type="image/jpeg" url="https://www.lyjia.us/rails/active_storage/blobs/redirect/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBLQT09IiwiZXhwIjpudWxsLCJwdXIiOiJibG9iX2lkIn19--2485e5e1a8c1239a246f52c5c758431914e9446f/0670_repro_2.jpg" filename="0670_repro_2.jpg" filesize="114499" width="500" height="592" previewable="true" presentation="gallery" caption="Girl with a Pearl Earring by Johannes Vermeer (Mauritshuis)"&gt;&lt;figure class="attachment attachment--preview attachment--jpg"&gt;
    &lt;img src="https://lyjia.us/rails/active_storage/representations/redirect/eyJfcmFpbHMiOnsiZGF0YSI6MzUsInB1ciI6ImJsb2JfaWQifX0=--805cc59529961449c9a28ddd5ce0664335dfc992/eyJfcmFpbHMiOnsiZGF0YSI6eyJmb3JtYXQiOiJqcGciLCJyZXNpemVfdG9fbGltaXQiOlsxMDI0LDc2OF19LCJwdXIiOiJ2YXJpYXRpb24ifX0=--4582af97109930a13c086ad18f8d949801b376ef/0670_repro_2.jpg"&gt;

  &lt;figcaption class="attachment__caption"&gt;
      Girl with a Pearl Earring by Johannes Vermeer (Mauritshuis)
  &lt;/figcaption&gt;
&lt;/figure&gt;&lt;/action-text-attachment&gt;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;Every so often I find myself looking for art for some reason or another -- maybe a blog post, or referencing something I saw in a museum, or maybe just for plain enjoyment -- and oftentimes it can be found in an open image collection! I've kept a small working list of these places in my head over the years, and I realized it might be useful to write them down and figure out what else is out there. As it turns out, there is a lot!&lt;br&gt;&lt;br&gt;This is a collection of &lt;strong&gt;high-resolution, open-access&lt;/strong&gt;, &lt;strong&gt;free-download&lt;/strong&gt;, &lt;strong&gt;free-to-use&lt;/strong&gt; image libraries, focusing on the visual arts like painting or photography. There are other styles of art (like sculptures, audio, or archaeological artifacts) in some of these links but that is not the focus. Most of these links are free to use in any application -- commercial or non-commercial -- but not all. Please check the license before using anything you find here commercially. The images in these links should be a mixture of public domain and Creative Commons-licensed content.&lt;br&gt;&lt;br&gt;This list was last updated in &lt;strong&gt;July 2024&lt;/strong&gt;.&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;Libraries&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;a href="https://digitalcollections.nypl.org/"&gt;New York Public Library Public Domain Collection&lt;/a&gt;s&lt;br&gt;&lt;br&gt;&lt;a href="https://www.flickr.com/photos/britishlibrary"&gt;The British Library (on Flickr)&lt;/a&gt;&lt;br&gt;&lt;br&gt;&lt;a href="https://www.loc.gov/free-to-use/"&gt;Library of Congress Free to Use&lt;/a&gt;&lt;br&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;Space&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;a href="https://images.nasa.gov/"&gt;NASA Image and Video Library&lt;/a&gt;&lt;br&gt;&lt;br&gt;&lt;a href="https://webbtelescope.org/images"&gt;WEBB Image Library&lt;/a&gt;&lt;br&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;Museums&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;a href="https://www.nga.gov/collection-search-result.html?sortOrder=DEFAULT&amp;amp;artobj_downloadable=Image_download_available&amp;amp;pageNumber=1&amp;amp;lastFacet=artobj_downloadable"&gt;National Gallery of Art Open Access&lt;/a&gt;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;a href="https://www.getty.edu/art/collection/search?open_content=true"&gt;The Getty Collection&lt;/a&gt; (Los Angeles)&lt;br&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;a href="https://www.metmuseum.org/art/collection/search?searchField=All&amp;amp;showOnly=openAccess&amp;amp;sortBy=relevance"&gt;The Metropolitan Museum of Art Open Collection&lt;/a&gt; (New York)&lt;br&gt;&lt;br&gt;&lt;a href="https://www.si.edu/openaccess"&gt;Smithsonian Open Access&lt;/a&gt; (Washington D.C.)&lt;br&gt;&lt;br&gt;&lt;a href="https://www.clevelandart.org/open-access"&gt;Cleveland Museum of Art Open Access&lt;/a&gt;&lt;br&gt;&lt;br&gt;&lt;a href="https://www.belvedere.at/en/open-content"&gt;Belvedere Open Content (Vienna)&lt;/a&gt;&lt;br&gt;&lt;br&gt;&lt;a href="https://kunstmuseumbasel.ch/en/collection/collectiononline"&gt;Kunstmuseum Basel Collection Online&lt;/a&gt;&lt;br&gt;&lt;br&gt;&lt;a href="https://collections.lacma.org/"&gt;Los Angeles County Museum of Art&lt;/a&gt;&lt;br&gt;&lt;br&gt;&lt;a href="https://www.mauritshuis.nl/en/our-collection/"&gt;Mauritshuis (The Hague)&lt;/a&gt;&lt;br&gt;&lt;br&gt;&lt;a href="https://collections.artsmia.org/info/open-access"&gt;Minneapolis Museum of Art Open Access&lt;/a&gt;&lt;br&gt;&lt;br&gt;&lt;a href="https://collections.tepapa.govt.nz/"&gt;Museum of New Zealand Collections Online&lt;/a&gt;&lt;br&gt;&lt;br&gt;&lt;a href="https://commons.wikimedia.org/wiki/Category:Images_from_the_Nationalmuseum_Stockholm"&gt;Nationalmuseum Stockholm (via Wikimedia Commons)&lt;/a&gt;&lt;br&gt;&lt;br&gt;&lt;a href="https://www.parismuseescollections.paris.fr/fr"&gt;Paris Musées Collections&lt;/a&gt;&lt;br&gt;&lt;br&gt;&lt;a href="https://www.rijksmuseum.nl/en/photoservice"&gt;Rijksmuseum Image Requests (Amsterdam)&lt;/a&gt;&lt;br&gt;&lt;br&gt;&lt;a href="https://www.lenbachhaus.de/entdecken/sammlung-online/"&gt;Lenbachhaus Collection Online (Munich)&lt;/a&gt;&lt;br&gt;&lt;br&gt;&lt;a href="https://www.smk.dk/en/article/smk-open/"&gt;Statens Museum for Kunst (Copenhagen)&lt;/a&gt;&lt;br&gt;&lt;br&gt;&lt;a href="https://wellcomecollection.org/search/works"&gt;Wellcome Collection (London)&lt;/a&gt;&lt;br&gt;&lt;br&gt;&lt;a href="https://www.tnm.jp/modules/r_collection/?controller=top"&gt;Tokyo National Museum Collection&lt;/a&gt;&lt;br&gt;&lt;br&gt;&lt;a href="https://theme.npm.edu.tw/opendata/"&gt;Taipei National Palace Museum Open Data&lt;/a&gt;&lt;br&gt;&lt;br&gt;&lt;a href="https://www.moma.co.uk/public-domain-images/"&gt;MoMa UK Public Domain Images for Artists&lt;/a&gt;&lt;br&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;Universities&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;a href="https://www.artic.edu/collection"&gt;Art Institute Chicago Open Access&lt;/a&gt;&lt;br&gt;&lt;br&gt;&lt;a href="https://www.ub.uni-heidelberg.de/en/service/digitale-bibliothek"&gt;University of Heidelberg Digital Library&lt;/a&gt;&lt;br&gt;&lt;br&gt;&lt;a href="https://harvardartmuseums.org/collections"&gt;Harvard Art Museums Open Access&lt;/a&gt;&lt;br&gt;&lt;br&gt;&lt;a href="https://artgallery.yale.edu/collection?f%5B0%5D=open_access%3A1"&gt;Yale University Art Gallery Open Access&lt;/a&gt;&lt;br&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;Single-Artist Collections&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;a href="https://foto.munchmuseet.no/fotoweb/"&gt;Munch Museum Norway (Edvard Munch)&lt;/a&gt;&lt;br&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;Aggregators&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;a href="https://www.europeana.eu/en"&gt;Europeana&lt;/a&gt;&lt;br&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;Other Lists&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;a href="https://www.apollo-magazine.com/open-access-image-libraries-a-handy-list/"&gt;Open Access Image Libraries - A Handy List (Apollo Magazine)&lt;/a&gt;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;a href="https://ucsd.libguides.com/c.php?g=90812&amp;amp;p=584160"&gt;Visual Arts: Open Access Art &amp;amp; Architecture Image Resources (UCSD)&lt;/a&gt;&lt;/div&gt;

&lt;/div&gt;
</description>
      <pubDate>Thu, 27 Jun 2024 17:49:31 +0000</pubDate>
      <link>https://lyjia.us/blog/p/lyjias-directory-of-free-open-art-collections</link>
      <guid>https://lyjia.us/blog/p/lyjias-directory-of-free-open-art-collections</guid>
    </item>
    <item>
      <title>Fix Broken Beatport MP3 Downloads in Serato</title>
      <description>&lt;div class="trix-content"&gt;
  &lt;div&gt;&lt;action-text-attachment sgid="BAh7CEkiCGdpZAY6BkVUSSI3Z2lkOi8vcGxheWdyb3VuZC9BY3RpdmVTdG9yYWdlOjpCbG9iLzMzP2V4cGlyZXNfaW4GOwBUSSIMcHVycG9zZQY7AFRJIg9hdHRhY2hhYmxlBjsAVEkiD2V4cGlyZXNfYXQGOwBUMA==--ea051fad57a414f19bad3f2eb32da5f9d6c76b5d" content-type="image/jpeg" url="https://www.lyjia.us/rails/active_storage/blobs/redirect/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBKZz09IiwiZXhwIjpudWxsLCJwdXIiOiJibG9iX2lkIn19--a5e0301d1ab707c9379716ae853e09e33fb72c9c/683ca8a7-9d78-4a3b-a5c5-472eba35d71c.jpg" filename="683ca8a7-9d78-4a3b-a5c5-472eba35d71c.jpg" filesize="351476" width="1024" height="1024" previewable="true" presentation="gallery" caption="It kinda be like that..."&gt;&lt;figure class="attachment attachment--preview attachment--jpg"&gt;
    &lt;img src="https://lyjia.us/rails/active_storage/representations/redirect/eyJfcmFpbHMiOnsiZGF0YSI6MzMsInB1ciI6ImJsb2JfaWQifX0=--e8fdc9b3aeeb5a26bcf50381db3ca68acdda040f/eyJfcmFpbHMiOnsiZGF0YSI6eyJmb3JtYXQiOiJqcGciLCJyZXNpemVfdG9fbGltaXQiOlsxMDI0LDc2OF19LCJwdXIiOiJ2YXJpYXRpb24ifX0=--4582af97109930a13c086ad18f8d949801b376ef/683ca8a7-9d78-4a3b-a5c5-472eba35d71c.jpg"&gt;

  &lt;figcaption class="attachment__caption"&gt;
      It kinda be like that...
  &lt;/figcaption&gt;
&lt;/figure&gt;&lt;/action-text-attachment&gt;&lt;br&gt;&lt;br&gt;If you're experiencing a problem with &lt;a href="https://serato.com/dj/pro"&gt;Serato DJ&lt;/a&gt;, where MP3s purchased on &lt;a href="https://www.beatport.com/"&gt;Beatport&lt;/a&gt; cannot seem to save key, BPM, or track analyses and overviews, then perhaps a tool that I have written might help!&lt;br&gt;&lt;br&gt;Check out &lt;a href="https://github.com/lyjia/mp3-tag-rebuilder"&gt;MP3TagRebuilder&lt;/a&gt;, a simple Python script I wrote to address this issue with my own DJ library!&lt;br&gt;&lt;br&gt;This tool addresses an issue I've been encountering somewhat frequently over the last few years, where my Beatport music purchases have a weird glitch in Serato where overviews and tag data won't save, even after using the "Analyze" feature. The only solution I have found, even after writing Serato support, is to rebuild the MP3 files' ID3 tags destructively. &lt;br&gt;&lt;br&gt;However, every program that I know of that does this ends up dropping important tags, such as Album Art, because none of them provide a direct pathway to simply destroying the ID3 tags and then rebuilding them with a new datastructure; most of them only seem to support converting from ID3v2 to ID3v1 and back again. So I wrote my own!&lt;br&gt;&lt;br&gt;If you are encountering this issue and are feeling bold enough to test my code on your own library (MAKE SURE YOU HAVE A BACKUP AND TEST IT!!!!), head on over to Github and &lt;a href="https://github.com/lyjia/mp3-tag-rebuilder"&gt;check it out&lt;/a&gt;!&lt;br&gt;&lt;br&gt;You will need a working Python environment and must be comfortable with a command prompt. Instructions for running this tool are included in README.md, and instructions for installing Python can be found &lt;a href="https://kinsta.com/knowledgebase/install-python/"&gt;here&lt;/a&gt;.&lt;br&gt;&lt;br&gt;&lt;strong&gt;Get it from Github here: &lt;/strong&gt;&lt;a href="https://github.com/lyjia/mp3-tag-rebuilder"&gt;https://github.com/lyjia/mp3-tag-rebuilder&lt;/a&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;/div&gt;

&lt;/div&gt;
</description>
      <pubDate>Tue, 20 Feb 2024 00:39:54 +0000</pubDate>
      <link>https://lyjia.us/blog/p/fix-broken-mp3-downloads-in-serato</link>
      <guid>https://lyjia.us/blog/p/fix-broken-mp3-downloads-in-serato</guid>
    </item>
    <item>
      <title>Annoucing my first album release: Listen EP!</title>
      <description>&lt;div class="trix-content"&gt;
  &lt;div&gt;I am pleased to announce the release of my first album, a love-letter to DnB and bass music titled &lt;strong&gt;Listen EP!&lt;/strong&gt;&lt;br&gt;&lt;br&gt;&lt;action-text-attachment sgid="BAh7CEkiCGdpZAY6BkVUSSI3Z2lkOi8vcGxheWdyb3VuZC9BY3RpdmVTdG9yYWdlOjpCbG9iLzI3P2V4cGlyZXNfaW4GOwBUSSIMcHVycG9zZQY7AFRJIg9hdHRhY2hhYmxlBjsAVEkiD2V4cGlyZXNfYXQGOwBUMA==--f2d4d9b0317d015c99b32e9260de232910ff02f4" content-type="image/png" url="https://www.lyjia.us/rails/active_storage/blobs/redirect/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBJQT09IiwiZXhwIjpudWxsLCJwdXIiOiJibG9iX2lkIn19--310f72185edeef9c89352a7e769318b6fa0fbf35/Listen%20EP%20Cover%20v2%201000p.png" filename="Listen EP Cover v2 1000p.png" filesize="2335461" width="1000" height="1000" previewable="true" presentation="gallery" caption="Cover art by K. Kam"&gt;&lt;figure class="attachment attachment--preview attachment--png"&gt;
    &lt;img src="https://lyjia.us/rails/active_storage/representations/redirect/eyJfcmFpbHMiOnsiZGF0YSI6MjcsInB1ciI6ImJsb2JfaWQifX0=--1e1743050c1710cc2d101bbc5d19963fbbefece0/eyJfcmFpbHMiOnsiZGF0YSI6eyJmb3JtYXQiOiJwbmciLCJyZXNpemVfdG9fbGltaXQiOlsxMDI0LDc2OF19LCJwdXIiOiJ2YXJpYXRpb24ifX0=--199294c92961cfde7a884b276194a0c797db67ef/Listen%20EP%20Cover%20v2%201000p.png"&gt;

  &lt;figcaption class="attachment__caption"&gt;
      Cover art by K. Kam
  &lt;/figcaption&gt;
&lt;/figure&gt;&lt;/action-text-attachment&gt;&lt;br&gt;&lt;br&gt;Almost half an hour of trippy, psychedelic beats spanning halftime and techstep Drum-n-Bass, and finishing with a soothingly psychedelic downtempo closer! I've commissioned cover art from the fellow San Diego-based artist &lt;a href="https://www.instagram.com/donwrryboutit_/"&gt;&lt;strong&gt;K. Kam&lt;/strong&gt;&lt;/a&gt;, and had the album mastered by DnB industry veteran and &lt;a href="https://www.metalheadz.co.uk/home"&gt;Metalheadz&lt;/a&gt; alum &lt;a href="https://www.villemmusic.com/"&gt;&lt;strong&gt;Villem&lt;/strong&gt;&lt;/a&gt;!&lt;br&gt;&lt;br&gt;While this album is available for &lt;strong&gt;FREE STREAMING&lt;/strong&gt; on &lt;a href="https://soundcloud.com/lyjia/sets/listen-ep"&gt;Soundcloud&lt;/a&gt; and YouTube, I would really prefer if you would support me by &lt;a href="https://lyjia.bandcamp.com/album/listen-ep"&gt;&lt;strong&gt;purchasing a copy&lt;/strong&gt;&amp;nbsp;(for streaming or download) &lt;strong&gt;at Bandcamp&lt;/strong&gt;&lt;/a&gt;&lt;strong&gt;!&lt;/strong&gt; It's only &lt;strong&gt;$5&lt;/strong&gt; ... for less than the price of a cup of coffee you'll get &lt;a href="https://lyjia.bandcamp.com/album/listen-ep"&gt;&lt;strong&gt;unlimited access to stream and download&lt;/strong&gt;&lt;/a&gt; the album and use it in your own DJ sets!&lt;br&gt;&lt;br&gt;I've poured my heart and soul -- not to mention two years of my life -- into making this the best-sounding album it can possibly be! And today, you get to hear the fruits of that effort! &lt;br&gt;&lt;br&gt;Releases on &lt;strong&gt;Spotify&lt;/strong&gt;, &lt;strong&gt;Apple Music&lt;/strong&gt;, &lt;strong&gt;Shazam&lt;/strong&gt;, and many other stores is planned for very soon! So watch this space!&lt;br&gt;&lt;br&gt;Get out there and show your support by &lt;a href="https://lyjia.bandcamp.com/album/listen-ep"&gt;picking up a copy of Listen EP today&lt;/a&gt;! &lt;strong&gt;It's only&lt;/strong&gt; &lt;strong&gt;five bucks!&lt;/strong&gt;&lt;/div&gt;

&lt;/div&gt;
</description>
      <pubDate>Wed, 10 Jan 2024 18:30:00 +0000</pubDate>
      <link>https://lyjia.us/blog/p/announcing-listen-ep</link>
      <guid>https://lyjia.us/blog/p/announcing-listen-ep</guid>
    </item>
    <item>
      <title>Surviving the Slashdot Effect: Caching Web Traffic with Rails and Cloudflare</title>
      <description>&lt;div class="trix-content"&gt;
  &lt;div&gt;So you've got a content-oriented website, maybe your own blog or something, and maybe you've (like me) decided to ignore the &lt;a href="https://news.ycombinator.com/item?id=37484322"&gt;advice&lt;/a&gt; about using static site generators. You build your Rails site, and it is wonderful and beautiful and &lt;em&gt;dynamic&lt;/em&gt;, and every page it replies with delights readers with your artisanally-crafted HTML. Maybe you've got some internal caching (&lt;a href="https://guides.rubyonrails.org/caching_with_rails.html"&gt;Rails has you covered here&lt;/a&gt;), maybe it's all roundtrips to the database. But who cares! Your site is up and receiving traffic!&lt;br&gt;&lt;br&gt;Then, suddenly, a storm hits. Congratulations! You've made it to the top of Reddit/Slashdot/Hacker News! You now have thousands, if not millions, of people beating down your door to read your content. &lt;strong&gt;But now your site is down! &lt;/strong&gt;The link's comment thread is filling up with complaints about being &lt;a href="https://en.wikipedia.org/wiki/Slashdot_effect"&gt;hugged to death&lt;/a&gt;, and a few helpful souls are posting the &lt;a href="https://archive.org/"&gt;archive.org&lt;/a&gt; equivalent of your link and siphoning away your traffic. &lt;br&gt;&lt;br&gt;&lt;strong&gt;How do we fix this?&lt;/strong&gt;&lt;br&gt;&lt;br&gt;You could throw more compute resources at it -- think "scaling vertically/horizontally" -- which I'm sure your server/application host would ab$olutely &lt;em&gt;love. &lt;br&gt;&lt;br&gt;Or, &lt;/em&gt;you could install some sort of proxy cache in front of it. The traditional answer here is to use &lt;a href="https://nginx.org/en/"&gt;nginx&lt;/a&gt; or &lt;a href="https://varnish-cache.org/"&gt;Varnish&lt;/a&gt; as a caching proxy, but if you use a content delivery network (such as &lt;a href="https://www.cloudflare.com/"&gt;Cloudflare&lt;/a&gt;) it may be better to use that CDN's caching features instead. (Some might recommend using both your own cache &lt;em&gt;and&lt;/em&gt; your CDN's cache, but I wouldn't advise this because troubleshooting cache issues is already difficult enough, and having multiple layers only makes debugging even more confusing. If you do this, you should understand your web application &lt;em&gt;thoroughly&lt;/em&gt;.) &lt;br&gt;&lt;br&gt;Since this site is fronted by Cloudflare, I want to make use of its page cache: it's &lt;strong&gt;free &lt;/strong&gt;and comes with the service!&lt;br&gt;&lt;br&gt;However, setting this up is not as simple as it may first appear: in a default configuration, Rails doesn't permit caching (the &lt;strong&gt;Cache-Control &lt;/strong&gt;headers it sends don't allow for it), and as a result, nearly every request you receive bypasses the cache and gets passed directly to the app server. This is a screenshot of my Cloudflare dashboard showing the percentage of page requests cached before I applied the fixes I describe here (those peaks top out at ~10%):&lt;br&gt;&lt;br&gt;&lt;action-text-attachment sgid="BAh7CEkiCGdpZAY6BkVUSSI3Z2lkOi8vcGxheWdyb3VuZC9BY3RpdmVTdG9yYWdlOjpCbG9iLzIxP2V4cGlyZXNfaW4GOwBUSSIMcHVycG9zZQY7AFRJIg9hdHRhY2hhYmxlBjsAVEkiD2V4cGlyZXNfYXQGOwBUMA==--ac7e591ac5d6eea1714397c39c0f95e5fcb0aa26" content-type="image/png" url="https://www.lyjia.us/rails/active_storage/blobs/redirect/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBHZz09IiwiZXhwIjpudWxsLCJwdXIiOiJibG9iX2lkIn19--16bfbb9f0dd70965c753f35f3877364097a77a1a/image.png" filename="image.png" filesize="14416" width="683" height="79" previewable="true" presentation="gallery" caption="Uh.... that's not very good!"&gt;&lt;figure class="attachment attachment--preview attachment--png"&gt;
    &lt;img src="https://lyjia.us/rails/active_storage/representations/redirect/eyJfcmFpbHMiOnsiZGF0YSI6MjEsInB1ciI6ImJsb2JfaWQifX0=--396b5d0e075ab94c82926b438bc01b6e2978636d/eyJfcmFpbHMiOnsiZGF0YSI6eyJmb3JtYXQiOiJwbmciLCJyZXNpemVfdG9fbGltaXQiOlsxMDI0LDc2OF19LCJwdXIiOiJ2YXJpYXRpb24ifX0=--199294c92961cfde7a884b276194a0c797db67ef/image.png"&gt;

  &lt;figcaption class="attachment__caption"&gt;
      Uh.... that's not very good!
  &lt;/figcaption&gt;
&lt;/figure&gt;&lt;/action-text-attachment&gt;&lt;br&gt;&lt;br&gt;Now, you can set up rules in the Cloudflare dashboard to override Rails' requested caching behavior, but this does not solve the underlying root cause: Rails is requesting no caching, because the &lt;strong&gt;Cache-Control&lt;/strong&gt; request header it sets explicitly forbids it:&lt;br&gt;&lt;br&gt;&lt;action-text-attachment sgid="BAh7CEkiCGdpZAY6BkVUSSI3Z2lkOi8vcGxheWdyb3VuZC9BY3RpdmVTdG9yYWdlOjpCbG9iLzI1P2V4cGlyZXNfaW4GOwBUSSIMcHVycG9zZQY7AFRJIg9hdHRhY2hhYmxlBjsAVEkiD2V4cGlyZXNfYXQGOwBUMA==--fa083f9f1f1a1e81b720236170d25872c6e75d7b" content-type="image/png" url="https://www.lyjia.us/rails/active_storage/blobs/redirect/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBIZz09IiwiZXhwIjpudWxsLCJwdXIiOiJibG9iX2lkIn19--32c034be6fdbf58eb5a7a2d233f4a2d440bba4cd/image.png" filename="image.png" filesize="5328" width="293" height="63" previewable="true" presentation="gallery" caption="Cache-Control: NO CACHE!"&gt;&lt;figure class="attachment attachment--preview attachment--png"&gt;
    &lt;img src="https://lyjia.us/rails/active_storage/representations/redirect/eyJfcmFpbHMiOnsiZGF0YSI6MjUsInB1ciI6ImJsb2JfaWQifX0=--1a631bace6495069ac8e23bf6e2e06c0d4bea5ae/eyJfcmFpbHMiOnsiZGF0YSI6eyJmb3JtYXQiOiJwbmciLCJyZXNpemVfdG9fbGltaXQiOlsxMDI0LDc2OF19LCJwdXIiOiJ2YXJpYXRpb24ifX0=--199294c92961cfde7a884b276194a0c797db67ef/image.png"&gt;

  &lt;figcaption class="attachment__caption"&gt;
      Cache-Control: NO CACHE!
  &lt;/figcaption&gt;
&lt;/figure&gt;&lt;/action-text-attachment&gt;&lt;br&gt;&lt;br&gt;&lt;strong&gt;Setting the Correct Cache-Control Headers with Rails&lt;br&gt;&lt;/strong&gt;&lt;br&gt;&lt;/div&gt;&lt;blockquote&gt;&lt;strong&gt;NOTE:&lt;/strong&gt; The directions given here apply to Ruby on Rails version 7, though &lt;strong&gt;expires_in &lt;/strong&gt;and &lt;strong&gt;fresh_when &lt;/strong&gt;have existed since at least version 2, and concerns have been available since version 4.&lt;/blockquote&gt;&lt;div&gt;&lt;strong&gt;&lt;br&gt;&lt;/strong&gt;Luckily, Rails makes changing this behavior fairly simple. We don't even need to really dive into how &lt;strong&gt;Cache-Control&lt;/strong&gt; works! (Though &lt;a href="https://www.cloudflare.com/learning/cdn/glossary/what-is-cache-control/"&gt;here is a good guide&lt;/a&gt; if you want to know.) You simply call the &lt;strong&gt;expires_in&lt;/strong&gt; and/or &lt;strong&gt;fresh_when &lt;/strong&gt;functions in your controller, supplying an expiry and ensuring that you set &lt;strong&gt;public &lt;/strong&gt;to &lt;strong&gt;true&lt;/strong&gt;. Like this:&lt;br&gt;&lt;br&gt;&lt;/div&gt;&lt;pre&gt;expires_in 1.hour, public: true
# or
fresh_when(@article, public: true)&lt;/pre&gt;&lt;div&gt;&lt;br&gt;However, setting this for every route is both tedious and a pretty egregious violation of &lt;a href="https://www.digitalocean.com/community/tutorials/what-is-dry-development"&gt;DRY&lt;/a&gt;. Instead, we can set as much as we can once and then propagate it through our application using either &lt;a href="https://www.rubyguides.com/2019/01/what-is-inheritance-in-ruby/"&gt;class inheritance&lt;/a&gt; or &lt;a href="https://riptutorial.com/ruby/example/17461/modules-and-class-composition"&gt;composition&lt;/a&gt; (via ActiveSupport's &lt;a href="https://api.rubyonrails.org/classes/ActiveSupport/Concern.html"&gt;concerns&lt;/a&gt;) feature. And while inheritance may be slightly easier, composition is a bit more modern and flexible; here we will be taking the latter approach.&lt;br&gt;&lt;br&gt;To start, we will want to make a new concern and call it "&lt;strong&gt;Cacheable&lt;/strong&gt;". The easiest way to do this is to simply go to the &lt;strong&gt;$RAILS_ROOT/app/controllers/concerns&lt;/strong&gt; folder and create a new file, naming it &lt;strong&gt;cacheable.rb&lt;/strong&gt;. In this file, we want to make one small action (called "&lt;strong&gt;set_cache_headers&lt;/strong&gt;") and call &lt;strong&gt;expires_in&lt;/strong&gt; within it. Here is a very basic and usable example, which also prevents page caching when a user is logged in:&lt;br&gt;&lt;br&gt;&lt;/div&gt;&lt;pre&gt;# app/controllers/concerns/cacheable.rb
module Cacheable
  extend ActiveSupport::Concern

  included do
    before_action :set_cache_headers, unless: current_user
  end

  def set_cache_headers
    expires_in 1.hour, public: true
  end

end&lt;/pre&gt;&lt;div&gt;&lt;br&gt;Then, for each controller whose content you wish to cache, simply add "&lt;strong&gt;include Cacheable&lt;/strong&gt;" at the top right below the class declaration. Here is an example pulled directly from this site's code, for the controller that powers the "About" feature:&lt;br&gt;&lt;br&gt;&lt;/div&gt;&lt;pre&gt;# app/controllers/static_controller.rb
class StaticController &amp;lt; ApplicationController
  include Cacheable
  def about
    @page_title = "About"
  end
end&lt;/pre&gt;&lt;div&gt;&lt;br&gt;Once this is done you will see that &lt;strong&gt;Cache-Control&lt;/strong&gt; is indeed being set correctly:&lt;br&gt;&lt;br&gt;&lt;action-text-attachment sgid="BAh7CEkiCGdpZAY6BkVUSSI3Z2lkOi8vcGxheWdyb3VuZC9BY3RpdmVTdG9yYWdlOjpCbG9iLzIyP2V4cGlyZXNfaW4GOwBUSSIMcHVycG9zZQY7AFRJIg9hdHRhY2hhYmxlBjsAVEkiD2V4cGlyZXNfYXQGOwBUMA==--1166b414786cb9d5a1c7d52472e62c2b8981c419" content-type="image/png" url="https://www.lyjia.us/rails/active_storage/blobs/redirect/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBHdz09IiwiZXhwIjpudWxsLCJwdXIiOiJibG9iX2lkIn19--e891f701622deb851ee0f02fe0a779c0c3b617ae/image.png" filename="image.png" filesize="4826" width="233" height="71" previewable="true" presentation="gallery" caption="Objective achieved!"&gt;&lt;figure class="attachment attachment--preview attachment--png"&gt;
    &lt;img src="https://lyjia.us/rails/active_storage/representations/redirect/eyJfcmFpbHMiOnsiZGF0YSI6MjIsInB1ciI6ImJsb2JfaWQifX0=--31879a29d5cdfe6c74ce452faefc2e0e2d02b500/eyJfcmFpbHMiOnsiZGF0YSI6eyJmb3JtYXQiOiJwbmciLCJyZXNpemVfdG9fbGltaXQiOlsxMDI0LDc2OF19LCJwdXIiOiJ2YXJpYXRpb24ifX0=--199294c92961cfde7a884b276194a0c797db67ef/image.png"&gt;

  &lt;figcaption class="attachment__caption"&gt;
      Objective achieved!
  &lt;/figcaption&gt;
&lt;/figure&gt;&lt;/action-text-attachment&gt;&lt;br&gt;&lt;br&gt;But! You are not finished yet! You may notice that while your 'cached' stats are going up, they aren't going up as much as one might think. This is because there is another component to page caching that we have not yet discussed: &lt;strong&gt;etags.&lt;br&gt;&lt;br&gt;Setting the Correct Etag Headers with Rails&lt;br&gt;&lt;br&gt;&lt;/strong&gt;This is where things get a bit more tricky: Rails generates another header, called an &lt;strong&gt;Etag&lt;/strong&gt;, that, in theory, is supposed to be unique for each page.&amp;nbsp; (For the more technically inclined, you can think of an Etag as like a SHA256 hash for your page.) But Rails, by default, makes this tag unique per request. Both your browser cache and your CDN cache read this header to determine whether a given request is a cache hit or cache miss, and so we will need to configure Rails' to set it correctly, based on our rendered page content (or other context).&lt;br&gt;&lt;br&gt;Enter&lt;strong&gt; &lt;/strong&gt;&lt;a href="https://api.rubyonrails.org/classes/ActionController/ConditionalGet.html#method-i-fresh_when"&gt;&lt;strong&gt;fresh_when&lt;/strong&gt;&lt;/a&gt;, which provides further direction to Rails on how to render the correct &lt;strong&gt;etag&lt;/strong&gt; header. You provide it with an object that describes what the page renders -- generally the model instance for the given page (the Rails docs use &lt;strong&gt;@article&lt;/strong&gt; in their examples) -- and it generates a hash that is used for the Etag header.&amp;nbsp;&lt;br&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;strong&gt;Using fresh_when with Dynamic Pages&lt;/strong&gt;&lt;/div&gt;&lt;div&gt;&lt;strong&gt;&lt;br&gt;&lt;/strong&gt;For dynamic pages backed by a model instance, such as the blog post example described above and in the Rails docs, simply call &lt;strong&gt;fresh_when&lt;/strong&gt; and pass it your model instance plus a value for &lt;strong&gt;public&lt;/strong&gt;, inside the controller route. Like so:&lt;br&gt;&lt;br&gt;&lt;/div&gt;&lt;pre&gt;# app/controllers/articles_controller.rb
def show_article
  fresh_when @article, public: !current_user
end&lt;/pre&gt;&lt;div&gt;&lt;br&gt;When combined with aforementioned &lt;strong&gt;Cacheable&lt;/strong&gt;, this is sufficient to avoid page-caching in the case of logged-in users, as the expires_in directive is never called when &lt;strong&gt;current_user&lt;/strong&gt; exists, and Rails reverts to its default, zero-expiry "private" cache behavior.&lt;br&gt;&lt;br&gt;If you &lt;em&gt;aren't&lt;/em&gt; using &lt;strong&gt;Cacheable&lt;/strong&gt;, as described above, you will need to &lt;a href="https://api.rubyonrails.org/classes/ActionController/ConditionalGet.html#method-i-fresh_when"&gt;consult the documentation&lt;/a&gt; as you need to provide additional information.&lt;br&gt;&lt;br&gt;&lt;strong&gt;Using fresh_when with Collections or ActiveRecord Relations&lt;br&gt;&lt;br&gt;&lt;/strong&gt;If your first parameter to fresh_when is an &lt;a href="https://api.rubyonrails.org/classes/ActiveRecord/Relation.html"&gt;ActiveRecord relation&lt;/a&gt; (such as what is returned with a &lt;strong&gt;.where()&lt;/strong&gt; call), passing the relation object will cause the entire collection it represents to be loaded into memory. For other kinds of collections, you will again be feeding it the entire array. &lt;strong&gt;This is not ideal! &lt;br&gt;&lt;br&gt;&lt;/strong&gt;Instead, you will want to pass it a single value representing the freshness of that collection, such as the most recent &lt;strong&gt;updated_at &lt;/strong&gt;value. For ActiveRecord relations, that can be retrieved with something like &lt;strong&gt;@relation.maximum(:updated_at)&lt;/strong&gt;.&lt;br&gt;&lt;br&gt;&lt;strong&gt;Using fresh_when with Static Pages&lt;br&gt;&lt;br&gt;&lt;/strong&gt;Static pages are a little bit simpler, as all we really need to include in the etag is &lt;strong&gt;controller_name&lt;/strong&gt; and &lt;strong&gt;action_name&lt;/strong&gt;, as well as something to identify the logged-in user. &lt;br&gt;&lt;br&gt;So, we craft another concern, this time called &lt;strong&gt;StaticCacheable,&lt;/strong&gt; and include this in each controller serving static content. Once again, like &lt;strong&gt;Cacheable&lt;/strong&gt;, this is a controller-level solution; if you need something per-action that is an exercise left up to you.&lt;br&gt;&lt;br&gt;To make this concern, create a new file called &lt;strong&gt;static_cacheable.rb&lt;/strong&gt; and save it to your &lt;strong&gt;$RAILS_ROOT/app/controllers/concerns&lt;/strong&gt; folder, right next to &lt;strong&gt;cacheable.rb&lt;/strong&gt;. Note that we will include a reference to &lt;strong&gt;Cacheable &lt;/strong&gt;from within &lt;strong&gt;StaticCacheable&lt;/strong&gt;, so that you only need to include &lt;strong&gt;StaticCacheable &lt;/strong&gt;on your static controllers. In the action it defined we simply grab the controller name, action name, and current user status and feed that into fresh_when:&lt;br&gt;&lt;br&gt;&lt;/div&gt;&lt;pre&gt;# app/controllers/concerns/static_cacheable.rb
module &lt;em&gt;StaticCacheable
  &lt;/em&gt;extend ActiveSupport::Concern

  included do
    include Cacheable
    before_action :set_static_cache_headers, unless: -&amp;gt; { :current_user || Rails.env.development? } #dont do this during development
  end

  def set_static_cache_headers
    &lt;em&gt;# Use Rails built-in asset tracking which automatically includes asset versions
    # The template option ensures ETags update when views change
    &lt;/em&gt;fresh_when(
      etag: [controller_name, action_name, current_user&amp;amp;.email],
      template: action_name,
      public: !current_user
    )
  end
end&lt;/pre&gt;&lt;div&gt;&lt;br&gt;&lt;strong&gt;Finally!&lt;br&gt;&lt;br&gt;&lt;/strong&gt;Once your &lt;strong&gt;Cache-Control&lt;/strong&gt; and &lt;strong&gt;Etag&lt;/strong&gt; headers are under control and correctly set, and you have correctly configured your proxy service, your site should be well-equipped to handle large volumes of traffic without falling over. Hurray!&lt;br&gt;&lt;br&gt;&lt;strong&gt;A Quick Note About Cloudflare and Implementing This&lt;/strong&gt;&lt;br&gt;&lt;br&gt;It's worth noting that Cloudflare seems to strip the &lt;strong&gt;Etag &lt;/strong&gt;header when &lt;strong&gt;Cache-Control&lt;/strong&gt; renders it useless, as is the case when &lt;strong&gt;Cache-Control&lt;/strong&gt; is set to private. This may seem annoying but it punches out your browser cache, presumably to ease troubleshooting. &lt;br&gt;&lt;br&gt;You can still see the &lt;strong&gt;Etag &lt;/strong&gt;header if you pull requests directly from your webserver (by its IP address), and it will also be visible during development. Unfortunately, it seems like you will have to rely on unit tests or Cloudflare's provided statistics to verify your cache strategy is working.&lt;/div&gt;

&lt;/div&gt;
</description>
      <pubDate>Tue, 12 Sep 2023 22:40:43 +0000</pubDate>
      <link>https://lyjia.us/blog/p/surviving-the-slashdot-effect</link>
      <guid>https://lyjia.us/blog/p/surviving-the-slashdot-effect</guid>
    </item>
    <item>
      <title>How To Set Your Windows Taskbar Application Icon with pyQT/pySide</title>
      <description>&lt;div class="trix-content"&gt;
  &lt;div&gt;If you are writing a Windows-only or cross-platform application using pyQT or pySide (version 6, as of this time of writing), you may discover that you need to change your application's icon, as it appears in the taskbar. By default, it is a boring, generic "window" icon, and, naturally, you will want to change it!&lt;br&gt;&lt;br&gt;But the instructions for doing so aren't very clear. This &lt;a href="https://stackoverflow.com/questions/17068003/application-icon-in-pyside-gui"&gt;StackOverflow post&lt;/a&gt; gets us started, but the given (and accepted) solutions seem to advise the unnecessary extra step of saving a copy of the icon as a Python variable itself!&lt;br&gt;&lt;br&gt;There is a simpler way, and it involves just a few lines of code. Inside the &lt;strong&gt;__init__()&lt;/strong&gt; function of your QT window's code, add the following:&lt;br&gt;&lt;br&gt;&lt;/div&gt;&lt;pre&gt;from pathlib import Path
from PySide6.QtGui import QPixmap, QIcon #substitute the equivalent pyQT line if you're using pyQT

# load window icon
path_to_icon = 'res/icon.png' #the path to your icon file, relative to the project root
pixmap = QPixmap()
pixmap.loadFromData( Path( path_to_icon ).read_bytes() )
appIcon = QIcon(pixmap)
self.setWindowIcon(appIcon)&lt;/pre&gt;&lt;div&gt;&lt;br&gt;And &lt;em&gt;voilà&lt;/em&gt;! You now have a pretty icon in the taskbar for your program.&lt;/div&gt;

&lt;/div&gt;
</description>
      <pubDate>Mon, 12 Jun 2023 21:30:19 +0000</pubDate>
      <link>https://lyjia.us/blog/p/how-to-set-win-taskbar-icon-with-pyqt-pyside</link>
      <guid>https://lyjia.us/blog/p/how-to-set-win-taskbar-icon-with-pyqt-pyside</guid>
    </item>
    <item>
      <title>Introducing... My Photography as Wallpapers!</title>
      <description>&lt;div class="trix-content"&gt;
  &lt;div&gt;There's a new "Photography" section on this site, and I want to highlight something I've been doing for years that has kind of flown under the radar: for a while, I maintained some galleries on Flickr of my photography formatted for display as wallpapers for your devices. It was cool, but it kind of languished, lost in a sea of public Flickr galleries that few ever saw.&lt;br&gt;&lt;br&gt;So, I decided to integrate those galleries with this website. Now, with this site and blog in full swing, I can bring a bit more attention to it! Plus, the images have been recut and polished just a wee bit more! &lt;br&gt;&lt;br&gt;They're available from the Photography dropdown right above this post, or &lt;a href="https://www.lyjia.us/photography/downloads/desktop"&gt;straight from this hyperlink&lt;/a&gt;!&lt;br&gt;&lt;br&gt;These are some of my favorites:&lt;br&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;action-text-attachment sgid="BAh7CEkiCGdpZAY6BkVUSSI3Z2lkOi8vcGxheWdyb3VuZC9BY3RpdmVTdG9yYWdlOjpCbG9iLzE1P2V4cGlyZXNfaW4GOwBUSSIMcHVycG9zZQY7AFRJIg9hdHRhY2hhYmxlBjsAVEkiD2V4cGlyZXNfYXQGOwBUMA==--e5f147729142aec068cd0bb64950ab7a7e13b4f1" content-type="image/jpeg" url="https://www.lyjia.us/rails/active_storage/blobs/redirect/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBGQT09IiwiZXhwIjpudWxsLCJwdXIiOiJibG9iX2lkIn19--7e976b9be6fb7715db1c6c6f6cea8875f35518e5/52903676106_0f6f942dcb_c.jpg" filename="52903676106_0f6f942dcb_c.jpg" filesize="88279" width="800" height="450" previewable="true" presentation="gallery" caption="Sunset Cliffs Misty Seas"&gt;&lt;figure class="attachment attachment--preview attachment--jpg"&gt;
    &lt;img src="https://lyjia.us/rails/active_storage/representations/redirect/eyJfcmFpbHMiOnsiZGF0YSI6MTUsInB1ciI6ImJsb2JfaWQifX0=--495cbc6da12610b3a3b60408f7dc315c92a4abd4/eyJfcmFpbHMiOnsiZGF0YSI6eyJmb3JtYXQiOiJqcGciLCJyZXNpemVfdG9fbGltaXQiOlsxMDI0LDc2OF19LCJwdXIiOiJ2YXJpYXRpb24ifX0=--4582af97109930a13c086ad18f8d949801b376ef/52903676106_0f6f942dcb_c.jpg"&gt;

  &lt;figcaption class="attachment__caption"&gt;
      Sunset Cliffs Misty Seas
  &lt;/figcaption&gt;
&lt;/figure&gt;&lt;/action-text-attachment&gt;&lt;br&gt;&lt;br&gt;&lt;action-text-attachment sgid="BAh7CEkiCGdpZAY6BkVUSSI3Z2lkOi8vcGxheWdyb3VuZC9BY3RpdmVTdG9yYWdlOjpCbG9iLzE2P2V4cGlyZXNfaW4GOwBUSSIMcHVycG9zZQY7AFRJIg9hdHRhY2hhYmxlBjsAVEkiD2V4cGlyZXNfYXQGOwBUMA==--1f125306556eb8de7165e51b9f0e78607bdb0a6a" content-type="image/jpeg" url="https://www.lyjia.us/rails/active_storage/blobs/redirect/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBGUT09IiwiZXhwIjpudWxsLCJwdXIiOiJibG9iX2lkIn19--17d8af6db0ed50853671ff194c5200c93bf72cc1/52904053295_7cd70eb2e4_c.jpg" filename="52904053295_7cd70eb2e4_c.jpg" filesize="137650" width="800" height="450" previewable="true" presentation="gallery" caption="Oceanside Wave I"&gt;&lt;figure class="attachment attachment--preview attachment--jpg"&gt;
    &lt;img src="https://lyjia.us/rails/active_storage/representations/redirect/eyJfcmFpbHMiOnsiZGF0YSI6MTYsInB1ciI6ImJsb2JfaWQifX0=--e79a9edfa6c3d96b8a6f8e89e3e0ce03f4fe15b7/eyJfcmFpbHMiOnsiZGF0YSI6eyJmb3JtYXQiOiJqcGciLCJyZXNpemVfdG9fbGltaXQiOlsxMDI0LDc2OF19LCJwdXIiOiJ2YXJpYXRpb24ifX0=--4582af97109930a13c086ad18f8d949801b376ef/52904053295_7cd70eb2e4_c.jpg"&gt;

  &lt;figcaption class="attachment__caption"&gt;
      Oceanside Wave I
  &lt;/figcaption&gt;
&lt;/figure&gt;&lt;/action-text-attachment&gt;&lt;br&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;action-text-attachment sgid="BAh7CEkiCGdpZAY6BkVUSSI3Z2lkOi8vcGxheWdyb3VuZC9BY3RpdmVTdG9yYWdlOjpCbG9iLzE3P2V4cGlyZXNfaW4GOwBUSSIMcHVycG9zZQY7AFRJIg9hdHRhY2hhYmxlBjsAVEkiD2V4cGlyZXNfYXQGOwBUMA==--c5fd60989fd3a4a2012fc3d9b050b9fd64fa635c" content-type="image/jpeg" url="https://www.lyjia.us/rails/active_storage/blobs/redirect/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBGZz09IiwiZXhwIjpudWxsLCJwdXIiOiJibG9iX2lkIn19--43a7f04ab3f92bbeb839bbb0154693508c99b505/52904146033_288c62fe08_c.jpg" filename="52904146033_288c62fe08_c.jpg" filesize="107926" width="800" height="450" previewable="true" presentation="gallery" caption="Fiery Sunset from Ocean Beack"&gt;&lt;figure class="attachment attachment--preview attachment--jpg"&gt;
    &lt;img src="https://lyjia.us/rails/active_storage/representations/redirect/eyJfcmFpbHMiOnsiZGF0YSI6MTcsInB1ciI6ImJsb2JfaWQifX0=--85091788d20b47a6ef339bac3fc175b58bcb17c8/eyJfcmFpbHMiOnsiZGF0YSI6eyJmb3JtYXQiOiJqcGciLCJyZXNpemVfdG9fbGltaXQiOlsxMDI0LDc2OF19LCJwdXIiOiJ2YXJpYXRpb24ifX0=--4582af97109930a13c086ad18f8d949801b376ef/52904146033_288c62fe08_c.jpg"&gt;

  &lt;figcaption class="attachment__caption"&gt;
      Fiery Sunset from Ocean Beack
  &lt;/figcaption&gt;
&lt;/figure&gt;&lt;/action-text-attachment&gt;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;br&gt;&lt;/div&gt;

&lt;/div&gt;
</description>
      <pubDate>Tue, 23 May 2023 21:18:37 +0000</pubDate>
      <link>https://lyjia.us/blog/p/introducing-my-photography-as-wallpapers</link>
      <guid>https://lyjia.us/blog/p/introducing-my-photography-as-wallpapers</guid>
    </item>
  </channel>
</rss>
