<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:atom="http://www.w3.org/2005/Atom">
<channel>
<title>ewr.is</title>
<link>https://ewr.is/</link>
<description>Talk of life and technology with Eric Richardson.</description>
<atom:link href="https://ewr.is/feed" rel="self" type="application/rss+xml"/>

<item>
<title>Consumer Sports Tech Is Having Its Square Moment</title>
<guid>https://ewr.is/2026/01/1820-consumer-sports-tech-is-having-its-square/</guid>
<link>https://ewr.is/2026/01/1820-consumer-sports-tech-is-having-its-square/</link>
<dc:creator>Eric Richardson</dc:creator>
<description><![CDATA[<p>People used to ask if I had hobbies. My answer was no—I have kids. But with the boys now 9 and 11, I've picked up something I can justify as kid-adjacent: consumer sports tech, specifically baseball.</p>
<p>The youth sports industry is booming right now (in good and bad ways, but that's a different story), and throughout it you're seeing technology that was once inapproachably expensive become a part of home setups and how young athletes train.</p>
<p>The edges of this space feel very much like the world in which Square first released its &quot;little white card reader.&quot; Consumers suddenly had a computer in their pocket, and Jack and team saw the chance to use that to revolutionize how businesses could get paid.</p>
<p>s for me, we put a batting cage in the backyard, and hitting sessions now involve a Rapsodo Pro 2.0 launch monitor and a Blast Motion bat sensor, along with a couple phones and iPads. I couldn't help trying to make those pieces fit together.</p>
<p>For those not in the weeds of all of this, the <a href="https://rapsodo.com/pages/learn-more-about-pro-2">Rapsodo Pro 2.0</a> is an example of a launch monitor. It contains camera and radar to observe the flight of the baseball. For pitching, that gives you amazing data on release points, spin rates, and velocity. For batting, it tracks similar details as the ball leaves the bat after contact. This tech is much more common in the golf world, where launch monitors are at the core of simulator setups. It's the same idea for baseball. The launch monitor is observing exit velocity, exit direction, launch angle, spin characteristics, etc. These are the outputs of the baseball swing.</p>
<p>The <a href="https://store.blastmotion.com/store/products/baseball/">Blast Motion bat sensor</a> is a tiny device that you slip onto the knob of the bat. It is an &quot;inertial measurement unit,&quot; meaning that it can understand how it moves through space. Using clever math, it turns that into an understanding of the path of a baseball bat from the batting stance to and through the point of contact. From Blast Motion you get what I think of as the inputs to a baseball swing: bat speed, attack angle, rotational acceleration, and a variety of metrics on angles and planes.</p>
<p>My basic question starting out was how you could marry these input and output ideas to get a more complete view of a swing. Rapsodo and Blast both have apps and websites, so could I access both data sets and match them up to create one view? This also presented an excuse to dip my toe back into iOS development, first more manually and more recently with a heavy dose of AI.</p>
<div class='pf pfW'><img src="/ah/i/79c68b4dbe8ada6e5ac76780a384e663/4324-wide.jpg" alt="In a CageStats swing view, you can see how inputs connect to outputs, as well as use the data we&amp;#39;ve recorded to visualize a trajectory path for the ball. I let Claude learn the physics here, not me." /><div class='pfP'></div>
    In a CageStats swing view, you can see how inputs connect to outputs, as well as use the data we've recorded to visualize a trajectory path for the ball. I let Claude learn the physics here, not me.
  </div>
<p>This visual represents sort of the core of what I first set out to see: how does the input bat data connect to the output launch data? <a href="https://baseballsavant.mlb.com">MLB StatCast</a> gives a number of definitions that become useful here: the Ideal Attack Angle range is 5-20°; the Launch Angle Sweet-Spot is 8-32°. From a coaching perspective, the idea that you're trying to teach is that the best swing matches the trajectory of the incoming pitch, which will naturally be coming down some as it travels toward the plate. Matching that trajectory gives you the most time where the bat and the ball are on the same plane, and the most chance of direct solid contact on the hit.</p>
<p>Via the combined data, you can compute a really interesting metric, the Squared-Up Rate. From StatCast, this is &quot;How much exit velocity was obtained compared to the maximum possible exit velocity available, given the speed of the swing and pitch.&quot; So pitch speed (Rapsodo) and bat speed (Blast) give you a theoretical max exit velocity, and you can look at actual exit velocity (Rapsodo) to figure out what percentage of the potential was achieved on the swing.</p>
<p>As for the boys, they tolerate my interests. When they have friends over to hit they enjoy watching each others' calculated distances in the Rapsodo app. They nod respectfully when I show them session rates for attack and launch angles after the fact. They pay attention when I scrub through videos comparing how their hips fire on a good swing vs a great swing.</p>
<div class='pf pfW'><img src="/ah/i/bc61fbab1fd1102214f37d982ec9e6b6/4325-wide.jpg" alt="SwingScrubber is a little app I built to take a video of a hitting session, allow the easy tagging of swing contact points, and then a way to compare two synchronized swings." /><div class='pfP'></div>
    SwingScrubber is a little app I built to take a video of a hitting session, allow the easy tagging of swing contact points, and then a way to compare two synchronized swings.
  </div>
<p>Honestly, I pretend this is all for them, but I played through high school and can totally see myself joining an old-man league in a decade when we're empty nesters. Don't ask how many more swings I've taken out there compared to them.</p>
<p>This is obviously just a tiny taste of the data-based approach to training pioneered by folks like <a href="https://www.drivelinebaseball.com">Driveline</a>, but it's also very much in the vein of what they're doing. The Rapsodo is still a relatively expensive piece of kit, but it's not actually that crazy in a world where youth two-piece composite bats at $500 a pop are lining every dugout fence, and it's not going to be that long until you get the same data off of an iPhone.</p>
<p>Plenty of apps—including Blast's—are out there trying to use the iPhone camera to act as a launch monitor. They aren't nearly as good as the Rapsodo yet, but the sensors will continue to get better.</p>
<p>The real explosion in this space is around AI, doing automated analysis of video to produce coaching tips, cut highlight packages from game film, and so forth. You name it, someone's out playing with it right now. <a href="https://winreality.com/swing-ai/">SwingAI</a> from WinReality (who bought Blast Motion in 2025), is a great example, though personally I'm not sure I'll ever be able to convince it that my Torso Load is great enough.</p>
<p>WinReality's <a href="https://winreality.com/train-vr/">TrainVR</a> is also an amazing example of what becomes possible as VR headsets advance. Right now the Meta Quest 3s it uses is ok, not great, but I love the idea that this can give my 11 year old a way to train the recognition of what a curveball looks like before he'll ever see a high quantity of them in a game.</p>
<p>So if I start posting here about sports tech, it's not that I've quit moving money as a day job. I've just picked up a hobby, and am so interested to see where this moment takes us and what kind of innovation comes next.</p>]]</description>
<pubDate>Sat, 31 Jan 2026 17:55:47 -0500</pubDate>
</item>

<item>
<title>Refreshed</title>
<guid>https://ewr.is/2022/03/1818-refreshed/</guid>
<link>https://ewr.is/2022/03/1818-refreshed/</link>
<dc:creator>Eric Richardson</dc:creator>
<description><![CDATA[<p>As <a href="/2022/01/1817-its-been-a-bit">mentioned in the last post</a>, the bones of this site are a little creaky. It's running on Rails 3.2.15, which came out in 2013, stuck on Ruby 2.1.5, which came out in 2014. It's spent the last eight years running on an m1.small instance in AWS, provisioned using Chef cookbooks that <a href="/2013/11/1812-making-recipes">were neat when I wrote them in 2013</a>, but haven't had a thing done to them since.</p>
<p>I didn't even have a good way to deploy an update to change the sidebar to show that I've spent the last six years working for Square and Cash App rather than public radio.</p>
<p>Definitely time for a refresh. I'd been messing around with using a Docker container to get my ancient Ruby running on this M1 iMac, so I decided to shoot for ECS (Elastic Container Service), using Amazon-provisioned database and Redis.</p>
<p>Fair note here: All of this is overkill for this blog, which should really be hosted as a static site somewhere. We'll cross that bridge later. I figured getting it running correctly was step one to doing any other changes. Also, as you can see in the screen above, Amazon is getting set to retire the setup my old instance was running in and therefore putting a cap on how long I can be lazy.</p>
<p>I was pretty lost on exactly where the industry had moved here going in. I knew I could get the app running in a Docker container and that would take care of a lot of my environment issues, but I've spend the last six years just deploying apps onto the tooling that platform teams give me, not trying to keep up with developments in AWS.</p>
<p>I knew <a href="https://aws.amazon.com/ecs/">ECS</a> was a way to run containers, but also saw mentions of Fargate and didn't understand the difference. TLDR: Fargate is just a way to deploy services into ECS without having to know about the EC2 instance underneath.</p>
<p>I struggled for a while to just get a handle on which AWS pieces I was going to need. This blog is a Rails app with a MySQL database and a Redis instance for caching. Traditionally it used a local filesystem to store image assets.</p>
<p>There are lots of &quot;how to deploy a Rails app in ECS&quot; blog posts, but <a href="https://dev.to/raphael_jambalos/deploy-rails-in-amazon-ecs-part-1-concepts-26nl">this series of posts</a> probably helped me walk through the concepts the best.</p>
<p>I needed to:</p>
<ol>
<li>Build my image</li>
<li>Get it to a container registry</li>
<li>Have a database</li>
<li>Build out an ECS cluster with a task using my container</li>
<li>Have a load balancer point to the containers</li>
<li>Lots of other stuff</li>
</ol>
<p>On the application side, I needed to get off that local storage and get assets hosted on S3 instead. Conveniently, I had apparently started that effort years ago and had an S3 bucket with a bunch of image files in it. Inconveniently, I had left it half-done and without any real notes on what was or wasn't working. Images on this site use an engine called AssetHost, which I wrote at SCPR and <a href="https://github.com/assethost/assethost">released as open source on Github</a>. In my second stint at SCPR I had ported AssetHost to use S3 for a backend (we actually used a locally-hosted S3-compatible interface through Riak CS, but that's a different story), but hadn't ever fully merged that back to the main repo.</p>
<p>In the end, it's debatable whether any of this pre-work was actually helpful: it left me thinking these changes were closer to complete than they were, which lured me away from just making the smallest possible updates to get things running.</p>
<p>I couldn't ever figure out how to get Ruby 2.1.5 to build on M1 and/or modern OSX, but I used Docker Compose to get the app running in a container by taking Ubuntu 14.4 and compiling Ruby 2.1.5 inside of it. I built my own, but my Dockerfile for the ruby container looked <a href="https://hub.docker.com/r/ashchan/ruby-2.1.5/dockerfile">very similar to this one</a>.</p>
<p>My eventual Dockerfile for the app looked like this:</p>
<pre tabindex="0"><code>FROM ewr/ruby-2.1.5:0.0.1
RUN mkdir -p /app
RUN mkdir -p /rails
WORKDIR /app
COPY Gemfile Gemfile.lock ./
RUN bundle install --jobs 20 --retry 5
RUN mkdir -p /puma/pids
RUN mkdir -p /puma/sockets
COPY . ./
RUN RAILS_ENV=production DATABASE_URL=nulldb://user:pass@127.0.0.1/dbname bundle exec rake assets:precompile
EXPOSE 3000
CMD [&#34;bundle&#34;, &#34;exec&#34;, &#34;puma&#34;, &#34;-C&#34;, &#34;config/docker_puma.rb&#34;, &#34;-p&#34;, &#34;3000&#34;]
</code></pre><p>Note on the asset compilation: In Rails 3 you could just add <code>config.assets.initialize_on_precompile = false</code> to not load the Rails environment when compiling. That didn't work for me, because a piece of the AssetHost javascript wants to know where the AssetHost engine is mounted to hardcode some paths. I needed the app to start up, but without any DB at this stage in the build. <a href="https://github.com/nulldb/nulldb">nulldb</a> let me get there. Later I <em>think</em> I refactored away the need to initialize the app entirely, but I haven't revisited the step yet.</p>
<p>Eventually I got the app running in Docker, using s3 for asset storage, and everything was looking up.</p>
<p>Then I had to figure out the AWS stuff.</p>
<p>I made a first pass in February just clicking buttons in the AWS console, following the blog posts I linked above. They helped me understand some pieces, but I didn't get anything working.</p>
<p>This month, in the effort that finally stuck, I started from <a href="https://section411.com/2019/07/hello-world/">this Fargate / Terraform tutorial</a> and actually got somewhere.</p>
<p>I've <a href="https://github.com/ewr/ewr-terraform">stuck my terraform code up on Github</a>. I extended the tutorial to add RDS, Redis, a worker task, and some Route 53 bits around domain validation for the SSL certificate.</p>
<p>So what's not done yet?</p>
<p>I have no clue how health checks work, and am probably doing wrong things there.</p>
<p>I still have to manually build images and trigger a deploy. I need to hook into something from Github.</p>
<p>There's no reason a worker should be hanging around all the time for the once every few weeks I upload an image here... That worker should be a lambda, and my Resque queue should just be SQS.</p>
<p>I should stick Cloudflare or something in front of the images, rather than serving them directly.</p>
<p>And, as mentioned before, this should all just be statically generated.</p>
<p>But still... Progress, right?</p>]]</description>
<pubDate>Wed, 30 Mar 2022 11:08:54 -0400</pubDate>
</item>

<item>
<title>It&#39;s been a bit...</title>
<guid>https://ewr.is/2022/01/1817-its-been-a-bit/</guid>
<link>https://ewr.is/2022/01/1817-its-been-a-bit/</link>
<dc:creator>Eric Richardson</dc:creator>
<description><![CDATA[<p>After thoroughly ignoring this site for the last six and a half years, I'm going to try to get back into a slightly better flow of posting something every now and again.</p>
<p>A lot has happened in between my last posts in 2015 and now. The <a href="/2014/10/1813-new-chapters">kid who had just been born</a> in this 2014 post is now seven and in first grade. He's got a brother, Brent, who is five. We've moved houses twice, and since early 2016 I've been working for <a href="https://squareup.com/us/en">Square</a>, where I took a job to do Ruby code and somehow quickly ended up in payments. Turns out, it's a fun space, and six years later I'm still learning it as I help teams on <a href="https://cash.app">Cash App</a>'s Financial Platform figure out how to move money around the world.</p>
<p>I've been loosely thinking about bringing this site back to life since mid-2021, when I picked up an M1 iMac as my first non-work computer in a bit. My 2012 Macbook Pro had gotten claimed by Brent when the boys suddenly needed technology for virtual school in the fall of 2020. Copying things over to the new machine made me a bit nostalgic, so I figured I would just get the old code for this site running and pick off some changes that had been on my todo list for a decade or so...</p>
<p>Turns out, trying to resurrect old Ruby code on a different architecture isn't straight-forward. This blog is running a version of Ruby on Rails that came out in mid-2013, running on a Ruby version that came out in late 2014. The Chef cookbooks to provision the AWS EC2 instance I'm hosting this on were put together in late 2013.</p>
<p>It's a bit amazing that it all still works in production, but also kind of terrifying to get working again to migrate to something more modern. My goal would be to switch the blog content here over to a static site generator, but I still want the nicety of a web UI for composing and for uploading image assets. I've debated between just starting from the database content and trying to adapt it to some more modern platform, or trying to get the current Rails code up to date and then just adding a static output generator. I haven't gotten far on either approach yet, so TBD.</p>
<p>Step one: figure out how to deploy some slightly updated templates to fix my bio and some copyright dates.</p>
<p>Somewhere along the way I want to do the same thing to blogdowntown, the site I ran for years about Downtown Los Angeles. The site belongs to Southern California Public Radio today, but it's fallen off their radar and hasn't been online for the last few years. I have a database dump and I think enough image assets to restore most of the archives, but I just need to get enough code running to figure out a game plan.</p>
<p>So, sometime in the next six years or so?</p>]]</description>
<pubDate>Sun, 16 Jan 2022 15:21:40 -0500</pubDate>
</item>

<item>
<title>Policy Routing for Xfinity Traffic on a Dual-WAN Unifi Gateway</title>
<guid>https://ewr.is/2022/01/1816-policy-routing-for-xfinity-traffic-on-a-dualwan/</guid>
<link>https://ewr.is/2022/01/1816-policy-routing-for-xfinity-traffic-on-a-dualwan/</link>
<dc:creator>Eric Richardson</dc:creator>
<description><![CDATA[<p>Since mid-2019 I've had redundant Internet connections here at the house, using AT&amp;T Fiber as our primary link but keeping a Comcast connection active just in case of outages. For a long time it was just set up warm, where I would need to go down to the basement and switch a cable to switch links. Somewhere along the way a coworker pointed out that my Unifi Security Gateway (USG) supported Dual-WAN failover, and I plugged them both in.</p>
<p>That all works great, but there's been one nagging case I've wanted to clean up: We do use Comcast for television, and I like their Xfinity Stream app for listening to news while I'm getting ready in the morning. Because of using the AT&amp;T Fiber link, though, the Xfinity app thinks I'm out-of-home, which limits my ability to watch in-progress recordings.</p>
<p>To fix that, I need to set the USG to route Xfinity traffic over the Comcast link, but leave other traffic doing its normal failover setup that prefers AT&amp;T. That's not an especially hard ask, but it's been sitting on the todo list for a while.</p>
<p>When we moved in mid-2019 I turned on Comcast internet because it was the quickest to get going, but then quickly moved to using AT&amp;T Fiber for our primary connection. I kept the Comcast internet active, though, because in this day and age redundant Internet connections seems</p>
<p><em>Update (2025-01-04):</em> I've since switched to using DirecTV Stream for TV, but Irv emailed to note that Comcast has changed its IP detection ranges. He suggested adding 69.252.0.0 - 69.252.127.255 and 68.87.64.0 - 68.87.127.255, so I've updated the directions below. I also in the time since this post switched to a Gateway Pro, which has a Policy-Based Routing UI. Not sure whether that's for all the hardware now...</p>
<p><em>Original post continues:</em></p>
<p>Full confession: I actually did this once already. I followed various online tutorials and set up the firewall rule on the router, which worked great. But with how Unifi works, making hot updates on the running gateway only applies until a reboot. I didn't take the final step of configuring it all to be saved for future provisioning, so along the way my changes disappeared and it took me a while to come back to them and do it correctly.</p>
<p>I guess that's part of why I'm writing this up... If I ever lose my config again, this can be a guide for my future self to get it set back up.</p>
<p>Also, how useful is it really to have a dedicated backup internet connection? 99% of days it sits there entirely unused, but it has saved me a handful of times. Just this month AT&amp;T went out for most of a day at a time where virtual school was going on. It was great to realize that a bit into the outage when AT&amp;T texted me, having not even noticed the actual point of outage as the connection flipped over. I get paid good money to do work on a computer. Paying $50/mon as a backup seems justified.</p>
<h2 id="the-goal">The Goal</h2>
<p>When you're using the Xfinity Stream app or the web interface, the logic does a check to see whether you're at home. The web app, for instance, hits <code>https://xtvapi.cloudtv.comcast.net/inhome-restrictions/</code>. When you're out-of-home (my AT&amp;T connection), you get this:</p>
<pre tabindex="0"><code>{
    &#34;_type&#34;: &#34;InHomeStatusRestrictions&#34;,
    &#34;_links&#34;: {
        &#34;self&#34;: {
            &#34;href&#34;: &#34;../inhome-restrictions/&#34;
        }
    },
    &#34;inHomeStatus&#34;: &#34;out-of-home&#34;,
    &#34;currentRestrictions&#34;: [
        &#34;out-of-home&#34;
    ]
}
</code></pre><p>That causes the app to refuse to play certain content where licensing doesn't allow out-of-home streaming.</p>
<p>Could you man-in-the-middle that request somehow and just remove the restrictions? Maybe? I didn't try. Since I've got the Comcast connection, I just want to send Xfinity requests there. I have no clue if other parts of the stack enforce similar in-home constraints.</p>
<p>The basic idea here is to use &quot;policy-based routing&quot; based on the destination IPs, and target the correct interface on the USG accordingly.</p>
<h2 id="step-1-what-ips-are-we-routing">Step 1: What IPs are we routing?</h2>
<p>So what IPs do we need to route? If we run <code>dig</code> we see that there are a number of IP addresses and CNAMEs behind the hostname.</p>
<pre tabindex="0"><code>~: dig xtvapi.cloudtv.comcast.net

; &lt;&lt;&gt;&gt; DiG 9.10.6 &lt;&lt;&gt;&gt; xtvapi.cloudtv.comcast.net
;; global options: +cmd
;; Got answer:
;; -&gt;&gt;HEADER&lt;&lt;- opcode: QUERY, status: NOERROR, id: 40875
;; flags: qr rd ra; QUERY: 1, ANSWER: 12, AUTHORITY: 0, ADDITIONAL: 1

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 512
;; QUESTION SECTION:
;xtvapi.cloudtv.comcast.net.    IN      A

;; ANSWER SECTION:
xtvapi.cloudtv.comcast.net. 73  IN      CNAME   xfinityapi-production.r53.aae.comcast.net.
xfinityapi-production.r53.aae.comcast.net. 45 IN CNAME xfinityapi-tango-production-active.r53.aae.comcast.net.
xfinityapi-tango-production-active.r53.aae.comcast.net. 45 IN CNAME xfinityapi-tango-production-aws-us-east-2-active.r53.aae.comcast.net.
xfinityapi-tango-production-aws-us-east-2-active.r53.aae.comcast.net. 45 IN CNAME xfinityapi-tango-production-aws-us-east-2-green.r53.aae.comcast.net.
xfinityapi-tango-production-aws-us-east-2-green.r53.aae.comcast.net. 45 IN A 96.115.218.47
xfinityapi-tango-production-aws-us-east-2-green.r53.aae.comcast.net. 45 IN A 96.115.218.162
xfinityapi-tango-production-aws-us-east-2-green.r53.aae.comcast.net. 45 IN A 96.115.218.250
xfinityapi-tango-production-aws-us-east-2-green.r53.aae.comcast.net. 45 IN A 96.115.218.32
xfinityapi-tango-production-aws-us-east-2-green.r53.aae.comcast.net. 45 IN A 96.115.219.99
xfinityapi-tango-production-aws-us-east-2-green.r53.aae.comcast.net. 45 IN A 96.115.218.180
xfinityapi-tango-production-aws-us-east-2-green.r53.aae.comcast.net. 45 IN A 96.115.218.223
xfinityapi-tango-production-aws-us-east-2-green.r53.aae.comcast.net. 45 IN A 96.115.218.68

;; Query time: 18 msec
;; SERVER: 192.168.1.1#53(192.168.1.1)
;; WHEN: Mon Jan 03 14:52:24 EST 2022
;; MSG SIZE  rcvd: 401
</code></pre><p>Instead of trying to target as few IPs as possible, my goal was to just end up with a range of Comcast IPs that was unlikely to break as they changed internal infrastructure. To arrive at that, I ran a <code>whois 96.115.218.47</code>. That gave us a good set of ranges:</p>
<pre tabindex="0"><code># whois.arin.net

NetRange:       96.64.0.0 - 96.124.255.255
CIDR:           96.120.0.0/14, 96.112.0.0/13, 96.124.0.0/16, 96.64.0.0/11, 96.96.0.0/12
NetName:        CABLE-1
NetHandle:      NET-96-64-0-0-1
Parent:         NET96 (NET-96-0-0-0-0)
NetType:        Direct Allocation
OriginAS:       AS7922
Organization:   Comcast Cable Communications, LLC (CCCS)
RegDate:        2008-02-21
Updated:        2021-01-25
Ref:            https://rdap.arin.net/registry/ip/96.64.0.0
</code></pre><p>For better or worse, I figured <code>96.120.0.0/14, 96.112.0.0/13, 96.124.0.0/16, 96.64.0.0/11, 96.96.0.0/12</code> was a good set to route. <em>Update:</em> And we later added <code>69.240.0.0/12, 68.80.0.0/13</code>.</p>
<h2 id="step-2-setting-up-the-routing-on-the-usg">Step 2: Setting up the routing on the USG</h2>
<p>There are a number of <a href="https://help.gowifi.co.nz/support/solutions/articles/48000960320-dual-wan-policy-based-routing-with-a-usg">good guides to USG policy-based routing</a> and <a href="https://help.ui.com/hc/en-us/articles/215458888-UniFi-How-to-further-customize-USG-configuration-with-config-gateway-json">how to customize the config.gateway.json</a> required to persist the changes.</p>
<p>So why can't you just set up static routes in the Unifi UI? Apparently if you have load balancing turned on the load balance rules are applied before the static routing rules, making them unhelpful here.</p>
<p>To apply the routing you'll need to SSH to the USG. <code>Settings -&gt; Site -&gt; Device Authentication</code> for settings in the older UI, or <code>Settings -&gt; System -&gt; Application Configuration -&gt; Device SSH Authentication</code> in the &quot;New UI&quot;. You can add an SSH key to make logins easier. Once that's applied you'll SSH to the USG.</p>
<p>The basic premise here is that we'll set up an address group with the Comcast IP ranges, a static routing table that routes to the Comcast interface, and then a firewall rule that says &quot;use the static routing table if the destination IP is in the address group.&quot;</p>
<p>I haven't figured out a great way to get the Comcast gateway address automatically in the rule (there <a href="https://community.ui.com/questions/PBR-next-hop-interface/736eab4f-64a4-4a5a-bf8a-94a7ed61e25b">are discussions on <code>next-hop-interface</code></a>, but caveats that it isn't a good idea). For the moment I'm content to grab it and hardcode it. On the USG:</p>
<pre tabindex="0"><code>admin@ubnt:~$ show load-balance status  
Group wan_failover
  interface   : eth0
  carrier     : up
  status      : active
  gateway     : 172.16.0.1
  route table : 201
  weight      : 100%
  flows
      WAN Out : 349000
      WAN In  : 8
    Local Out : 1502

  interface   : eth2
  carrier     : up
  status      : failover
  gateway     : 66.56.48.1
  route table : 202
  weight      : 0%
  flows
      WAN Out : 53345
      WAN In  : 0
    Local Out : 397
</code></pre><p>eth2 is my Comcast interface (wan2 in the UI), so the gateway IP I need is <code>66.56.48.1</code>.</p>
<p>Now, to apply:</p>
<pre tabindex="0"><code>configure
set protocols static table 5 route 0.0.0.0/0 next-hop 66.56.48.1
set firewall group address-group comcast 96.120.0.0/14
set firewall group address-group comcast 96.124.0.0/16
set firewall group address-group comcast 96.96.0.0/12
set firewall group address-group comcast 96.112.0.0/13
set firewall group address-group comcast 96.64.0.0/11
set firewall group address-group comcast 69.240.0.0/12
set firewall group address-group comcast 68.80.0.0/13
set firewall modify LOAD_BALANCE rule 2500 action modify
set firewall modify LOAD_BALANCE rule 2500 modify table 5
set firewall modify LOAD_BALANCE rule 2500 destination group address-group comcast
set firewall modify LOAD_BALANCE rule 2500 protocol all
commit
exit
</code></pre><p>Once it's committed, if you run <code>traceroute xtvapi.cloudtv.comcast.net</code> you should see it take a Comcast hop from the router instead of the AT&amp;T hop.</p>
<h2 id="step-3-persist-the-config">Step 3: Persist the Config</h2>
<p>Those commands on the USG just make the changes in memory. Now we need to ship the config to wherever we're running the Unifi management console. In my case, that's a Cloud Key Gen2. Following <a href="https://help.ui.com/hc/en-us/articles/215458888-UniFi-How-to-further-customize-USG-configuration-with-config-gateway-json">the guide I linked above</a>, you'll dump the JSON config on the USG, strip it down to the bits we've done here, and then load that onto the Cloud Key as effectively an override file for the config input through the UI.</p>
<p>On the USG, you'll dump config using <code>mca-ctrl -t dump-cfg &gt; config.json</code>. Then just get that JSON to a desktop.</p>
<p>From here I'm positive there's some magic <code>jq</code> incantation that could extract just the JSON leaf nodes we care about, but I couldn't figure it out.</p>
<p>JSON sections that matter to you (at least as of the time of writing... No clue how stable the config format is here):</p>
<ul>
<li><code>firewall.group.address-group.comcast</code></li>
<li><code>firewall.modify.LOAD_BALANCE.rule.2500</code></li>
<li><code>protocols.static.table.5</code></li>
</ul>
<p>Strip the JSON file down and then transfer it to the device running the management console. For me, running a Cloud Key, I needed to make it <code>/srv/unifi/data/sites/default/config.gateway.json</code> and <code>chown unifi:unifi</code>.</p>
<p>Once you've got the file in place, you can go into the management console, find the USG, Settings -&gt; Manage -&gt; Trigger Provision.</p>
<p>If you formatted the file correctly, the USG will cleanly load the config. If not, it'll get stuck in a startup loop. If you hit that case just move the <code>config.gateway.json</code> to something like <code>config.gateway.json.tmp</code> to let it start clean while you debug.</p>]]</description>
<pubDate>Sun, 16 Jan 2022 14:32:13 -0500</pubDate>
</item>

<item>
<title>Computing Podcast Stats with Elasticsearch</title>
<guid>https://ewr.is/2015/07/1815-computing-podcast-stats-with-elasticsearch/</guid>
<link>https://ewr.is/2015/07/1815-computing-podcast-stats-with-elasticsearch/</link>
<dc:creator>Eric Richardson</dc:creator>
<description><![CDATA[<p>Since rejoining <a href="http://www.scpr.org">KPCC</a> last fall, I've spent a good bit of time on the topics of metrics and analytics. There's a very healthy ecosystem of options for monitoring the traffic to your website or the health of your servers, but as a radio station we end up with a lot of questions around how our audio is getting consumed that don't always fit in those same tools.</p>
<p>Podcasts are the hot topic in the audio world right now, but they present a big challenge for those of us trying to make sense out of the numbers. Some of that challenge is intractable right now—podcast clients simply don't give us any information about when a downloaded file is played—but some of it is just about putting a better wrapper around old-school download stats.</p>
<p>At KPCC we're using two free tools—<a href="https://www.elastic.co/products/logstash">Logstash</a> and <a href="https://www.elastic.co/products/elasticsearch">Elasticsearch</a>—to make it much quicker and easier to keep track of podcast downloads.</p>
<h1 id="a-few-things-to-consider">A few things to consider</h1>
<p>Before you jump into setting up tools, it's important to understand a few characteristics of podcast downloads that you need to think about for metrics:</p>
<h2 id="one-download-doesnt-equal-one-request">One download doesn't equal one request</h2>
<p>Many podcast clients will use byte range requests to fetch a file in multiple requests. Some clients probe for the size first and then try to get it all via one followup request, while others simply divide the download into chunks, using one request per chunk. In the server's access log, these requests are typically represented via a status code of 206, or Partial Content. Other clients use a single request, and their response is given a status code of 200.</p>
<p>Either way, we want to make sure we count a single download one time.</p>
<h2 id="some-requests-may-not-equal-downloads">Some requests may not equal downloads</h2>
<p>In an ideal world, podcasters would be able to track and understand the number of times actual people listen to their content. Unfortunately, that's not how podcast clients work. They download files, which may or may not ever be listened to by the user. That's a reality we just sort of have to live with right now.</p>
<p>But not every request to a podcast file is even a client downloading audio. Some requests are from bots, others are from clients that like to inspect the beginning of a file but don't actually follow through with a download.</p>
<p>Broadly, we end up with specific requests we want to exclude (maybe via an IP blacklist), and requests that are too small to count.</p>
<h1 id="concepts">Concepts</h1>
<p>I'll get into the specifics, but basically there are two important concepts to our approach.</p>
<h2 id="session-uuid">Session UUID</h2>
<p>In order to easily deduplicate the pieces that make up one request, we inject a session UUID query parameter during the client's first request for audio content. We do this via a <strong>302 Moved Temporarily</strong> response, which asks the browser to use the UUID URL for its download, but to try the original URL again next time it wants to access the same content.</p>
<p>If you aren't using a session or request ID of some sort, you end up needing to do the same deduplication manually during analysis by processing all your requests and looking for some unique combination of IP address, request path, user agent and time. You'll arrive in largely the same place, but the analysis script has to work much harder.</p>
<h2 id="elasticsearch-cardinality-aggregation">Elasticsearch Cardinality Aggregation</h2>
<p>Once we've processed requests with the UUID injected above, we're able to then leverage Elasticsearch's <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-metrics-cardinality-aggregation.html">cardinality aggregation</a> to reliably estimate a number of unique sessions, without having to load all the session IDs into memory and count them.</p>
<p>Importantly, aggregations are something that Elasticsearch can do well across a cluster, meaning that your query can still perform well even as your number of requests and sessions scales.</p>
<h1 id="the-specifics">The Specifics</h1>
<h2 id="nginx">nginx</h2>
<p>The only important bit here is that you somehow inject the UUID. There are a million different ways you could do that.  At KPCC, we use an <a href="https://github.com/scpr/Podroller">old in-house tool called Podroller</a> behind nginx for our audio delivery. It integrates with another old in-house system that serves up transcoded prerolls, which isn't relevant to this conversation. Because Podroller was sitting in the right place in our delivery pipeline, though, it was easy to add a step that takes any request that comes in without a <code>uuid</code> query param, generates one, adds it to the URL and redirects the user there.</p>
<p>If your audio delivery is via CDN, the redirect step may just need to be a light script that runs on a local server and redirects the altered URL to the CDN.</p>
<h2 id="logstash">Logstash</h2>
<p>We use <a href="https://github.com/elastic/logstash-forwarder">logstash-forwarder</a> to ship our nginx log files from the media server to our central Logstash server.</p>
<p>Once they get there, we use patterns matching our NGINX log file format and a Logstash filter to break each request into a number of fields:</p>
<script src="https://gist.github.com/ewr/50515f709fc00c3bdff8.js"></script>
<p>After breaking the request log into all its fields (including, importantly, fields for the query parameters), Logstash outputs the results into Elasticsearch.</p>
<h2 id="elasticsearch">Elasticsearch</h2>
<p>Here's where the fun happens. Now that all our data is in Elasticsearch, it's time for analysis.</p>
<p>I wrote a little Node.js tool called <a href="https://github.com/SCPR/scpr-media-scripts">scpr-media-tools</a> that we use for extracting Podcast stats. It ends up creating a rather lengthy JSON query body:</p>
<script src="https://gist.github.com/ewr/5e79dda66d630ed44e04.js"></script>
<p>So what is it doing? It's filtering for:</p>
<ul>
<li>Requests that come from our audio delivery host and are marked as &quot;podcasts&quot; with a <code>via</code> query parameter. The latter part wouldn't be necessary if podcast data is all you were piping into the system.</li>
<li>Requests that involved sending more than 8kb, which for us, this ended up being a suitable floor to filter out most of the non-requests mentioned above.</li>
<li>Requests that match the day we're looking for, since our script grabs stats one day at a time.</li>
<li>Requests that did not come from an IP address that we've blacklisted as a non-human scraper.</li>
</ul>
<p>By saying <code>size: 0</code>, we're asking Elasticsearch not to return any actual data. Instead, we're asking for it to just return the output of two aggregations, one nested inside the other.</p>
<p>The <code>show</code> aggregation uses a <code>context</code> query parameter that we tag onto our URLs to break downloads out by program. If you've instead got well-named directories or files, you could pull them out via a Logstash filter and do the same thing here.</p>
<p>For each program, we then use the afore-linked cardinality aggregation to produce an estimated number of unique session UUIDs, using the <code>uuid</code> query parameter. The precision threshold tells Elasticsearch to manually count result counts under the threshold, but to do estimation above. Increasing that number will increase memory use, but reduce estimation error.</p>
<p>Once you've got your data to this point, you can also use <a href="https://www.elastic.co/products/kibana">Kibana</a> to visualize it and build nifty dashboards:</p>
<div class='pf pfW'><img src="/ah/i/07b9e26d4615e47808f2823aee7d7983/4316-wide.jpg" alt="Example podcasts dashboard using Kibana." /><div class='pfP'>Eric Richardson / KPCC</div>
    Example podcasts dashboard using Kibana.
  </div>
<p>We have a couple different metrics pipelines for different types of audio delivery, but Elasticsearch aggregations have become the common denominator in all of them.</p>]]</description>
<pubDate>Fri, 10 Jul 2015 17:43:24 -0400</pubDate>
</item>

<item>
<title>Back to Work: Looking Beyond the Play Button</title>
<guid>https://ewr.is/2015/02/1814-back-to-work-looking-beyond-the-play-button/</guid>
<link>https://ewr.is/2015/02/1814-back-to-work-looking-beyond-the-play-button/</link>
<dc:creator>Eric Richardson</dc:creator>
<description><![CDATA[<p>I've been back at <a href="http://www.scpr.org">KPCC</a> for about four months now, and it's been a trip to drop back into code and ideas that I had left behind in 2012 when we headed east to Atlanta. There have definitely been times where I've wished code that I wrote back then wasn't still around to greet me, but on the whole it's been great to get to pick up the process of thinking about how radio works in the digital age.</p>
<p>Today some of that thinking actually gets into the hands of users. We just <a href="https://itunes.apple.com/us/app/kpcc/id293825085?mt=8">pushed a completely-rewritten version of KPCC's iPhone app</a> that lets you rewind to the start of the currently-live program regardless of when you start listening.</p>
<p>Right at the tail end of my first stint at the station, we switched KPCC's online streams over to <a href="https://github.com/StreamMachine/StreamMachine">StreamMachine</a>, a server package that I had written to replace our aging Shoutcast processes. It's done a great job serving our online listeners over that period, but until today it hasn't had a chance to show off its party trick: letting listeners step back in time and explore &quot;nearly live&quot; radio.</p>
<p>In March of 2012 I <a href="http://ewr.is/2012/03/1788-taking-radio-beyond-the-play-button">wrote here about &quot;taking radio beyond the play button&quot;</a> and a little bit about the potential for new listening experiences we could enable by treating our &quot;recently live&quot; stream as a big buffer that you could use in different ways.</p>
<p>StreamMachine has had that buffer for three years now, but no one really ever got a chance to take advantage of it. The Knight Foundation didn't choose to take us up on the grant proposal we submitted, and other priorities left StreamMachine dutifully serving up plenty of live connections, but none that wandered back in time.</p>
<p>With today's launch—credit for which should go to <a href="https://twitter.com/seandillingham">@seandillingham</a>, <a href="https://github.com/bhawk9780">Ben Hochberg</a> and <a href="https://twitter.com/meekr5">@meeker5</a>—we're finally taking a first step into those capabilities. The app lets listeners play the live stream or rewind to the start of the current show, along with the usual complement of on-demand podcasts, etc. You can't yet seek to arbitrary positions or explore previous programs, but you can finally solve the problem of tuning in only to realize you missed the top of the show.</p>
<p>Under the covers is another very cool change: the app and StreamMachine are now using Apple's HTTP Live Streaming (HLS) protocol instead of Shoutcast for pulling the audio stream. Where Shoutcast is one long connection that needs to stay active, HLS uses a stream of short-lived connections that grab segmented chunks of media content. That's way more user-friendly in a mobile world where listeners hop from WIFI to 3G/LTE and back with regularity.</p>
<p><em>There's lots that I'll want to write about the process of implementing HLS and its implications for things like listener analytics, but that will be a different post.</em></p>
<p>A nifty byproduct of the switch: you can now hit pause, run into the store, and hit play again afterward to pick up right where you left off. In the Shoutcast world, that would have required that the app keep sucking data while you weren't listening, but HLS lets us just go back to pulling segments as needed.</p>
<p>As today's launch trickles down to users, we know we'll run into lots of edges that could work just a little bit better, but hopefully we'll also get listeners excited about new ideas in how a radio stream fits into their daily life.</p>]]</description>
<pubDate>Wed, 04 Feb 2015 20:25:47 -0500</pubDate>
</item>

<item>
<title>New Chapters</title>
<guid>https://ewr.is/2014/10/1813-new-chapters/</guid>
<link>https://ewr.is/2014/10/1813-new-chapters/</link>
<dc:creator>Eric Richardson</dc:creator>
<description><![CDATA[<p>September was an eventful month.</p>
<p>On the 20th, Kathy and I became the very proud parents of a very little boy who shares the name Robert with one grandfather, two great grandfathers and a great-great grandfather.</p>
<p>And then yesterday I ended the month with my last day at <a href="http://emcien.com">Emcien</a>, where I've been working for the last two-and-a-half years since moving to Atlanta. On Monday I (virtually) head back to my last job with <a href="http://scpr.org">KPCC</a>, where I'll get to dive back into a lot of cool ideas about the future of radio.</p>
<p>During the time since I left, we've continued to slowly push forward on <a href="https://github.com/StreamMachine/StreamMachine">StreamMachine</a>, the streaming audio server that I had written for the station. I'm very excited to get to ramp that work up and dive back into all the other cool things that we can do to deliver great content to listeners in this ever-more-connected world.</p>]]</description>
<pubDate>Wed, 01 Oct 2014 16:27:03 -0400</pubDate>
</item>

<item>
<title>Making Recipes</title>
<guid>https://ewr.is/2013/11/1812-making-recipes/</guid>
<link>https://ewr.is/2013/11/1812-making-recipes/</link>
<dc:creator>Eric Richardson</dc:creator>
<description><![CDATA[<p>I brought this site out of mothballs last month by <a href="http://ewr.is/2013/10/1811-falling-behind">bemoaning how easy it was to fall behind on the things that we do for a living</a>.  I'm proud to say that I took my admonition to heart, and today this site is running on a newly spun-up virtual machine that is 100% built via automation.</p>
<p>I'm using Chef for configuration management, since that's what I use day-to-day at <a href="http://emcien">Emcien</a>.  My little one-server deployment gave me an excuse to try out their hosted product, as compared to the open-source server we're running.</p>
<p>A handful of off-the-shelf recipes do the initial setup:</p>
<ul>
<li><a href="https://github.com/opscode-cookbooks/hostname">hostname</a>: Set the server's host name based on the node name and the &quot;ewr.is&quot; domain.</li>
<li><a href="https://github.com/opscode-cookbooks/users">users::sysadmins</a>: Create my user account from information in a data bag.</li>
<li><a href="https://github.com/opscode-cookbooks/sudo">sudo</a>: Grant sudo rights to the proper user groups.</li>
<li><a href="https://github.com/opscode-cookbooks/ntp">ntp</a>: Make sure we're running the NTP daemon to sync the server clock.</li>
<li><a href="https://github.com/mdxp/nodejs-cookbook">nodejs</a>: Install NodeJS from packages.</li>
<li><a href="https://github.com/miah/chef-redis">redis::server</a>: Install <a href="http://redis.io">Redis</a> server.</li>
<li><a href="https://github.com/phlipper/chef-percona">percona::server</a>: Install the Percona MySQL server.</li>
</ul>
<p>After that, a few of my own cookbooks take over.</p>
<ul>
<li>
<p>I put together a quick <a href="https://github.com/ewr/nginx_passenger-cookbook">nginx_passenger cookbook</a> to deploy NGINX and <a href="https://www.phusionpassenger.com/">Phusion Passenger</a> via the .deb packages that Phusion recently started building.</p>
</li>
<li>
<p>A <a href="https://github.com/ewr/lifeguard-cookbook">lifeguard cookbook</a> installs the <a href="https://github.com/emcien/lifeguard">lifeguard process runner</a> that I wrote for Emcien.  A <code>lifeguard_service</code> resource then offers a quick way to install new services that will restart via the Capistrano-friendly <code>touch tmp/restart.txt</code>.</p>
</li>
<li>
<p>The cleverly-named <a href="https://github.com/ewr/ewr-cookbook">ewr cookbook</a> ties the two together, illustrating how this site gets configured:</p>
</li>
</ul>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-ruby" data-lang="ruby"><span style="display:flex;"><span>include_recipe <span style="color:#e6db74">&#34;nginx_passenger&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># -- Pre-reqs -- #</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>package <span style="color:#e6db74">&#34;imagemagick&#34;</span>
</span></span><span style="display:flex;"><span>package <span style="color:#e6db74">&#34;libimage-exiftool-perl&#34;</span>
</span></span></code></pre></div><p>Run the <code>nginx_passenger::default</code> recipe, then install two packages the site needs to run.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-ruby" data-lang="ruby"><span style="display:flex;"><span><span style="color:#75715e"># -- Create a User and Directory -- #</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>ewr_app <span style="color:#e6db74">&#34;ewr&#34;</span> <span style="color:#66d9ef">do</span>
</span></span><span style="display:flex;"><span>  action      <span style="color:#e6db74">:create</span>
</span></span><span style="display:flex;"><span>  with_cap    <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>  ruby        <span style="color:#e6db74">&#34;1.9.1&#34;</span>
</span></span><span style="display:flex;"><span>  env({<span style="color:#e6db74">RAILS_ENV</span>:<span style="color:#e6db74">&#34;production&#34;</span>})
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">end</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>dir <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;</span><span style="color:#e6db74">#{</span>node<span style="color:#f92672">.</span>ewr<span style="color:#f92672">.</span>app_path<span style="color:#e6db74">}</span><span style="color:#e6db74">/ewr&#34;</span>
</span></span></code></pre></div><p>Use a little <code>app</code> resource to create a user and home directory, install Ruby 1.9.3 (1.9.1 is the ABI version, which packages still use) and set our environment.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-ruby" data-lang="ruby"><span style="display:flex;"><span><span style="color:#75715e"># -- Make asset_images directory -- #</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>directory <span style="color:#e6db74">&#34;/data/ewr/asset_images&#34;</span> <span style="color:#66d9ef">do</span>
</span></span><span style="display:flex;"><span>  action    <span style="color:#e6db74">:create</span>
</span></span><span style="display:flex;"><span>  owner     <span style="color:#e6db74">&#34;ewr&#34;</span>
</span></span><span style="display:flex;"><span>  recursive <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">end</span>
</span></span></code></pre></div><p>Create the directory that we need for storing images.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-ruby" data-lang="ruby"><span style="display:flex;"><span><span style="color:#75715e"># -- Install the Site File -- #</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>nginx_passenger_site <span style="color:#e6db74">&#34;ewr&#34;</span> <span style="color:#66d9ef">do</span>
</span></span><span style="display:flex;"><span>  action      <span style="color:#e6db74">:install</span>
</span></span><span style="display:flex;"><span>  dir         <span style="color:#e6db74">&#34;</span><span style="color:#e6db74">#{</span>dir<span style="color:#e6db74">}</span><span style="color:#e6db74">/current&#34;</span>
</span></span><span style="display:flex;"><span>  server      <span style="color:#e6db74">&#34;ewr.is&#34;</span>
</span></span><span style="display:flex;"><span>  ruby        <span style="color:#e6db74">&#34;/usr/bin/ruby1.9.1&#34;</span>
</span></span><span style="display:flex;"><span>  rails_env   <span style="color:#e6db74">&#34;production&#34;</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">end</span>
</span></span></code></pre></div><p>Create an NGINX site configuration file pointing to the server we created.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-ruby" data-lang="ruby"><span style="display:flex;"><span><span style="color:#75715e"># -- Install Lifeguard and Tasks -- #</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>lifeguard_service <span style="color:#e6db74">&#34;ewr.is Resque Pool&#34;</span> <span style="color:#66d9ef">do</span>
</span></span><span style="display:flex;"><span>  action  <span style="color:#f92672">[</span><span style="color:#e6db74">:enable</span>,<span style="color:#e6db74">:start</span><span style="color:#f92672">]</span>
</span></span><span style="display:flex;"><span>  command <span style="color:#e6db74">&#34;bundle exec resque-pool&#34;</span>
</span></span><span style="display:flex;"><span>  user    <span style="color:#e6db74">&#34;ewr&#34;</span>
</span></span><span style="display:flex;"><span>  dir     <span style="color:#e6db74">&#34;</span><span style="color:#e6db74">#{</span>dir<span style="color:#e6db74">}</span><span style="color:#e6db74">/current&#34;</span>
</span></span><span style="display:flex;"><span>  service <span style="color:#e6db74">&#34;ewr-resque&#34;</span>
</span></span><span style="display:flex;"><span>  path    <span style="color:#e6db74">&#34;</span><span style="color:#e6db74">#{</span>dir<span style="color:#e6db74">}</span><span style="color:#e6db74">/bin&#34;</span>
</span></span><span style="display:flex;"><span>  env({<span style="color:#e6db74">RAILS_ENV</span>:<span style="color:#e6db74">&#34;production&#34;</span>})
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">end</span>
</span></span></code></pre></div><p>Install a Lifeguard-managed Resque Pool instance, used for processing images.</p>
<p>The resulting code may not be useful to anyone else, but at least I don't have to be embarrassed when I log on to my own server.</p>]]</description>
<pubDate>Thu, 21 Nov 2013 23:37:53 -0500</pubDate>
</item>

<item>
<title>Ten Days in Europe</title>
<guid>https://ewr.is/2013/11/1810-ten-days-in-europe/</guid>
<link>https://ewr.is/2013/11/1810-ten-days-in-europe/</link>
<dc:creator>Eric Richardson</dc:creator>
<description><![CDATA[<p>Nearly two months ago, Kathy and I returned from a whirlwind ten days in Europe. The purpose of the trip was to revisit a few of Kathy's favorite places from her semester in France, and along the way we discovered a new favorite in the Swiss Riviera.</p>
<p>Technical difficulties (see my <a href="http://ewr.is/2013/10/1811-falling-behind">post about falling behind</a>) kept me from posting these favorite photos here earlier, but I think it is only right to eventually give them a public viewing.</p>]]</description>
<pubDate>Thu, 21 Nov 2013 03:33:29 -0500</pubDate>
</item>

<item>
<title>Falling Behind</title>
<guid>https://ewr.is/2013/10/1811-falling-behind/</guid>
<link>https://ewr.is/2013/10/1811-falling-behind/</link>
<dc:creator>Eric Richardson</dc:creator>
<description><![CDATA[<p>It's funny sometimes how easy it is to fall behind personally in the very things that we do professionally.</p>
<p>At Emcien, my main role these days involves creating the processes we use to build our servers and infrastructure.  That means a lot of building server images and a lot of thinking about how to make sure our environments are always running up to their potential.  I spend a lot of my day writing recipes to make sure that we don't have to worry about our systems and whether a deploy was done correctly.</p>
<p>So imagine my surprise when I logged onto my long-neglected personal server yesterday and realized how much it was the antithesis of all those principles I stick to at work.</p>
<p>It's running Ubuntu 11.10, which hit end-of-life back in May.  Services were cobbled together using a variety of screen sessions. Ruby, Node.js and MySQL are all ancient.</p>
<p>Obviously that's natural in many ways: you're more likely to put the work in when you're paid to do it than you are in your free time, and the rewards for doing the work are higher when you're talking about dozens of servers than they are when you're only managing one.</p>
<p>Still, there has to be some kind of a life lesson here, right?  The things we do when we're in private eventually catch up to us?</p>
<p>As for me, I need to get going on building up some new server recipes.</p>]]</description>
<pubDate>Wed, 23 Oct 2013 07:16:22 -0400</pubDate>
</item>

<item>
<title>End of a Chapter: blogdowntown Shuts Down</title>
<guid>https://ewr.is/2013/08/1809-end-of-a-chapter-blogdowntown-shuts-down/</guid>
<link>https://ewr.is/2013/08/1809-end-of-a-chapter-blogdowntown-shuts-down/</link>
<dc:creator>Eric Richardson</dc:creator>
<description><![CDATA[<p>On Tuesday afternoon <a href="http://www.scpr.org/">KPCC</a> shut down <a href="http://blogdowntown.com">blogdowntown</a>, the community news site that I started in 2005.  It's not much of a surprise: the site has been drifting downward since February of 2011, when I had to stop printing the weekly paper that we had launched six months earlier.</p>
<p>It was an amazing ride while it lasted, though. What started as just me writing about my exploration of a neighborhood turned into a news source that at its peak was attracting 40,000 to 50,000 unique monthly visitors.  That traffic was down more than half by the time KPCC <a href="http://blogdowntown.com/2013/08/7246-kpcc-moves-blogdowntown-coverage-to-main">pulled the plug</a>.</p>
<p>In the end, the question I couldn't answer was the same one that has been haunting journalism for years: how do you make money on this?</p>
<p>As <a href="http://www.laobserved.com/archive/2013/08/blogdowntown_is_folding_i.php">Kevin Roderick noted at LA Observed</a>, we went through several models over the years.  We tried the non-profit thing, but discovered that local businesses really don't care about a tax write-off—they care about whether you can bring people to their door.  The print edition was by far the biggest shot at making a financial model, and I believe that it is one that could have succeeded given a little more runway and a little more business savvy.</p>
<p>That's the biggest lesson I took away from my time as an attempted entrepreneur: I can't do everything, at least not all at once.  If I were doing it all over again I would have made sure to find a partner who could have lived and breathed the numbers side in the same way that I did the content side. When I was having to wear both hats, too often it was the money side of things that got put off in favor of the content and the product, and that's clearly not sustainable.</p>
<p>As I became less a part of blogdowntown after going to KPCC, I threw myself back into technology to fill the void of the writing and photography that I had come to so enjoy.  It's been a fun rediscovery to realize how much I do enjoy building systems and software, but I definitely have a soft spot still for the world of journalism.  Who knows, our paths may again cross one day...</p>]]</description>
<pubDate>Thu, 29 Aug 2013 07:42:16 -0400</pubDate>
</item>

<item>
<title>Two Worlds of Commits</title>
<guid>https://ewr.is/2013/05/1808-two-worlds-of-commits/</guid>
<link>https://ewr.is/2013/05/1808-two-worlds-of-commits/</link>
<dc:creator>Eric Richardson</dc:creator>
<description><![CDATA[<p>When you go to a profile page on <a href="http://github.com">GitHub</a>, you get a graphical map of the user's last year of activity, with green squares representing a day-by-day view of how active the user has been.  When you visit your own page while logged in, that map includes contributions to private repositories. That view can look very different than the one that someone who isn't logged in sees on the same page.</p>
<p>The image above shows the two different views of <a href="http://github.com/ewr">my activity</a> over the last year, with public activity on top and my private view on the bottom.  Apparently it's been a private year: of my 1,189 commits, only 67 show up in public.</p>
<p>That says that while it's been a productive year in general, it hasn't been much of a year for the side-projects that tend to be my public contributions.</p>
<p>While that's not all bad—I've had plenty of chances to play with interesting technology at Emcien—there is something cool about developing side projects.</p>
<p>On the bright side, 25 of those public commits have been to <a href="https://github.com/StreamMachine/StreamMachine">StreamMachine</a> in the past month.  I launched the online streaming project with KPCC on May 1 of last year, just before I left the station to head east, and it has continued to reliably deliver audio to thousands of listeners every day since.  Some of those commits are ones requested by the station, but a number of them are just about wanting to push forward on a project that <a href="http://ewr.is/2012/03/1788-taking-radio-beyond-the-play-button">inspired some big ideas</a>.</p>
<p>Those ideas may have taken some time, but the reason I want to see them happen <a href="http://ewr.is/2012/04/1794-scratching-your-own-itch">hasn't changed</a>: no one else has implemented exactly what I'm looking for yet.</p>]]</description>
<pubDate>Sat, 18 May 2013 10:03:16 -0400</pubDate>
</item>

<item>
<title>Twiddling the Bits for AAC Audio</title>
<guid>https://ewr.is/2013/04/1807-twiddling-the-bits-for-aac-audio/</guid>
<link>https://ewr.is/2013/04/1807-twiddling-the-bits-for-aac-audio/</link>
<dc:creator>Eric Richardson</dc:creator>
<description><![CDATA[<p>I've been doing some work on <a href="https://github.com/StreamMachine/StreamMachine">StreamMachine</a> for KPCC lately, and on a call the other day they mentioned that more of the industry seemed to be standardizing on AAC for audio and moving away from MP3.  It's a move that makes sense: the format's newer, a little smarter, and can sound a little better at lower bit rates.</p>
<p>I set to work reading up on it yesterday, just trying to scope out what it would take to add support to StreamMachine.  Turns out, not much, but I got to have some fun playing with binary in the process.</p>
<p>In the streaming world, AAC audio frames contain an Audio Data Transport Stream (ADTS) header.  It tells you things like what encoding the audio is using and how much of it there is before the next frame header. Conveniently, that's very similar to MP3.</p>
<p><a href="http://www.mp3-tech.org/programmer/frame_header.html">MP3 frame headers</a> are four-bytes, starting with 11 sync bits that are all turned on (all 1's, in the binary world).  The next 21 bits tell you things like the sample rate, whether the audio is mono or stereo, etc.</p>
<p>ADTS headers <a href="http://wiki.multimedia.cx/index.php?title=ADTS">are either seven or nine bytes</a>, depending on whether error protection is enabled.  They start with 12 sync bits.</p>
<p>Laid out as zeros and ones, the ADTS header would be either 56 or 72 digits long.  That's not how you interact with it in the programming world, though.  Instead, bytes (eight bits) are represented as a hexadecimal pair.  So instead of <code>01011100</code> you get <code>0x5C</code> (both equal 92 -- either 4+8+16+64 in the binary or &quot;five-sixteens plus 12&quot; in the hex).</p>
<p>That's all well and good, but for something like this I still end up having to go back to the pen and paper and draw out my bits, as you can see above.  It may be digital, but sometimes it still takes seeing it on paper to actually understand it.</p>]]</description>
<pubDate>Sat, 20 Apr 2013 17:38:00 -0400</pubDate>
</item>

<item>
<title>Big Steps / Little Steps</title>
<guid>https://ewr.is/2013/04/1806-big-steps--little-steps/</guid>
<link>https://ewr.is/2013/04/1806-big-steps--little-steps/</link>
<dc:creator>Eric Richardson</dc:creator>
<description><![CDATA[<p>In February I installed <a href="http://www.moves-app.com/">Moves</a>, a free app that uses your location and movement to create a daily storyline.  It's sort of a pedometer-plus, counting steps but also mapping locations and tracks throughout the day.</p>
<p>I'm blessed to be able to walk to work most days, so most of the steps that Moves ends up tracking are on the 0.6 mile trek between our house and Emcien.  That walk is pretty short, but includes a steep climb.  Because of that, my walking speed is noticeably faster coming home from work than it is heading in: approximately 0.3 - 0.5mph different, according to Moves.</p>
<p>Even more noticeable is the difference in steps.  Each day I take approximately 1300 steps on the way into the office, and 1100 steps on the way back.  I'm covering about 15% more ground with each step coming down the hill.</p>
<p>I guess it makes sense, then, that I keep having to tell Moves that I wasn't running.  Given the difference, it's a fairly understandable mistake.</p>]]</description>
<pubDate>Wed, 03 Apr 2013 20:54:40 -0400</pubDate>
</item>

<item>
<title>getting back into the habit of things</title>
<guid>https://ewr.is/2013/01/1805-getting-back-into-the-habit-of-things/</guid>
<link>https://ewr.is/2013/01/1805-getting-back-into-the-habit-of-things/</link>
<dc:creator>Eric Richardson</dc:creator>
<description><![CDATA[<p>I've been pretty quiet here for the past <em>six years</em> or so.  The last time I hit double digit postings in a month <a href="http://ewr.is/2006/04">was April of 2006</a>.  For most of that time I had the excuse that I was producing plenty of words over at blogdowntown, but those days ended at the end of 2011.  This past year was the first since 1998 where I wasn't writing something on a roughly daily basis.</p>
<p>It was the same in photography.  In Lightroom I have 15,595 pictures from 2009, 14,685 from 2010, 9,125 from 2011 and <strong>690</strong> from 2012. Even that number doesn't quite tell the whole story, since 220 of those were taken in December.</p>
<p>It's funny, sometimes, how hard it is to get back into a habit once you've stepped away.</p>
<p>My time was plenty busy with other things.  Along with moving across the country and buying a house, 2012 became my year to dive back into technology.  Starting the year at KPCC that meant getting into <a href="http://nodejs.org/">Node.js</a> and <a href="http://ewr.is/2012/03/1788-taking-radio-beyond-the-play-button">diving into ideas about where to take online radio</a>.  More recently it has meant getting a handle on dev-ops for <a href="http://emcien.com">Emcien</a>, fleshing out a strategy for using <a href="http://www.opscode.com/chef/">Chef</a> to provision and deploy our servers in the cloud.</p>
<p>Those first passions draw at you, though.  I do miss the research, the writing, the working to get pictures, just the storytelling process in general.</p>
<p>No clue where that will lead in the future, but for now how about it just turns into a few more frequent updates here? Six years might just be long enough...</p>
<p><em>Housekeeping note:</em> A few days ago I moved this site to the domain <strong>ewr.is</strong> (my middle name is William, to answer the obvious question). I just got tired of how long <strong>ericrichardson.com</strong> was to type. All the cool kids have short domains these days.</p>]]</description>
<pubDate>Sun, 06 Jan 2013 20:35:48 -0500</pubDate>
</item>

<item>
<title>We&#39;re New Homeowners</title>
<guid>https://ewr.is/2012/10/1804-were-new-homeowners/</guid>
<link>https://ewr.is/2012/10/1804-were-new-homeowners/</link>
<dc:creator>Eric Richardson</dc:creator>
<description><![CDATA[<p>Kathy and I are now homeowners.  The last week of September we closed on a townhouse in Vinings, a neighborhood tucked just inside the 285 perimeter to the northwest of downtown.</p>
<p>If you look at <a href="http://ericrichardson.com/2012/06/1800-looking-for-a-place-to-settle-into-atlanta">our map from back in June</a>, Vinings wasn't really on it (and neither were townhouses), but over the summer <a href="http://emcien.com">my work</a> relocated there from midtown. That took away the idea of looking for something near transit, and instead we ended up asking ourselves how you could really do better than somewhere in walking distance of the new office.</p>
<p>We decided you couldn't.</p>
<p>Friends helped us move our stuff from storage a week ago, so this weekend was the first we spent as homeowners in our new place.  Even with buying new construction, there were still plenty of projects to get started on.</p>
<p>On Friday we stopped by a Lowes to pick up two <a href="http://www.nest.com/">Nest thermostats</a>, replacing two basic programmable stats with a device that has a little higher geek cred. I had been eying them after how highly <a href="http://eecue.com/">eecue</a> spoke of his, and the combination of a price break after the introduction of the second version, a 10% off coupon and a Georgia tax-free weekend for energy purchases let me pick up the $250 device for a little under $200.  Good thing, since I needed two (and really could use three—I didn't switch the stat on the third zone on our bottom level).</p>
<p>I got those up and running quickly on Saturday morning, and Sunday got to do the much less exciting task of installing towel racks and a new showerhead (1.75 gpm may be &quot;eco-concious&quot;, but it just wasn't cutting it).</p>
<p>Vinings has given us a bit of the walkable lifestyle we missed from our time in downtown L.A. There are a number of good restaurant options in easy walking distance, along with yogurt, ice cream and a few shops. No good coffee, though... Still miss that.</p>]]</description>
<pubDate>Mon, 08 Oct 2012 04:46:49 -0400</pubDate>
</item>

<item>
<title>Looking for a place to settle into Atlanta</title>
<guid>https://ewr.is/2012/06/1800-looking-for-a-place-to-settle-into-atlanta/</guid>
<link>https://ewr.is/2012/06/1800-looking-for-a-place-to-settle-into-atlanta/</link>
<dc:creator>Eric Richardson</dc:creator>
<description><![CDATA[<p>Two weeks ago today Kathy and I landed in Atlanta, kicking off <a href="http://ericrichardson.com/2012/05/1797-starting-a-new-chapter-and-moving-east">our new post-California life</a>.  It's an auspicious anniversary: today's high is either 103 or 105, depending on which forecast you look at.</p>
<p>We're starting to get used to our new routines, but won't really be able to get settled in until we find a house and can get the rest of our things out of the storage unit we dropped them off in when the moving truck arrived.  Last weekend we put in some major miles toward that process, driving a big chunk of the city to get a feel for where we might be looking.</p>
<p>While we'll be <a href="http://www.richhomesatlanta.com/">working with my uncle</a> to actually pick a place, our weekend's exploration was mostly powered by <a href="http://www.zillow.com">Zillow</a>.  After doing some initial research online, Kathy drove and I navigated as we wound our way around neighborhood after neighborhood doing drive-bys of homes in our price range.</p>
<p>We probably spent ten hours in the car between the two days, and then supplemented that with a heavy dose of research into schools on Saturday evening.</p>
<p>Zillow was a huge help, and we made good use of it across the web, iPhone and iPad platforms.  In the process, though, I arrived at some thoughts about what I would like to see changed.</p>
<ul>
<li>
<p>On the web, Zillow will give you the schools for a listing, but it won't give them to you on the iPhone or iPad.   While a multi-platform application has to build its user interface around the unique traits of each platform, feature parity is important in creating a consistant experience.</p>
</li>
<li>
<p>Integration to link to or pull in school info from something like <a href="http://www.schooldigger.com/">schooldigger</a> would be a real time saver.  Lots of flipping to a new tab and googling for &quot;es name schooldigger&quot;.  Taking a next step forward to show map overlays with elementary districts would be even better.</p>
</li>
<li>
<p>Though I can't say I've done a huge amount of looking, I haven't found an app that will give me census information for the tract I'm in right now.  Why isn't there an app that does the same sort of visualizations as <a href="http://projects.nytimes.com/census/2010/map">the NY Times' census maps</a>?</p>
</li>
<li>
<p>Zillow lets you make a &quot;Favorite Homes&quot; list, but we really wanted the ability to do multiple lists breaking listings down by &quot;Good Schools&quot;, &quot;outside the perimeter&quot;, etc.</p>
</li>
</ul>
<p>We'll be taking a break this weekend to host a friend, but after that Kathy and I will start the process of actually touring homes.  We've got our target area in mind, so now it's just time to look for the right place to come on the market and make a move.</p>]]</description>
<pubDate>Fri, 29 Jun 2012 05:40:30 -0400</pubDate>
</item>

<item>
<title>Looking to the Future of AssetHost</title>
<guid>https://ewr.is/2012/05/1799-looking-to-the-future-of-assethost/</guid>
<link>https://ewr.is/2012/05/1799-looking-to-the-future-of-assethost/</link>
<dc:creator>Eric Richardson</dc:creator>
<description><![CDATA[<p>When I left <a href="http://www.scpr.org">KPCC</a> earlier this month, I left behind two systems that I was particularly proud of having developed: <a href="http://github.com/AssetHost/AssetHost">AssetHost</a>, a Rails engine that serves up all of the station's web image and video assets, and <a href="http://github.com/StreamMachine/StreamMachine">StreamMachine</a>, a Node.JS streaming audio broadcast system.</p>
<p>Both will live on, since the station was generous enough to allow me to open-source them. There's a thin line between a live open-source project and abandonware that just happens to have code online, though, so it remains to be seen whether either generates the kind of momentum needed to create a life beyond KPCC's uses.</p>
<h2 id="the-vision">The Vision</h2>
<p>The idea behind AssetHost is a simple one: your CMS shouldn't have to know or care what kind of media you want to slap on a news story or blog post.  It should just know what goes where and how to call the system that takes care of implementation. That system, AssetHost, is in turn based around the concept that regardless of media type, you're always going to need to create thumbnails and flat visual representations for things like homepages, section indexes and mobile devices.</p>
<p>The current generation of AssetHost does a good job of moving in that direction.  We hooked the chooser and asset models into both Django and Rails apps with great success, and seamlessly integrated support for videos hosted on Brightcove.</p>
<p>But what if you took that idea to the next level?</p>
<h2 id="a-future-of-media-plugins">A Future of Media Plugins</h2>
<p>At the beginning of the year <a href="http://ericrichardson.com/2012/01/1777-introducing-assetgallery">I wrote AssetGallery</a>, the code that <a href="http://ericrichardson.com/gallery/">serves up the albums in my Photos section</a>.  It's a nifty bit of code, using AssetHost for image serving and turning out some galleries that look cool both on the desktop and on IOS devices.</p>
<p>What would be even cooler, though, is if AssetGallery was a plugin for AssetHost that created gallery assets out of image assets.  Then you could go into the AssetHost admin UI, create a new gallery and use that gallery as an asset on a story without the CMS having to know or care that it is serving up a slideshow instead of a photo.</p>
<p>Or what if you had an &quot;AssetMap&quot; plugin that would take a geocoding and serve up a map as an asset, putting a nice interactive map UI on the story page and cutting thumbnail versions automatically to serve up in static contexts?  That would sure seem a heck of a lot more useful than the map screenshots you too often see on breaking news stories.</p>
<p>The goal would be to have AssetHost act as a framework for these sorts of rich media, allowing plugins to be lightweight bits of code that just focus on their specific implementation.</p>
<h2 id="engines-for-engines">Engines for Engines</h2>
<p>What you basically end up with are engines that can be plugged into AssetHost in the same way that AssetHost can be plugged into your CMS.</p>
<p>Rails does a lot of the heavy-lifting on this already. The <a href="http://edgeguides.rubyonrails.org/engines.html">engine overhaul that came in Rails 3.1</a> is great, and both AssetHost and AssetGallery are written as mountable engines.</p>
<p>The trick that I'm still working to wrap my head around, though, is what an engine would need to do to export multiple routers.  For instance, AssetGallery currently has a routes file that looks like:</p>
<pre><code>AssetGallery::Engine.routes.draw do
  resources :sets do
    member do
      match '/:asset(/detail)' =&gt; &quot;sets#show_asset&quot;, :as =&gt; :show_asset
      post :assets
    end
  end

  match '/' =&gt; 'home#index', :as =&gt; :home
end
</code></pre>
<p>That allows me to pull the gallery functionality into my site just by including one line in my site's routes file:</p>
<pre><code>  mount AssetGallery::Engine =&gt; '/gallery', :as =&gt; :gallery
</code></pre>
<p>What I want for this AssetHost engine scheme, though, is to allow AssetGallery to also have a set of admin routes that it register with AssetHost, so that going to /assethost/galleries/ would give you an admin interface for galleries just like the one /assethost/assets/ would give you for other assets.</p>
<p>I have a feeling the answer lies somewhere in <code>ActionDispatch::Routing::RouteSet</code>, but exactly where has escaped my first few attempts.</p>]]</description>
<pubDate>Thu, 31 May 2012 20:31:32 -0400</pubDate>
</item>

<item>
<title>Tour of California Zips Past</title>
<guid>https://ewr.is/2012/05/1798-tour-of-california-zips-past/</guid>
<link>https://ewr.is/2012/05/1798-tour-of-california-zips-past/</link>
<dc:creator>Eric Richardson</dc:creator>
<description><![CDATA[<p>The eighth and final stage of the 2012 <a href="http://www.amgentourofcalifornia.com/">Tour of California</a> rolled through Downtown L.A. on Sunday, part of a busy day that included Kings and Clippers playoff games at L.A. Live.  Kathy and I didn't go down there for any of that, but we couldn't help but take in a little of the bike race: our apartment was dead in the middle of the five-lap sprint loop that finished out the stage.</p>
<p>Riders zipped by us heading north on Olive street, and then a few minutes later heading south on Hill street.</p>
<p>My photos from yesterday are just little snapshots compared to <a href="http://blogdowntown.com/2010/05/5365-rogers-holds-onto-his-tour-of-california">the ones I took for blogdowntown the last time the tour came through Downtown</a>, but it was fun to get out for a few minutes with the camera nonetheless.</p>
<p>You can <a href="http://ericrichardson.com/gallery/sets/362">see larger versions of the photos over in the gallery section</a>.</p>]]</description>
<pubDate>Mon, 21 May 2012 07:01:12 -0400</pubDate>
</item>

<item>
<title>Starting a New Chapter and Moving East</title>
<guid>https://ewr.is/2012/05/1797-starting-a-new-chapter-and-moving-east/</guid>
<link>https://ewr.is/2012/05/1797-starting-a-new-chapter-and-moving-east/</link>
<dc:creator>Eric Richardson</dc:creator>
<description><![CDATA[<p>It has been a whirlwind month for Kathy and me, starting with a vacation back east and culminating with my last day at KPCC just over one week ago. Next month we'll be backing our bags and moving to Atlanta after a decade here in L.A.</p>
<p>Yesterday I bought the plane tickets: our time as Angelenos ends at 1:10pm on June 15.</p>
<p>Moving back east is a thought that we had entertained for a while, since we don't have any family out here on the west coast. Once Kathy decided that she wasn't going to be returning to her school next year, the conversation about where we might want to be started in earnest.</p>
<p>We went back to South Carolina for Kathy's spring break, and while there decided that we were serious enough about the idea of Atlanta as a possible destination that I should start looking into companies who might be hiring.  I sent off a few resumes, and ended up setting two interviews for a day that we would be passing through before flying back out west.  Both turned into offers, and a week later I turned in my notice to the station and we began the process of planning a move.</p>
<p>Last Monday I started my new job with <a href="http://emcien.com/">Emcien</a>, a company that has a suite of applications built around really fast pattern matching in large data sets.  While I won't be going anywhere near the math itself—I had to admit during my interview that my last math class was my junior year of high school—I'm really intrigued by the challenges that come in building the applications that allow users to make effective use of their data and the results that come out of it.</p>
<p><a href="http://blogdowntown.com">blogdowntown</a> stays at KPCC, where it hopefully has a long life ahead of it.  My day-to-day involvement in the site ended back in January, so our move won't create much of a change there.  I'll continue to be involved with the open-source development of <a href="https://github.com/AssetHost/AssetHost">AssetHost</a> and <a href="https://github.com/StreamMachine/StreamMachine">StreamMachine</a>, the visual asset handling and audio streaming systems I wrote while there.</p>
<p>There's a lot that we'll miss here in L.A., but we're excited to head out on a new adventure.  Unfortunately, that adventure starts with logistics: this week I get to start calling movers.</p>]]</description>
<pubDate>Mon, 14 May 2012 07:08:26 -0400</pubDate>
</item>

<item>
<title>Making Sense of Numbers</title>
<guid>https://ewr.is/2012/05/1796-making-sense-of-numbers/</guid>
<link>https://ewr.is/2012/05/1796-making-sense-of-numbers/</link>
<dc:creator>Eric Richardson</dc:creator>
<description><![CDATA[<p>After several partial-deployment tests, we launched <a href="http://github.com/SCPR/StreamMachine">StreamMachine</a> at KPCC on Tuesday evening, putting <a href="http://www.scpr.org/listen_live/">online listeners</a> in the hands of a Node.JS app that <a href="http://ericrichardson.com/2012/01/1778-getting-over-the-hump-with-node">I barely figured out how to start writing back in January</a>.</p>
<p>There have been a few hiccups—I don't think people with Roku boxes are very happy at the moment—but on the whole it's been a very successful launch.</p>
<p>While audio streaming provides a number of interesting challenges—how do you handle deployment of new app versions when your connections are of indefinite length, for instance—I'm really interested to play with ways of visualizing listener behavior and interactions.</p>
<p>For the StreamMachine launch I'm using a pair of really interesting open source packages put out recently by <a href="https://squareup.com/">Square</a> to enable real-time analysis of listening patterns.  <a href="http://square.github.com/cube/">Cube</a> is a datastore built on top of MongoDB that's designed specifically for time-series data.  <a href="http://square.github.com/cubism/">Cubism</a> is a visualization plugin using <a href="http://d3js.org/">d3</a> that then turns that data into interesting visuals.</p>
<p>Traditional logging for a listening stream doesn't make for a very good event.  Saying that a listener completed a 45-minute streaming session at 6:55pm is great, but that data point needs to be spread out over the whole listening period to give a true picture.</p>
<p>But what if instead you logged every minute listened as it was listened?  You would end up with a series of events taking place at the right time in the timeline, and you could then analyze them as they're still in progress.</p>
<p>The screenshot above shows the results, which gives a snapshot of all listening, a comparison to yesterday's stats, and a view of KPCC app listening vs some in-browser numbers.  Refining the options and presentation is a work-in-progress, but already it's a fascinating live view of what's going out of our servers.</p>]]</description>
<pubDate>Thu, 03 May 2012 22:02:12 -0400</pubDate>
</item>

<item>
<title>Authenticating a Google Service Account</title>
<guid>https://ewr.is/2012/04/1795-authenticating-a-google-service-account/</guid>
<link>https://ewr.is/2012/04/1795-authenticating-a-google-service-account/</link>
<dc:creator>Eric Richardson</dc:creator>
<description><![CDATA[<p>I spent a few hours at work today trying to make sense of Google's new <a href="https://developers.google.com/accounts/docs/OAuth2ServiceAccount">OAuth2 service accounts</a>, implementing code to generate the JSON Web Token it wants and then query Google's auth server to get the OAuth token. Google intends the accounts to be used by backend services, which makes a lot more sense than shoe-horning them into access via some user's account as is normally the case.</p>
<p>Only after I had all that and I tried to make an API request did I realize that Google doesn't support service accounts in Analytics yet, so all of that work was in vain.  To try to keep it from going to waste, I figured I would post it here.  Looking to authenticate a Google service account  for a Ruby app? Hopefully this will save you a little time.</p>
<p>The <a href="https://github.com/progrium/ruby-jwt">JWT gem can be found here</a>.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-ruby" data-lang="ruby"><span style="display:flex;"><span>    require <span style="color:#e6db74">&#39;jwt&#39;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e"># -- Create JWT -- #</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e"># load in private key</span>
</span></span><span style="display:flex;"><span>    keydata <span style="color:#f92672">=</span> <span style="color:#66d9ef">nil</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">File</span><span style="color:#f92672">.</span>open(<span style="color:#66d9ef">Rails</span><span style="color:#f92672">.</span>root<span style="color:#f92672">.</span>to_s<span style="color:#f92672">+</span><span style="color:#e6db74">&#34;/config/google_api_key.p12&#34;</span>,<span style="color:#e6db74">:encoding</span> <span style="color:#f92672">=&gt;</span> <span style="color:#e6db74">&#34;binary&#34;</span>) {<span style="color:#f92672">|</span>f<span style="color:#f92672">|</span> keydata <span style="color:#f92672">=</span> f<span style="color:#f92672">.</span>read(); }
</span></span><span style="display:flex;"><span>    pk <span style="color:#f92672">=</span> <span style="color:#66d9ef">OpenSSL</span><span style="color:#f92672">::</span><span style="color:#66d9ef">PKCS12</span><span style="color:#f92672">.</span>new(keydata,<span style="color:#e6db74">&#34;notasecret&#34;</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e"># generate the JWT</span>
</span></span><span style="display:flex;"><span>    jwt <span style="color:#f92672">=</span> <span style="color:#66d9ef">JWT</span><span style="color:#f92672">.</span>encode({
</span></span><span style="display:flex;"><span>      <span style="color:#e6db74">:iss</span>    <span style="color:#f92672">=&gt;</span> <span style="color:#e6db74">&#34;12345678@developer.gserviceaccount.com&#34;</span>,
</span></span><span style="display:flex;"><span>      <span style="color:#e6db74">:scope</span>  <span style="color:#f92672">=&gt;</span> <span style="color:#e6db74">&#34;https://www.googleapis.com/auth/someservice.readonly&#34;</span>,
</span></span><span style="display:flex;"><span>      <span style="color:#e6db74">:aud</span>    <span style="color:#f92672">=&gt;</span> <span style="color:#e6db74">&#34;https://accounts.google.com/o/oauth2/token&#34;</span>,
</span></span><span style="display:flex;"><span>      <span style="color:#e6db74">:exp</span>    <span style="color:#f92672">=&gt;</span> (<span style="color:#66d9ef">Time</span><span style="color:#f92672">.</span>now <span style="color:#f92672">+</span> <span style="color:#ae81ff">3600</span>)<span style="color:#f92672">.</span>to_i,
</span></span><span style="display:flex;"><span>      <span style="color:#e6db74">:iat</span>    <span style="color:#f92672">=&gt;</span> <span style="color:#66d9ef">Time</span><span style="color:#f92672">.</span>now<span style="color:#f92672">.</span>to_i
</span></span><span style="display:flex;"><span>    },pk<span style="color:#f92672">.</span>key,<span style="color:#e6db74">&#34;RS256&#34;</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e"># -- Convert into an OAUTH2 Token -- #</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    uri <span style="color:#f92672">=</span> <span style="color:#66d9ef">URI</span>(<span style="color:#e6db74">&#34;https://accounts.google.com/o/oauth2/token&#34;</span>)
</span></span><span style="display:flex;"><span>    req <span style="color:#f92672">=</span> <span style="color:#66d9ef">Net</span><span style="color:#f92672">::</span><span style="color:#66d9ef">HTTP</span><span style="color:#f92672">::</span><span style="color:#66d9ef">Post</span><span style="color:#f92672">.</span>new uri<span style="color:#f92672">.</span>path
</span></span><span style="display:flex;"><span>    req<span style="color:#f92672">.</span>set_form_data(<span style="color:#e6db74">:grant_type</span> <span style="color:#f92672">=&gt;</span> <span style="color:#e6db74">&#34;assertion&#34;</span>, <span style="color:#e6db74">:assertion_type</span> <span style="color:#f92672">=&gt;</span> <span style="color:#e6db74">&#34;http://oauth.net/grant_type/jwt/1.0/bearer&#34;</span>, <span style="color:#e6db74">:assertion</span> <span style="color:#f92672">=&gt;</span> jwt)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    conn <span style="color:#f92672">=</span> <span style="color:#66d9ef">Net</span><span style="color:#f92672">::</span><span style="color:#66d9ef">HTTP</span><span style="color:#f92672">.</span>new uri<span style="color:#f92672">.</span>host, uri<span style="color:#f92672">.</span>port
</span></span><span style="display:flex;"><span>    conn<span style="color:#f92672">.</span>use_ssl <span style="color:#f92672">=</span> <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    resp <span style="color:#f92672">=</span> conn<span style="color:#f92672">.</span>start <span style="color:#66d9ef">do</span> <span style="color:#f92672">|</span>http<span style="color:#f92672">|</span>
</span></span><span style="display:flex;"><span>      http<span style="color:#f92672">.</span>request(req)
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">end</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    json <span style="color:#f92672">=</span> <span style="color:#66d9ef">JSON</span><span style="color:#f92672">.</span>parse(resp<span style="color:#f92672">.</span>body)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e"># token is at json[&#39;access_token&#39;]</span>
</span></span><span style="display:flex;"><span>    puts json
</span></span></code></pre></div>]]</description>
<pubDate>Thu, 26 Apr 2012 15:34:34 -0400</pubDate>
</item>

<item>
<title>Scratching your own itch...</title>
<guid>https://ewr.is/2012/04/1794-scratching-your-own-itch/</guid>
<link>https://ewr.is/2012/04/1794-scratching-your-own-itch/</link>
<dc:creator>Eric Richardson</dc:creator>
<description><![CDATA[<p>It feels nice to be able to build something that scratches your own itch, even if it may be one that no one else has.  This morning I commuted to work while listening to some &quot;nearly live&quot; radio via a small iPhone app that I put together to work with <a href="http://github.com/SCPR/StreamMachine/">StreamMachine</a>, my experiment in next-gen streaming audio server.</p>
<p>While we <a href="http://ericrichardson.com/2012/03/1788-taking-radio-beyond-the-play-button">didn't get picked to receive the grant I wrote about a few weeks ago</a>, I've been saying for several months now that I really wanted an app to let me grab a commute's worth of audio just before I walked out the door and onto the subway.</p>
<p>Last night, I finally got the app to the point where I could do just that.  The server side of things has been in place since mid-February, but the app side of things kept running into the fact that I don't know Objective-C and have never put the time into getting up to speed on iOS app development.</p>
<p>That point kept being hammered home to me over the weekend as I again and again ran into variations of the <code>EXC_BAD_ACCESS</code> error as I tried to reference things that I was inadvertently failing to keep in memory.</p>
<p>Last night I finally refactored my way past that and got the app working.</p>
<p>Then this morning I pushed the &quot;Grab New Audio!&quot; button a few minutes before walking out the door and quickly had the last fifty minutes of live radio to listen to as I navigated the Red and Gold lines—a route that has both periods of no data connection and connectivity losses.</p>
<p>It may just be a proof-of-concept, but it worked like a champ for me.</p>]]</description>
<pubDate>Wed, 25 Apr 2012 19:04:42 -0400</pubDate>
</item>

<item>
<title>Extra terabytes</title>
<guid>https://ewr.is/2012/04/1793-extra-terabytes/</guid>
<link>https://ewr.is/2012/04/1793-extra-terabytes/</link>
<dc:creator>Eric Richardson</dc:creator>
<description><![CDATA[<p>Six years ago today <a href="http://ericrichardson.com/2006/04/1688-counting-drives">I added up all the hard drives in my apartment and got ~1,210 gigabytes</a>. The largest single drive was 250 gigabytes.</p>
<p>Today I have a two terabyte drive that I use for backups, and 750 gigabytes inside my laptop. Another terabyte drive (the one pictured) is sitting on the desk, but not being used for anything in particular.</p>
<p>I still marvel at how cheap storage has gotten, and how quick it has gotten there.</p>]]</description>
<pubDate>Wed, 18 Apr 2012 18:58:53 -0400</pubDate>
</item>

<item>
<title>A Return to Reading</title>
<guid>https://ewr.is/2012/04/1792-a-return-to-reading/</guid>
<link>https://ewr.is/2012/04/1792-a-return-to-reading/</link>
<dc:creator>Eric Richardson</dc:creator>
<description><![CDATA[<p>When the new iPad came out a few weeks ago, I hopped in the car and took a drive over to the Apple store at The Grove to pick one up.  I had been intrigued but not seriously tempted by the first two models, but this time the new screen and a growing fascination with mobile devices made it something I couldn't resist.</p>
<p>My first few weeks have produced an interesting result, though: they've made me a reader again.</p>
<p>I've gone through four books since buying the device just a little over a month ago. I started out with Neal Stephenson's most recent release, <a href="http://www.goodreads.com/book/show/10552338-reamde">Reamde</a>, then circled back to some of the books that followed Orson Scott Card's <a href="http://www.goodreads.com/book/show/375802.Ender_s_Game">Ender's Game</a>, reading <a href="http://www.goodreads.com/book/show/7967.Speaker_for_the_Dead">Speaker for the Dead</a>, <a href="http://www.goodreads.com/book/show/9532.Ender_s_Shadow">Ender's Shadow</a> and <a href="http://www.goodreads.com/book/show/9534.Shadow_of_the_Hegemon">Shadow of the Hegemon</a>.</p>
<p>In that time no apps have become essential parts of my life, though I will admit to spending a few hours on <a href="http://firemint.com/real-racing-2-hd-home/">Real Racing 2 HD</a> (and have spent a few hours more than I will admit).  I think the iPad makes an amazing web browser, and have been thoroughly impressed with the battery life.</p>
<p>Above all, though, I've just loved having a device that I can carry to catch up on news, do some emails, and sneak a few pages from a book every time I've got an extra minute.  I've started carrying it instead of my laptop on my commute, which means that I can grab a few pages while waiting for the train and then a chapter more as I'm on the Gold Line.  Sitting in Starbucks before I head into the office it's web and email, then maybe a few more pages at lunch if I'm headed off on my own.</p>
<p>I don't doubt that eventually other apps will find their way into my day-to-day, and I have a few ideas for ones that I could write to scratch some of my own itches, but in the meantime I'm enjoying this new return to books.  Next up: Don Delillo's <a href="http://www.goodreads.com/book/show/11761.Underworld">Underworld</a>, a book that's been sitting on my shelf for the last few years.  Here's to hoping this time I'll go ahead and read it through.</p>]]</description>
<pubDate>Sun, 15 Apr 2012 10:41:20 -0400</pubDate>
</item>

<item>
<title>Making Django and Rails Play Nice, Part 4: Nginx Conditionals and Passenger</title>
<guid>https://ewr.is/2012/04/1791-making-django-and-rails-play-nice-part-4/</guid>
<link>https://ewr.is/2012/04/1791-making-django-and-rails-play-nice-part-4/</link>
<dc:creator>Eric Richardson</dc:creator>
<description><![CDATA[<p>I got distracted and never managed to post the last piece of my look at making Django and Rails play nice together <a href="http://www.scpr.org/beta">for KPCC's new beta website</a>.  Part one <a href="http://ericrichardson.com/2012/03/1784-making-rails-and-django-play-nice-with-the">looked at mapping generic relationships with MySQL views</a>, part two at <a href="http://ericrichardson.com/2012/03/1786-making-django-and-rails-play-nice-part-2">building interoperable sessions</a> and part three at <a href="http://ericrichardson.com/2012/03/1787-making-django-and-rails-play-nice-part-3">creating an interwoven caching model</a>.</p>
<p>Once you've got all those pieces in place and have both applications happily running off the same data, though, you still need a way to route incoming requests to one or the other.  That could be something that you handle in logic on the load balancer, but our setup instead left this as something to solve as the request came into the web server.  Turns out, though, <a href="http://nginx.org/">nginx</a> made it all quite easy.</p>
<h2 id="the-requirements">The Requirements</h2>
<p>In order to declare our deployment a success, we needed the ability to gradually roll features and users from the existing Django site to the new Ruby on Rails frontend.</p>
<p>Some paths needed to be conditional based on a beta cookie, while others needed to only point to the Django site to pick up things that we hadn't yet implemented in Rails.  Still others needed to always point to Rails, to allow ground-up development (such as [the <a href="http://www.scpr.org/video/">new videos section</a>) to exist only on the new site regardless of cookie status.</p>
<h2 id="nginx-to-the-rescue">Nginx to the rescue!</h2>
<p>It turns out that those requirements are exactly the sort of thing nginx is good at handling.  While &quot;www.scpr.org&quot; is defined in a &quot;server&quot; block in the nginx config, deeper constructs such as conditionals have all the same rights to define settings related to how the request will be handled.</p>
<p>Early in the process of testing out whether this idea would work, I deployed nginx with <a href="http://modrails.com/">Passenger</a> to see whether Passenger's &quot;experimental&quot; wsgi support might work for hosting our Django app.  It turned out to run quite well, which allowed me to know that all I needed to do in nginx was set <code>root</code> to either the Django or Rails apps and let Passenger handle the rest.</p>
<h2 id="defining-a-map">Defining a map</h2>
<p>nginx has a nifty <code>map</code> construct that makes up the bulk of our conditional logic:</p>
<pre><code>map $uri $test {
    ~^/$                1;
    ~^/assets/          2;
    default             0;
}
</code></pre>
<p>That takes <code>$uri</code> and tests it against the map, putting the result in $test.  The tilde in front of the keys allows regular expressions to be used.  Our simple test here will return <strong>one</strong> (can go either way, based on cookie status) when the user is visiting the homepage, <strong>two</strong> (Rails-only) if they're looking for something under /assets/, and <strong>zero</strong> (Django-only) if they're trying to get anything else.  Our production map is a lot longer, but you get the idea.</p>
<p>Once we have that, we need to add the cookie test.  This gets a little funkier.  If <code>$test2</code> contains the results of your cookie test, typically you would want to say something like:</p>
<pre><code>if ( $test1 == 0 || ($test1 == 1 &amp;&amp; $test2 == 0) ) {
    # django
} else {
    # rails
}
</code></pre>
<p>nginx, though, doesn't support testing multiple variables in a conditional.  That meant we needed to combine the two:</p>
<pre><code># default to old site
set $site 0;
set $test2 &quot;&quot;;

# possible match...  two tests are combined
if ($test = 1) {
   set $test2 A;
}

# map based on existence of a cookie
if ($cookie_scprbeta = &quot;true&quot;) {
    # use new site
    set $test2 &quot;${test2}B&quot;;
}

if ($test2 = &quot;AB&quot;) {
     set $site 1;
}

if ($test = 2) {
     # always map to the new site
    set $site 1;
}
</code></pre>
<p>Default to site <strong>zero</strong>, but use site <strong>one</strong> if either a) we have a conditional match from the map and have the cookie set or b) we have a map match that is only on Rails.</p>
<p>From there we simply set our root based on the <code>$site</code> value:</p>
<pre><code>if ($site = 0) {
        # run old site
        root /web/django_app/public/;
        passenger_min_instances 4;
}

if ($site = 1) {
        # run the new site
        root /web/rails_app/current/public/;
        rails_env &quot;production&quot;;
        passenger_min_instances 4;
}
</code></pre>
<p>Ta-da! Seamless handoff in the same visit.</p>]]</description>
<pubDate>Sat, 14 Apr 2012 09:42:01 -0400</pubDate>
</item>

<item>
<title>Brookgreen Gardens</title>
<guid>https://ewr.is/2012/04/1790-brookgreen-gardens/</guid>
<link>https://ewr.is/2012/04/1790-brookgreen-gardens/</link>
<dc:creator>Eric Richardson</dc:creator>
<description><![CDATA[<p>Kathy and I are in South Carolina visiting family during her Spring Break.  Just up the highway from our house at Litchfield are <a href="http://www.huntingtonbeachstatepark.net/">Huntington Beach State Park</a> and <a href="http://www.brookgreen.org/">Brookgreen Gardens</a>, a pair of properties once owned by Archer and Anna Hyatt Huntington.</p>
<p>My family used to camp at the state park every year, so I've spent much time on that side of the road, but I can only remember once that we had ever visited Brookgreen, the sculpture gardens that the pair opened to the public in 1932. We paid a return visit this week and really enjoyed wandering around the former rice plantation.</p>
<p>A warm spring meant that even in the first week of April, many of the Brookgreen blooms were already past their prime.</p>
<p>Given our normal views in Los Angeles, though, all the green and color was still a sight to behold.</p>
<p>You can <a href="http://ericrichardson.com/gallery/sets/361">see more photos over in the Photos section</a>.</p>]]</description>
<pubDate>Fri, 06 Apr 2012 04:58:35 -0400</pubDate>
</item>

<item>
<title>Logging in JSON</title>
<guid>https://ewr.is/2012/04/1789-logging-in-json/</guid>
<link>https://ewr.is/2012/04/1789-logging-in-json/</link>
<dc:creator>Eric Richardson</dc:creator>
<description><![CDATA[<p>I happened to wander past the node.js blog yesterday and read <a href="http://blog.nodejs.org/2012/03/28/service-logging-in-json-with-bunyan/">this article on Bunyan, a logging framework built around JSON</a>.</p>
<p>It was a fortuitous coincidence, since I was looking for ideas on how best to implement logging inside <a href="http://github.com/SCPR/StreamMachine">StreamMachine</a>, the rewindable streaming audio server I've been playing with at KPCC.</p>
<p><a href="https://github.com/trentm/node-bunyan">Bunyan's</a> toolset isn't quite fleshed out at the moment, but the idea is that logging loosely-structured data as JSON is a heck of a lot more functional than squashing everything into strings.</p>
<p>So instead of a typical Shoutcast w3c log line:</p>
<pre><code>96.10.155.166 - - [04/Apr/2012:07:11:50 -0700] &quot;GET /kpcc HTTP/1.1&quot; 200 219606 &quot;-&quot; &quot;VLC/2.0.1 LibVLC/2.0.1&quot; 85
</code></pre>
<p>StreamMachine can now log a connection as:</p>
<pre><code>{
&quot;name&quot;: &quot;StreamMachine&quot;,
&quot;hostname&quot;: &quot;dev2.kpcc.org&quot;,
&quot;pid&quot;: 18605,
&quot;source&quot;: &quot;kpcclive&quot;,
&quot;level&quot;: 20,
&quot;req&quot;: {
    &quot;method&quot;: &quot;GET&quot;,
    &quot;url&quot;: &quot;/kpcclive.mp3?socket=11516610901102892326&quot;,
    &quot;headers&quot;: {
        &quot;host&quot;: &quot;scprdev.org:8000&quot;,
        &quot;connection&quot;: &quot;keep-alive&quot;,
        &quot;user-agent&quot;: &quot;Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_2) AppleWebKit/535.11 (KHTML, like Gecko) Chrome/17.0.963.83 Safari/535.11&quot;,
        &quot;accept&quot;: &quot;*/*&quot;,
        &quot;referer&quot;: &quot;http://www.scpr.org/assets-flash/streammachine.swf&quot;,
        &quot;accept-encoding&quot;: &quot;gzip,deflate,sdch&quot;,
        &quot;accept-language&quot;: &quot;en-US,en;q=0.8&quot;,
        &quot;accept-charset&quot;: &quot;ISO-8859-1,utf-8;q=0.7,*;q=0.3&quot;
    }
},
&quot;listeners&quot;: 0,
&quot;msg&quot;: &quot;Connection end&quot;,
&quot;time&quot;: &quot;2012-04-05T11:49:38.871Z&quot;,
&quot;v&quot;: 0
}
</code></pre>
<p>(Ignore that I'm not actually including seconds listened or bytes sent in that log line...)</p>
<p>It's wordy, but if the computer is the only one reading, who cares?</p>
<p>I've been writing a lot of log-parsing tools lately, taking in Shoutcast logs and running stats on listening habits like time-of-day and duration.  A structured JSON object gives me a lot more lee-way when it comes to adding and removing log data than a w3c-formatted log does because I no longer have to remember that user agent is the third match in my regex results, etc.  Calling <code>log.req['user-agent']</code> is nice and simple.</p>
<p>Given the headaches that trying to make sense from log data already present—and always will—that's a good thing.</p>]]</description>
<pubDate>Thu, 05 Apr 2012 05:12:54 -0400</pubDate>
</item>

<item>
<title>Taking Radio Beyond the Play Button</title>
<guid>https://ewr.is/2012/03/1788-taking-radio-beyond-the-play-button/</guid>
<link>https://ewr.is/2012/03/1788-taking-radio-beyond-the-play-button/</link>
<dc:creator>Eric Richardson</dc:creator>
<description><![CDATA[<p>Back in January I <a href="http://ericrichardson.com/2012/01/1778-getting-over-the-hump-with-node">mentioned my explorations with Node.JS and the process of getting over the hump with a new framework</a>.  After a couple months of false starts, I had finally gotten a little streaming audio relay up and running.</p>
<p>Turns out, that may have been the start of something interesting.</p>
<p>Over the last few months I've become a bit obsessed with the idea of where radio goes in this new Internet world, and particularly with the idea that the experience of listening to a digital stream has the potential to be worlds better than that offered by your radio.  Your radio, after all, only offers you a play button.</p>
<p>Up to now, though, that's all we've offered Internet listeners as well. Connect, hit play, and go do something else.</p>
<p>That's not going to be good enough any more.  Yesterday <a href="http://newschallenge.tumblr.com/post/19438765941/take-radio-streaming-beyond-the-play-button">KPCC submitted a grant application to the Knight Foundation's News Challenge, seeking funds to build pieces of the next-generation of streaming radio infrastructure</a>.</p>
<p>My Saturday morning is a great example of one of the problems we are trying to solve.  My favorite NPR show is <a href="http://www.cartalk.com/">Car Talk</a> (what that says about me as a news person, I'm not sure). Inevitably, though, I am useless when it comes to remembering to start listening at the top of the hour.  Via <a href="http://www.npr.org/services/mobile/iphone.php">NPR's iPhone app</a>, I can find a station streaming Car Talk at pretty much any hour of the weekend, but none of them do me any good if I only remember to listen at :15 past.</p>
<p>What if I could take control of the process and rewind to the point where I meant to start listening, instead of just the point where I did start listening?</p>
<p><a href="http://espn.go.com/espnradio/losangeles/play?s=la">ESPN Radio's web player does something like this</a>. While the UI leaves a bit to be desires, the idea is killer. Technically, it's not even really that difficult: just build a big buffer up on the server and build a UI around being able to access the stream at different points.</p>
<p>So far I've <a href="http://www.scpr.org/listen_live/demo">built a demo web player implementing this functionality</a>, and continued to play around with <a href="http://github.com/SCPR/StreamMachine">StreamMachine</a> as the server backend.</p>
<h2 id="just-the-beginning">Just the beginning....</h2>
<p>Rewinding is just the beginning when it comes to the potential for new functionality.</p>
<p>I get to the station every morning via two trains. It's a 40 - 45 minute trip. I would love to listen to the radio on my way in, but between the subway and connectivity dropouts on the Gold Line, it's just not practical. I don't want podcasts either: I want today's news, not yesterday's.</p>
<p>But what if our iPhone app had a function where I could tell it that I had a 45 minute trip and it would pump me the last 45 minutes of live radio? Over my home wifi, it would download in a couple minutes and I could commute listening to &quot;nearly live&quot; radio.  Today's news, just a little delayed.</p>
<p>What if I just heard something awesome on the radio and I want to share it with my friends? What if I could hit a button and share that point in the stream to my Facebook timeline or Twitter followers?</p>
<p>All of a sudden, radio gets interesting and social.</p>
<h2 id="whats-next">What's next?</h2>
<p>This project started as a spare-time pursuit on my evenings and weekends, but I have a feeling it's going to become a much larger part of my work hours over the next few months.  If we do end up getting the Knight grant, that would come this summer and would certainly help accelerate the process of building out more player interfaces and integrating into the station's existing broadcast systems.</p>
<p>In the meantime, though, it's time to just keep playing.</p>]]</description>
<pubDate>Sat, 17 Mar 2012 10:57:29 -0400</pubDate>
</item>

<item>
<title>Making Django and Rails Play Nice, Part 3: Caching</title>
<guid>https://ewr.is/2012/03/1787-making-django-and-rails-play-nice-part-3/</guid>
<link>https://ewr.is/2012/03/1787-making-django-and-rails-play-nice-part-3/</link>
<dc:creator>Eric Richardson</dc:creator>
<description><![CDATA[<p><em>This is the third section of a multi-part look at some of the issues that we faced in developing <a href="http://www.scpr.org/beta">KPCC's new beta website</a>, which is written in Ruby on Rails but runs side-by-side with the existing Django site. Part one <a href="http://ericrichardson.com/2012/03/1784-making-rails-and-django-play-nice-with-the">looked at mapping generic relationships with MySQL views</a>, while part two <a href="http://ericrichardson.com/2012/03/1786-making-django-and-rails-play-nice-part-2">talked about adapting sessions to allow them to wander from one site to the other</a>.</em></p>
<p>There are plenty of ways to do caching.  Page caches, partial caches, timestamp-based caches, versioned caches... they've all got some place in valid place in the caching arsenal.  In Rails, the convention is to build sweepers that get fired when model objects are saved, giving a spot to handle expiration or rebuilding of caches.  But what do you do when Django needs to be the one to expire your Rails caches?</p>
<p>Django's default cache is built around memcache and expiration times, with a version number prefix that can be incremented to change keys and start over when content changes.</p>
<p>That was the setup when I first got to the station, but expiration-based caches have a number of consequences that are less than ideal. First, unless you are keeping track of versioning and updating that key prefix, you're always going to have a delay before the cache times out and updates show up on the site.  Keep that timeout too low, though, and you'll expire caches unnecessarily.  That creates a situation where most page views are fast, but every time the cache expires some unlucky visitor is going to get burned as we rebuild the entire page.</p>
<p>Last summer we switched that setup out, putting in a new system that attempts to do a cleaner job of expiring caches only when the content in them changes.  Taking advantage of <a href="http://redis.io/">Redis</a> and its <code>sets</code> data type, the system stashes a copy of the cache key in a set for each piece of content inside.  When that piece of content is updated, the cache system expires every cache in that object's set.</p>
<p>While that move was made well before the plan to implement Rails, it turns out the system didn't need much change to power both systems at once. Make sure the cache keys are stored in full form with any framework-specific prefixing, and all of a sudden a content update in Django can expire caches put in place by Rails.</p>
<h2 id="listening-in-to-the-broadcast">Listening in to the broadcast</h2>
<p>Going a step further, what if you want a content update to trigger a cache rebuild, rather than leaving that action to trigger when a poor user hits an uncached view?</p>
<p>Again, Redis comes to the rescue.  The database includes a <a href="http://redis.io/topics/pubsub">Pub/Sub messaging system</a> that allows an easy abstraction between publishers and subscribers.</p>
<p>In Django, I set up a post_save action on our content that triggers a <code>PUBLISH</code> action with a JSON object containing the content's unique key and some commonly-needed bits of metadata (what is the content's publish status? what action is this: a save, a publish, an unpublish?).</p>
<p>Using the worker listener in <a href="https://github.com/defunkt/resque">Resque</a> as a model, I then set up a Rails process to sit and listen on that same channel. When it sees a change that should update the contents of the homepage, it fires a process that rebuilds our Sphinx caches and then caches new views.</p>
<h2 id="getting-fancy-what-if-you-want-rails-to-cache-for-django">Getting Fancy... What if you want Rails to cache for Django?</h2>
<p>Want to get super-fancy? Use that Rails worker to cache back to Django.</p>
<p>It's actually not that hard, thanks to <a href="http://rubypython.rubyforge.org/">RubyPython</a>:</p>
<pre><code>if Rails.env == &quot;production&quot;
  RubyPython.start(:python_exe =&gt; &quot;/usr/local/python2.7.2/bin/python&quot;)
else
  RubyPython.start()      
end

@pickle = RubyPython.import(&quot;cPickle&quot;)
</code></pre>
<p>Now you have a Ruby interface to Python's Pickle, the important piece you need to write caches that Django will be happy to open:</p>
<pre><code># if we're passed a pickle object, also perform django headlines caching
if pickle
  (Rails.cache.instance_variable_get :@data).set(
    ':1:hsection:headlines',
    pickle.dumps( view.render(
        :partial =&gt; &quot;home/cached/django/headlines&quot;, 
        :object =&gt; scored_content[:headlines]) 
    ),
    :raw =&gt; true
  )
end
</code></pre>
<p>Nifty.</p>
<h2 id="rails-code">Rails Code</h2>
<p>The Rails side of our caching code is <a href="https://github.com/SCPR/redis-content-store">available in the redis-content-store gem on SCPR's Github account</a>.  It includes the underlying caching code and simple view helpers for <code>cache_content</code> and <code>register_content</code>.</p>
<p>It's not perfect—my next step is to put some work into how nested caches would keep track of their content objects correctly—but it's working well for our purposes.</p>
<h2 id="django-code">Django Code</h2>
<p>The Django code can be <a href="https://gist.github.com/1126400">found in this Gist</a>:</p>
<script src="https://gist.github.com/1126400.js"> </script>]]</description>
<pubDate>Fri, 16 Mar 2012 09:04:18 -0400</pubDate>
</item>

</channel>
</rss>
