Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .github/workflows/lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,9 @@ jobs:
- name: Run golangci-lint
uses: golangci/golangci-lint-action@v7
with:
version: latest
# Pinned to avoid CI surprises when new lint releases tighten
# checks. Bump deliberately, not silently.
version: v2.12.2

vet:
name: Go Vet & Build
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ go.work

# Build artifacts
/rig
/test-shazam
/dist/
/build/

Expand Down
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ No accounts. No ads. Just radio.
- 🎨 Beautiful terminal UI with multiple themes
- ⌨️ Keyboard-driven interface for fast navigation
- ⭐ Save your favourite stations
- 🎤 Identify the playing track with one keypress — Shazam-style, no API key required
- 🎵 Now playing display with station metadata

### Search and play
Expand All @@ -37,6 +38,11 @@ No accounts. No ads. Just radio.
<!-- Replace with actual gif -->
![Managing favourites](docs/assets/favourites.gif)

### Identify the playing track
Press `i` while a station is playing. rig taps the audio, fingerprints it locally in pure Go, and asks Shazam to identify it. Press `o` to open the track on shazam.com.
<!-- Replace with actual gif -->
![Identifying a track in the terminal](docs/assets/identify.gif)


## Installation

Expand Down Expand Up @@ -85,6 +91,8 @@ rig
| `Space` | Pause/Resume |
| `+` / `-` | Volume up/down |
| `s` | Search stations |
| `i` | Identify the playing track |
| `o` | Open identified track on shazam.com |
| `?` | Help |
| `q` | Quit |

Expand Down
113 changes: 113 additions & 0 deletions cmd/test-shazam/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
// Command test-shazam taps a live radio stream URL, fingerprints ~12 seconds
// of audio, and asks Shazam to identify the track. It exists so the
// identification pipeline can be smoke-tested end-to-end without booting the
// TUI.
//
// Usage:
//
// go run ./cmd/test-shazam <stream_url> [duration_seconds]
package main

import (
"context"
"flag"
"fmt"
"os"
"os/signal"
"strconv"
"time"

"github.com/mrwhyte/rig/pkg/identifier"
"github.com/mrwhyte/rig/pkg/identifier/shazam"
)

func main() {
os.Exit(run())
}

func run() int {
flag.Usage = func() {
fmt.Fprintf(os.Stderr, "Usage: %s <stream_url> [duration_seconds]\n", os.Args[0])
flag.PrintDefaults()
}
flag.Parse()

if flag.NArg() < 1 {
flag.Usage()
return 2
}

streamURL := flag.Arg(0)
duration := identifier.DefaultSampleSeconds * time.Second
if flag.NArg() >= 2 {
secs, err := strconv.Atoi(flag.Arg(1))
if err != nil || secs <= 0 {
fmt.Fprintf(os.Stderr, "invalid duration_seconds: %q\n", flag.Arg(1))
return 2
}
duration = time.Duration(secs) * time.Second
}

ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt)
defer cancel()

// Generous overall timeout: capture + network round-trip + Shazam rate limiter.
ctx, cancelTimeout := context.WithTimeout(ctx, duration+30*time.Second)
defer cancelTimeout()

fmt.Fprintf(os.Stderr, "tapping %s for %s...\n", streamURL, duration)

captureStart := time.Now()
samples, sampleRate, err := identifier.CaptureMonoSamples(ctx, streamURL, duration)
if err != nil {
fmt.Fprintf(os.Stderr, "capture error: %v\n", err)
return 1
}
captureWall := time.Since(captureStart)
audioSeconds := float64(len(samples)) / float64(sampleRate)
fmt.Fprintf(
os.Stderr,
"captured %d samples at %d Hz (%.2fs of audio, wall time %s)\n",
len(samples), sampleRate, audioSeconds, captureWall.Round(time.Millisecond),
)
if audioSeconds < float64(duration/time.Second)*0.9 {
fmt.Fprintf(os.Stderr, "warning: captured less audio than requested — stream may have closed early\n")
}

resampled := identifier.Resample(samples, sampleRate, identifier.FingerprintSampleRate)
fmt.Fprintf(
os.Stderr,
"resampled to %d samples at %d Hz\n",
len(resampled), identifier.FingerprintSampleRate,
)

identifyStart := time.Now()
sig := shazam.ComputeSignature(identifier.FingerprintSampleRate, resampled)
result, err := shazam.Identify(ctx, sig)
if err != nil {
fmt.Fprintf(os.Stderr, "identify error: %v\n", err)
return 1
}
fmt.Fprintf(os.Stderr, "shazam round trip %s\n", time.Since(identifyStart).Round(time.Millisecond))

if !result.Found {
fmt.Fprintln(os.Stderr, "no match")
return 1
}

fmt.Printf("Title: %s\n", result.Title)
fmt.Printf("Artist: %s\n", result.Artist)
if result.Album != "" {
fmt.Printf("Album: %s\n", result.Album)
}
if result.Year != "" {
fmt.Printf("Year: %s\n", result.Year)
}
if u := result.ShazamURL(); u != "" {
fmt.Printf("Shazam: %s\n", u)
}
if result.AppleID != "" {
fmt.Printf("AppleID: %s\n", result.AppleID)
}
return 0
}
Loading
Loading