Daniel's Weblog
Posts About
Tags Colophon

Tags / Posts

Lossless Cut. When I have an hour of video and I need to extract 5 minutes I reach for Lossless Cut instead of Final Cut. It’s a cross platform GUI built on top of ffmpeg. It can merge clips from the same camera in to a single file and extract pieces of a file, both without reencoding. It compliments the commands documented here. I like that I can watch through a video, mark multiple sections for export, and then export them all at once. Since it doesn’t reencode the video it’s much faster than using conventional video editing software.

A few caveats:

  • On MacOS it can read a file over SMB without issue but I’ve found trying to write to SMB causes issues. Someone else reported the same issue but it seems like its an SMB implmentation issue and they won’t be fixing that.
  • It often leaves a split second of black frame before and after each extracted clip. Be sure to trim that off before sharing!

My go to commands for processing GoPro footage (or really any video) with ffmpeg:

  1. Merge all mp4 files in to a single file without reencoding:

    This is very useful on computers without powerful GPUs (older computers or in my case my Synology NAS)!

ffmpeg -f concat -safe 0 -i <(for f in GX*.MP4; do echo "file '$PWD/$f'"; done) -c copy output.mp4
  1. Compress:
for i in GX*.MP4; do ffmpeg -i "$i" -c:a copy -c:v h264 -crf 22 "${i%.}_lowres.mp4"; done
  1. Extract audio from a video file:
ffmpeg -i $1 -vn -acodec copy "${1%.*}.aac"

TRMNL Earthquake Recipe I’m having fun building TRMNL recipes! Here’s one that shows recent earthquakes (pulled from USGS’ public API) and plots them on a map.


NYC Subway Alerts plugin for TRMNL Trmnl is proving to be the internet connected eink screen I always wanted to build but never wanted to maintain. It lives in my kitchen and predominantely shows my calendar but I decided to try building a recipe of my own. It was pretty easy and I’m pretty happy with the result!


Uploading an entire directory to the Internet Archive: I had a collection of files around ~50 GB in size that I wanted to upload to a single item in the Internet Archive directly from my NAS. There is a cli that could work but didn’t have a trivial way to upload all the files in a directory. I tried the S3-compatible endpoint with Cyberduck but that also didn’t work as expected.

I ended up leaving running the Python library in a detached tmux session

# In Bash:
python3 -m venv venv;
source venv/bin/activate;
pip install internetarchive;

# In Python:
from internetarchive import upload
upload('DESTINATION_ITEM_ON_INTERNET_ARCHIVE', 'LOCAL_FOLDER/', access_key='YOUR_ACCESS_KEY', secret_key='YOUR_SECRET_KEY')

Note: Remember, uploading things to the Internet Archive makes the email address associated with your account publicily visible


Grandpa hiked Mount Marcy in 1955. After hiking Mount Marcy twice myself I recognized where one of the pictures was taken.

Grandpa’s original photo, a lone hiker with backpack looks across Marcy Dam Pond up at Mount Marcy

Simulated terrain view of the location of the original picture from Google Earth

Google Maps terrain map

Marcy Dam was damaged by Hurricane Irene in 2011 and the dam has since been removed (Wikipedia).

The same view in 2019:

View in 2019


SSH to server via Tailscale if possible: If Tailscale is not running fall back to a different IP

Requirements:

  • Tailscale installed via Mac App Store
  • jq installed via Brew

Replace the following values: {SERVER_NAME}: These configuration options will be used when you type ssh {SERVER_NAME} {SERVER_USERNAME}: The username you use to connect to the server {SERVER_TAILSCALE_IP}: IP Address or DNS name {SERVER_NON_TAILSCALE_IP}: I said “IP” but “hostname” will also work here {SERVER_PRIVATE_KEY_FILENAME}: Private key to log in with

# If Tailscale is running connect via this:
Match originalhost {SERVER_NAME} exec "[ $(/Applications/Tailscale.app/Contents/MacOS/Tailscale status --json | jq -r .BackendState) != Stopped ]"
    HostName {SERVER_TAILSCALE_IP}
    User {SERVER_USERNAME}
    IdentityFile ~/.ssh/{SERVER_PRIVATE_KEY_FILENAME}

# If Tailscale is not running connect via this:
Host {SERVER_NAME} 
    HostName {SERVER_NON_TAILSCALE_IP}
    User {SERVER_USERNAME}
    IdentityFile ~/.ssh/{SERVER_PRIVATE_KEY_FILENAME}

BeReal Notification Times

My crowd has been enjoying the app BeReal recently. The world is split up geographically in to zones; every user in each zone recieves a notification at the same time which is randomly chosen each day.

I’ve started keeping track of those times. US data is the best but hopefully I can record the others too going forward:

Edit: The BeReal API changed and this is no longer functional. Most of the data I collected is available in other data sets or here on Archive.org.


Supporting Music in Brooklyn (Literally!)

The L Train Brass Band is a community brass band from Brooklyn open to all.

We try to have as much music as possible memorized for parades (it’s more fun that way!) but for some low-key gigs and rehearsals we read music off the page. Many of us like to use our phones to read our music.

Neither I, nor the L Train Brass Band have any association with any of the products linked or mentioned here.

Phones Mounted to Instruments

Lyres and PopSockets

Many band members with smaller phones find that PopSockets fit well in standard lyres.

Closeup of a baritone horn with a phone mounted to it via a PopSocket and lyre

A second closeup of a baritone horn with a phone mounted to it via a PopSocket and lyre

This configuration seems to work well for trumpets, clarinets, many horns.

Lyres without PopSockets

This makes me so nervous but people do it.

A phone clamped in a lyre on an alto saxophone

Another phone clamped in a lyre on an alto saxophone

Lyre with eFlip

Phoe clamped in to an eFlip on a saxophone

Tonal Innovation makes a product called the “eFlip” in several variations—the eFlip holds your phone and the lyre clips on to the bottom of the eFlip. This is what I personally use on my saxophone and I like it a lot!

It’s not perfect, I wish I could buy an eFlip permanently fastened to a lyre and I wish it would hold my phone with springs instead of screws. But those would make it more expensive to produce and mechanically complex! EDIT: After additional time with my eFlip I’ve changed my mind on this—despite moderate movement the eFlip has never slipped out of my lyre.

Clamps

These are less common, here is a brand called “Grip-O-Phone” mounted on a trumpet.

This trumpet player actually modified the original product to add longer threaded rods to hold a small tablet instead of a phone.

Here is a similar product (brand unknown) holding an iPhone on a trombone:

Sousaphones

Sousaphone players seems to like flexible holders. I don’t have much to add about these.

By Hand

Of course some people just hold their phone in their hand. This seems to work for sousaphones and trombones.

Trombone player in a Halloween costume playing while holding their phone in their hand

Tablets on Stands

Here are some observations:

Don’t get the cheapest option

It’s worth paying $10 for a sturdier, higher quality product that will last longer and fold down to be more compact.

More expensive stands will have high quality “jaws” that grip the tablet. Cheap jaws are entirely plastic with a spring system. They’re secure but hard to take on and off.

Better jaws:

Cheap jaws:

Cheaper stands tend to use large screws on the telescoping pole. Once again these work but more expensive options have a lever arm that you fold open and close which is quicker.

The real failing of the cheap stands is the ball joint at the top that allows you to rotate the tablet around. This tends to be the first part to become loose and while it won’t fail catastrophically it is very annoying when you can’t secure your table at just the right angle.


Weather Cal Custom Component

I’ve been using the iOS Scriptable app and the fantastic Weather Cal widget to have an information rich homescreen for a while now. The built-in COVID widget only had data to the country level but I wanted to see the New York Times’ data at the US county level.

OK, so “New York City” isn’t technically a county but the Times makes an exception and groups the boroughs together—call it a hometown bias.

This also serves as a decent example of how to make a custom Weather Cal component that loads, caches, and displays data from a custom API.

The New York Times doesn’t present a REST api but they do publish their data to Github about 3 times a day!

#!/bin/bash
# Live moving averages
curl https://raw.githubusercontent.com/nytimes/covid-19-data/master/rolling-averages/us-counties-recent.csv -o data.csv

# Example of the CSV data:
# date,geoid,county,state,cases,cases_avg,cases_avg_per_100k,deaths,deaths_avg,deaths_avg_per_100k
# 2021-12-21,USA-36998,New York City,New York,17958,10049.14,120.54,33,15.43,0.19

# Data.csv holds the last month of data for every US county sorted in ascending date order.
# I only care about the most recent date so we can search it backwards using tac and grep
tac data.csv | grep -m 1 "New York City" > current_cases_avg.txt

# Grab the 6th column
awk -F "\"*,\"*" '{print $6}' current_cases_avg.txt > current_cases_avg_only.txt

# Copy the text file to my webserver
cp current_cases_avg_only.txt /var/www/FAKE/PATH/current_cases_avg_only.txt

This bash script can live in cron and run every hour.

15 * * * * ~/update.sh >/dev/null 2>&1

In Scriptable, weather-cal.js in the existing custom = {} variable add the following:

  async setupDanielC19() {
    const danielC19Path = code.fm.joinPath(code.fm.libraryDirectory(), "weather-cal-danielC19")
    // getCache(path REQUIRED, minTime OPTIONAL, maxTime OPTIONAL)
    let danielC19Data = code.getCache(danielC19Path, 20, 60)

    if (!danielC19Data || danielC19Data.length == 0 || danielC19Data.cacheExpired) 
      try {

		 //       
      	 // NOTE: Be sure to put in a real URL here!
      	 //
        let rawRequest = new Request("https://danielbeadle.net/FAKE/PATH/current_cases_avg_only.txt")
        let rawData = await rawRequest.loadString()
        if (!rawData || rawData.length == 0) { throw 0 }

        danielC19Data = Number(rawData)

        // Write Data to cache
        code.fm.writeString(danielC19Path, JSON.stringify(danielC19Data, null, 2))

      } catch (err) {
        danielC19Data = code.getCache(danielC19Path, 20, 60)
      }
  
    return danielC19Data
  },
  
  async danielC19(column) {
    if (!this.danielC19Data) {
      danielC19Data = await custom.setupDanielC19()
    }

    // Set up the stack
    let danielC19Stack = code.align(column)
    danielC19Stack.layoutHorizontally()
    danielC19Stack.centerAlignContent()
    danielC19Stack.setPadding(code.padding/2, code.padding*2, code.padding/2, code.padding)
  
    // Round to an integer and add commas
    const c19rkiLine = code.provideText(`NYC 7-Day Avg: ${Number.parseInt(danielC19Data).toLocaleString('en-US')}`, danielC19Stack)
  },

And finally at the top of weather-cal.js add the new component

const layout = `
  row 
    column
      date
      sunset
      battery
      danielC19
      space
      events
`