aboutsummaryrefslogtreecommitdiff
path: root/internal/pvfm/recording/recording.go
blob: bb0abfc086723a3c0b2768c373e4ca455bc0e5e8 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
package recording

import (
	"context"
	"errors"
	"fmt"
	"io"
	"log"
	"log/slog"
	"os"
	"os/exec"
	"path/filepath"
	"strings"
	"time"
)

var (
	ErrMismatchWrite = errors.New("recording: did not write the same number of bytes that were read")
)

// Recording ...
type Recording struct {
	ctx      context.Context
	url      string
	fname    string
	tmpDir   string
	cancel   context.CancelFunc
	started  time.Time
	restarts int

	Debug bool
	Err   error
}

// New creates a new Recording of the given URL to the given filename for output.
func New(url, destFname string) (*Recording, error) {
	ctx, cancel := context.WithTimeout(context.Background(), 8*time.Hour)

	tmpDir, err := os.MkdirTemp("", "aura-*")
	if err != nil {
		return nil, err
	}

	r := &Recording{
		ctx:     ctx,
		url:     url,
		fname:   destFname,
		cancel:  cancel,
		started: time.Now(),
		tmpDir:  tmpDir,
	}

	return r, nil
}

// Cancel stops the recording.
func (r *Recording) Cancel() {
	r.cancel()
}

// Done returns the done channel of the recording.
func (r *Recording) Done() <-chan struct{} {
	return r.ctx.Done()
}

// OutputFilename gets the output filename originally passed into New.
func (r *Recording) OutputFilename() string {
	return r.fname
}

// StartTime gets start time
func (r *Recording) StartTime() time.Time {
	return r.started
}

// Start blockingly starts the recording and returns the error if one is encountered while streaming.
// This should be stopped in another goroutine.
func (r *Recording) Start() error {
	sr, err := exec.LookPath("streamripper")
	if err != nil {
		return err
	}

	fname := filepath.Join(r.tmpDir, "temp.mp3")

	cmd := exec.CommandContext(r.ctx, sr, r.url, "-A", "-a", fname)
	cmd.Stderr = os.Stderr
	cmd.Stdout = os.Stdout

	slog.Info("starting streamripper", "cmd", cmd.Args)

	err = cmd.Start()
	if err != nil {
		return err
	}

	// Automatically kill recordings after eight hours
	go func() {
		t := time.NewTicker(8 * time.Hour)
		defer t.Stop()

		log.Println("got here")

		for {
			select {
			case <-r.ctx.Done():
				return
			case <-t.C:
				log.Printf("Automatically killing recording after 8 hours...")
				r.Cancel()
			}
		}
	}()

	go func() {
		defer r.Cancel()
		err := cmd.Wait()
		if err != nil {
			log.Println(err)
		}
	}()

	defer r.cancel()

	for {
		time.Sleep(250 * time.Millisecond)

		select {
		case <-r.ctx.Done():
			return Move(fname, r.fname)
		default:
		}
	}
}

func Move(source, destination string) error {
	err := os.Rename(source, destination)
	if err != nil && strings.Contains(err.Error(), "invalid cross-device link") {
		return moveCrossDevice(source, destination)
	}
	return err
}

func moveCrossDevice(source, destination string) error {
	src, err := os.Open(source)
	if err != nil {
		return fmt.Errorf("Open(source): %w", err)
	}
	defer src.Close()

	dst, err := os.Create(destination)
	if err != nil {
		return fmt.Errorf("Create(destination): %w", err)
	}
	defer dst.Close()

	_, err = io.Copy(dst, src)
	if err != nil {
		return fmt.Errorf("Copy: %w", err)
	}

	err = os.Remove(source)
	if err != nil {
		return fmt.Errorf("Remove(source): %w", err)
	}

	return nil
}