And now for something completely unrelated: Sunday morning

Routine slips in like a thief with me and Sunday mornings are no different. Tho the haughty cries of my youth always proclaimed: freedom! I got suck’d in immediately. Because I am weak and replete with hedonistic tendencies.

I sit sipping italian coffee, clothed in flannel, feet up just so on the stool under my desk. Classical music plays post 06:00 and that’s why I am sitting here, sipping coffee; for the music, because it’s Sunday and they play it every time.

My pal and colleague, Bob, turned me on to this one station long ago that one time we sat in his office Sunday morning, listening, and sharing tales and tastes of music and I learned so much about all this fabulous music from Bob, an old guy with all those decades of listening, especially that I loved it and lo, had been missing it my whole dinosaur-rock life.

Of course, I knew that I enjoyed classical because Bugs Bunny. I turned the TV up! I knew that I dug jazz and soul and funk but it was few and far between. I knew that I loved that olde-tymey radio show stuff, too, lordy. Bob had all of that right on his desk and in bookcases. He had it RIGHT THERE! I have some tapes from that day, I play them in Autumn and it’s Autumn.

As we talked and he played we rolled through a few radio stations, too. Pfft, I already found FCR from Amherst (I think) and listened all the time, man, I’m pretty hip. Then he landed on, whatever, I don’t even remember, but, we stopped rolling through the stations.

He was gone the next day, shipped out, and I never heard from or of him again. I say ‘shipped-out’ and I don’t mean to imply military, naw, we were civilians. I took over his position at location, we were both “the nurse”.

I landed back-to-life a season later and that station we landed on has been playing, exclusively, on my radios/desktop since. This is not a plug for that station but it’s integral to what I am ‘doing’ as I sit here on a regular Sunday morning, for me, listening to classical because it’s Sunday morning.

The ‘frost is on the pumpkin’ this morning, the sun is shining through the window, and some bombastic piece rips through the silence of the new day, every once in a while seeming to correspond to whatever is going on in front of me as I go about my business.

5 Likes

Love this. :smiling_face_with_sunglasses: Funny how one Sunday with someone can stick with you for decades. Coffee, flannel and classical on the radio is a pretty hard combo to beat. Which station is it? Just curious now.

2 Likes

WICN, outta woostah, ma: https://wicn-ice.streamguys1.com/live-mp3 (90.5)

I even have a cronjob setup to record some of their shows, they are so good. IN fact, tonight at 19:00: Soul Serenade. They don’t broadcast headers so I record the stream and the script fills in the tags and downloads the playlist from spinitron and saves it into a nicely formatted text file.

SO handy, then I copy the mp3 onto an old ipod so I can groove in the ride. I mean, I used to do that, and I will again, once I actually take the ipod out of the car. A 2 or 3 hour show in a 145mb mp3 file. Nice.

share the bits…

here’s the script:

#!/usr/bin/env bash
# wicn-show — cron record a WICN show, tag it, save the spinitron playlist.
# usage: wicn-show <slug> "Show Name" <end HH:MM> "Comment"
#   wicn-show new-orleans-grooves "New Orleans Grooves" 20:31 "With Tony Baglio"
# cron fires at 18:59 (1 min early); recorder hard-stops at END.
# If the box is asleep/off at cron time the job never fires (quiet fail, free).
# Forwards to: $HOME/.local/state/wicn-show.log
set -euo pipefail

URL="https://wicn-ice.streamguys1.com/live-mp3"
UA="Mozilla/5.0 (X11; Linux x86_64; rv:128.0) Gecko/20100101 Firefox/128.0"
STREAM_HOST="wicn-ice.streamguys1.com"

[[ $# -eq 4 ]] || { echo "usage: wicn-show <slug> <\"Show Name\"> <end HH:MM> <comment>" >&2; exit 2; }
SLUG=$1 NAME=$2 END=$3 COMMENT=$4

DATE=$(date +%F)
DAY=$(date +%-d) MONTH=$(date +%B) YEAR=$(date +%Y)
YM=$(date +%Y-%m)
WICN_HOME="${WICN_MUSIC:-$HOME/Music}"   # WICN_MUSIC overrides the base (for tests)
DIR="$WICN_HOME/WICN/$SLUG/$YM"
FETCH_DELAY="${WICN_FETCH_DELAY:-75}"   # seconds after recording ends before 1st playlist try

log() { echo "$(date '+%F %T') [$SLUG] $*"; }

# --- 1) network pre-flight: reach the stream host or quietly fail
if ! timeout 8 bash -c "exec 3<>/dev/tcp/$STREAM_HOST/443" 2>/dev/null; then
    log "network unavailable - quietly failing"
    exit 0
fi

# --- 2) disk guard: need a couple GB of slack
mkdir -p "$DIR"
FREE=$(df -BG --output=avail "$DIR" | tail -1 | tr -dc '0-9')
if (( ${FREE:-0} < 2 )); then
    log "only ${FREE:-0}G free on /home - quietly failing"
    exit 0
fi

# --- 3) record (reconnects until END through network drops)
log "recording $NAME until $END ($DATE)"
"$HOME/bin/streamrec" --url "$URL" --show "$SLUG" \
    --base "$WICN_HOME/WICN/$SLUG" --end "$END" --no-tracks \
    || log "streamrec exited nonzero - finalizing what we have"

# --- 4) find + rename the show file
shopt -s nullglob
BEST="" BESTSZ=0
for f in "$DIR"/"$SLUG"_"$DATE"_*.mp3; do
    SZ=$(stat -c%s "$f" 2>/dev/null || echo 0)
    if (( SZ > BESTSZ )); then BEST=$f BESTSZ=$SZ; fi
done
if [[ -z $BEST ]]; then
    log "no show file found - nothing to tag; done"
else
    OUT="$DIR/$SLUG-$DATE.mp3"
    if [[ -e $OUT ]]; then
        HH=$(stat -c %y "$BEST" | awk '{print substr($2,1,5)}' | tr -d ':')
        OUT="$DIR/$SLUG-$DATE-$HH.mp3"
        mv "$BEST" "$OUT"
        log "target existed, kept both: new copy -> $(basename "$OUT")"
    else
        mv "$BEST" "$OUT"
    fi
    if (( BESTSZ < 1000000 )); then
        log "recording under 1 MB ($BESTSZ bytes) - likely no broadcast; leaving file, no tags"
    else
        # --- 5) tag: Title = "WICN <Name> <day> <Month> <Year>", Album, Comment (ID3v2.3)
        python3 - "$OUT" "WICN $NAME $DAY $MONTH $YEAR" "$NAME" "$COMMENT" <<'PY'
import sys
from mutagen.id3 import ID3, ID3NoHeaderError
from mutagen.id3 import TIT2, TALB, COMM
path, title, album, comment = sys.argv[1:5]
try:
    tags = ID3(path)
except ID3NoHeaderError:
    tags = ID3()
tags.add(TIT2(encoding=3, text=title))
tags.add(TALB(encoding=3, text=album))
tags.add(COMM(encoding=3, lang="eng", desc="", text=comment))
tags.save(path, v2_version=3)
print(f"tagged {path}: Title={title!r} Album={album!r} Comment={comment!r}")
PY
        log "saved $(basename "$OUT") ($(( BESTSZ / 1048576 )) MB)"
    fi
fi

# --- 6) spinitron playlist: raw page as title-named .html (like the manual
#        save-as) + a clean .txt spin list. Two tries: at +FETCH_DELAY, then
#        +4 min (catches DJs entering the last song late).
sleep "$FETCH_DELAY"

save_playlist() {
    local feed page tmp1 tmp2 plurl rc
    mkdir -p "$HOME/.cache"
    feed=$(mktemp -p "$HOME/.cache") page=$(mktemp -p "$HOME/.cache")
    if ! curl -sS -m 30 -A "$UA" \
        "https://spinitron.com/WICN/calendar-feed?timeslot=15&start=$DATE&end=$DATE" \
        -o "$feed"; then
        rm -f "$feed" "$page"; return 1
    fi
    plurl=$(python3 - "$feed" "$NAME" 2>/dev/null <<'PY'
import json, re, sys
norm = lambda s: re.sub(r"[^a-z0-9]", "", s.lower())   # "Soul Serenade!" == "Soul Serenade"
d = json.load(open(sys.argv[1]))
evs = d.get("events", []) if isinstance(d, dict) else d
want = norm(sys.argv[2])
for e in evs:
    if norm(str(e.get("title", ""))) == want and e.get("url"):
        print(e["url"]); raise SystemExit(0)
raise SystemExit(3)
PY
) \
        && rc=0 || rc=$?
    if [[ $rc -ne 0 ]]; then rm -f "$feed" "$page"; return $rc; fi
    if ! curl -sS -m 40 -A "$UA" "https://spinitron.com$plurl" -o "$page"; then
        rm -f "$feed" "$page"; return 1
    fi
    # html+t txt named after the page title (spinitron's own save-as convention)
    if ! python3 - "$page" "$DIR" "$NAME" "$DATE" <<'PY'
import sys, re, html, urllib.parse
from html.parser import HTMLParser

page, outdir, name, date = sys.argv[1], sys.argv[2], sys.argv[3], sys.argv[4]
src = open(page, encoding="utf-8", errors="replace").read()

m = re.search(r"<title>(.*?)</title>", src, re.S)
title = html.unescape(m.group(1)).strip() if m else name
fname = re.sub(r"[\x00-\x1f/\\:*?\"<>|].*$", " ", title)
fname = re.sub(r"\s+", " ", fname).strip() or f"{name} {date}"

class Spins(HTMLParser):
    FIELDS = {"artist", "song", "release", "label", "released", "genre"}
    def __init__(self):
        super().__init__()
        self.rows, self.row, self.key, self.time = [], None, None, None
    def handle_starttag(self, tag, attrs):
        c = dict(attrs).get("class", "")
        if tag == "tr" and "spin-item" in c:
            self.row = {}
        elif self.row is not None:
            if tag == "td" and "spin-time" in c:
                self.key = "time"
            elif tag == "span" and c in self.FIELDS:
                self.key = c
    def handle_data(self, data):
        if self.key and self.row is not None:
            self.row[self.key] = (self.row.get(self.key, "") + data).strip()
    def handle_endtag(self, tag):
        if tag in ("span", "td") and self.row is not None:
            self.key = None
        if tag == "tr" and self.row is not None:
            self.rows.append(self.row); self.row = None; self.key = None

p = Spins(); p.feed(src)
lines = []
for r in p.rows:
    if not r.get("song") and not r.get("artist"):
        continue
    who = " - ".join(x for x in (r.get("artist"), r.get("song")) if x)
    extra = " - ".join(x for x in (r.get("release"),) if x)
    pub = " ".join(x for x in (r.get("label"), r.get("released")) if x)
    bits = [f'{r.get("time",""):>7}', who]
    if extra: bits.append(extra)
    if pub: bits.append(f"({pub})")
    lines.append(" | ".join(bits))

with open(f"{outdir}/{fname}.txt", "x", encoding="utf-8") as f:
    f.write(title + "\n" + "=" * len(title) + "\n")
    f.write("\n".join(lines) + "\n")
print(f"{len(p.rows)} spins saved to {outdir}/{fname}.txt")
PY
        then rm -f "$feed" "$page"; return 4   # playlist present but empty/parse failed
    else
        rm -f "$feed" "$page"; return 0
    fi
}

PTRC=0
if save_playlist; then
    log "playlist saved"
else
    PTRC=$?
    log "playlist not ready yet (rc=$PTRC) - retrying once at +4 min"
    sleep 225
    if save_playlist; then
        log "playlist saved (retry)"
    else
        log "playlist quietly failed (rc=$?) - the manual browser save still works"
    fi
fi
log "done"
exit 0

Here’s the cronjobs:

59 18 * * 1 $HOME/bin/wicn-show soul-serenade "Soul Serenade" 22:01 "with Tom Shaker" >> $HOME/.local/state/wicn-show.log 2>&1
59 18 * * 5 $HOME/bin/wicn-show latin-jazz-now "Latin Jazz Now" 22:01 "with Jim Benitez" >> $HOME/.local/state/wicn-show.log 2>&1
59 18 * * 6 $HOME/bin/wicn-show new-orleans-grooves "New Orleans Grooves" 21:01 "With Tony Baglio" >> $HOME/.local/state/wicn-show.log 2>&1

Everything lands in ~/Music/WICN/$SHOW/$DATE

1 Like