mirror of
https://github.com/fiatjaf/nak.git
synced 2026-09-12 13:42:57 +02:00
podcast: stop the player on ctrl-c.
nak only waited for the player with cmd.Run() and never handled interrupts itself. on windows the console ctrl-c event reaches every process attached to the console, but mpv.exe is a gui subsystem binary that installs no console handler (mpv.com is the console wrapper), so the player kept playing while nak went away. handle the interrupt ourselves and kill the player, killing the whole process tree with taskkill on windows so helper processes go away too. also drop the hardcoded .exe from the windows candidates: LookPath honours PATHEXT, so mpv.com is picked when it is around. Claude-Session: https://claude.ai/code/session_01GvbaFeAfu16pDBy6ridwop
This commit is contained in:
76
podcast.go
76
podcast.go
@@ -2,11 +2,17 @@ package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/signal"
|
||||
"runtime"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"fiatjaf.com/nostr"
|
||||
"fiatjaf.com/nostr/nip05"
|
||||
@@ -63,7 +69,7 @@ var podcast = &cli.Command{
|
||||
return err
|
||||
}
|
||||
|
||||
return playPodcastEpisode(c.String("player"), episode)
|
||||
return playPodcastEpisode(ctx, c.String("player"), episode)
|
||||
}
|
||||
|
||||
for target := range getStdinLinesOrArguments(c.Args()) {
|
||||
@@ -81,7 +87,7 @@ var podcast = &cli.Command{
|
||||
return err
|
||||
}
|
||||
|
||||
if err := playPodcastEpisode(c.String("player"), episode); err != nil {
|
||||
if err := playPodcastEpisode(ctx, c.String("player"), episode); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -462,7 +468,7 @@ func fetchLatestPodcastEpisode(ctx context.Context, podcast podcastInfo) (*nostr
|
||||
return latest, nil
|
||||
}
|
||||
|
||||
func playPodcastEpisode(player string, episode *nostr.Event) error {
|
||||
func playPodcastEpisode(ctx context.Context, player string, episode *nostr.Event) error {
|
||||
audioURL := firstPodcastAudioURL(*episode)
|
||||
if audioURL == "" {
|
||||
return fmt.Errorf("podcast episode has no audio tag")
|
||||
@@ -484,7 +490,61 @@ func playPodcastEpisode(player string, episode *nostr.Event) error {
|
||||
cmd.Stdout = color.Output
|
||||
cmd.Stderr = color.Error
|
||||
|
||||
return cmd.Run()
|
||||
return runPodcastPlayer(ctx, cmd)
|
||||
}
|
||||
|
||||
// errPodcastInterrupted is returned when the user stops playback with ctrl-c.
|
||||
var errPodcastInterrupted = errors.New("interrupted")
|
||||
|
||||
// runPodcastPlayer runs the player and makes sure it goes away when we do.
|
||||
// on windows a ctrl-c doesn't necessarily reach the player (mpv.exe is a gui
|
||||
// subsystem binary and never installs a console handler), so instead of relying
|
||||
// on the console sending it the event we catch the interrupt ourselves and kill
|
||||
// the player.
|
||||
func runPodcastPlayer(ctx context.Context, cmd *exec.Cmd) error {
|
||||
if err := cmd.Start(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
interrupt := make(chan os.Signal, 1)
|
||||
signal.Notify(interrupt, os.Interrupt, syscall.SIGTERM)
|
||||
defer signal.Stop(interrupt)
|
||||
|
||||
done := make(chan error, 1)
|
||||
go func() { done <- cmd.Wait() }()
|
||||
|
||||
select {
|
||||
case err := <-done:
|
||||
return err
|
||||
case <-interrupt:
|
||||
case <-ctx.Done():
|
||||
}
|
||||
|
||||
killPodcastPlayer(cmd)
|
||||
|
||||
// give the player a moment to die so it doesn't keep writing to our terminal
|
||||
select {
|
||||
case <-done:
|
||||
case <-time.After(3 * time.Second):
|
||||
}
|
||||
|
||||
return errPodcastInterrupted
|
||||
}
|
||||
|
||||
func killPodcastPlayer(cmd *exec.Cmd) {
|
||||
if cmd.Process == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if runtime.GOOS == "windows" {
|
||||
// players tend to spawn helper processes, so kill the whole tree
|
||||
kill := exec.Command("taskkill", "/T", "/F", "/PID", strconv.Itoa(cmd.Process.Pid))
|
||||
if err := kill.Run(); err == nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
cmd.Process.Kill()
|
||||
}
|
||||
|
||||
func buildPodcastPlayerCommand(player string, audioURL string) (*exec.Cmd, error) {
|
||||
@@ -526,9 +586,11 @@ func podcastPlayerCandidates() []struct {
|
||||
name string
|
||||
args []string
|
||||
}{
|
||||
{"mpv.exe", nil},
|
||||
{"vlc.exe", nil},
|
||||
{"ffplay.exe", []string{"-nodisp", "-autoexit"}},
|
||||
// no .exe here on purpose: LookPath honours PATHEXT, so mpv.com,
|
||||
// the console wrapper that handles ctrl-c, is preferred when present
|
||||
{"mpv", nil},
|
||||
{"vlc", nil},
|
||||
{"ffplay", []string{"-nodisp", "-autoexit"}},
|
||||
{"cmd", []string{"/c", "start"}},
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user