241 lines
6.4 KiB
Go
241 lines
6.4 KiB
Go
package podcasts
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"log/slog"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"testing"
|
|
"time"
|
|
|
|
"git.noroutes.net/NoRoutes/vibetunes/internal/database"
|
|
)
|
|
|
|
const testFeedXML = `<?xml version="1.0" encoding="UTF-8"?>
|
|
<rss version="2.0" xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd">
|
|
<channel>
|
|
<title>Test Podcast</title>
|
|
<description>A podcast for testing</description>
|
|
<item>
|
|
<title>Episode One</title>
|
|
<description>The first episode</description>
|
|
<guid>episode-1-guid</guid>
|
|
<pubDate>Mon, 01 Jan 2024 00:00:00 GMT</pubDate>
|
|
<itunes:duration>125</itunes:duration>
|
|
<enclosure url="%s/audio.mp3" type="audio/mpeg" length="12345"/>
|
|
</item>
|
|
<item>
|
|
<title>Episode Two</title>
|
|
<description>The second episode</description>
|
|
<guid>episode-2-guid</guid>
|
|
<pubDate>Tue, 02 Jan 2024 00:00:00 GMT</pubDate>
|
|
<itunes:duration>2:05</itunes:duration>
|
|
<enclosure url="%s/audio.mp3" type="audio/mpeg" length="12345"/>
|
|
</item>
|
|
</channel>
|
|
</rss>`
|
|
|
|
func TestParseRSS(t *testing.T) {
|
|
feed, err := parseRSS([]byte(fmt.Sprintf(testFeedXML, "http://x", "http://x")))
|
|
if err != nil {
|
|
t.Fatalf("parseRSS: %v", err)
|
|
}
|
|
if feed.Channel.Title != "Test Podcast" {
|
|
t.Errorf("Title = %q", feed.Channel.Title)
|
|
}
|
|
if len(feed.Channel.Items) != 2 {
|
|
t.Fatalf("Items = %d, want 2", len(feed.Channel.Items))
|
|
}
|
|
if feed.Channel.Items[0].episodeGUID() != "episode-1-guid" {
|
|
t.Errorf("guid = %q", feed.Channel.Items[0].episodeGUID())
|
|
}
|
|
}
|
|
|
|
func TestParseDuration(t *testing.T) {
|
|
cases := map[string]float64{
|
|
"125": 125,
|
|
"2:05": 125,
|
|
"1:02:05": 3725,
|
|
"": 0,
|
|
"bogus": 0,
|
|
}
|
|
for in, want := range cases {
|
|
if got := parseDuration(in); got != want {
|
|
t.Errorf("parseDuration(%q) = %v, want %v", in, got, want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func newTestManager(t *testing.T) (*Manager, string) {
|
|
t.Helper()
|
|
dir := t.TempDir()
|
|
db, err := database.Open(filepath.Join(dir, "test.db"))
|
|
if err != nil {
|
|
t.Fatalf("database.Open: %v", err)
|
|
}
|
|
t.Cleanup(func() { db.Close() })
|
|
log := slog.New(slog.NewTextHandler(io.Discard, nil))
|
|
return NewManager(db, dir, "", log), dir
|
|
}
|
|
|
|
func TestRefreshChannel(t *testing.T) {
|
|
m, _ := newTestManager(t)
|
|
|
|
var feedURL string
|
|
mux := http.NewServeMux()
|
|
mux.HandleFunc("/feed.xml", func(w http.ResponseWriter, r *http.Request) {
|
|
fmt.Fprintf(w, testFeedXML, feedURL, feedURL)
|
|
})
|
|
mux.HandleFunc("/audio.mp3", func(w http.ResponseWriter, r *http.Request) {
|
|
w.Write([]byte("fake mp3 bytes"))
|
|
})
|
|
srv := httptest.NewServer(mux)
|
|
defer srv.Close()
|
|
feedURL = srv.URL
|
|
|
|
channel, err := database.CreatePodcastChannel(m.DB, srv.URL+"/feed.xml")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
|
defer cancel()
|
|
if err := m.RefreshChannel(ctx, channel.ID); err != nil {
|
|
t.Fatalf("RefreshChannel: %v", err)
|
|
}
|
|
|
|
updated, err := database.GetPodcastChannel(m.DB, channel.ID)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if updated.Title != "Test Podcast" {
|
|
t.Errorf("channel title = %q", updated.Title)
|
|
}
|
|
if updated.Status != "completed" {
|
|
t.Errorf("channel status = %q, want completed", updated.Status)
|
|
}
|
|
|
|
episodes, err := database.ListPodcastEpisodes(m.DB, channel.ID)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(episodes) != 2 {
|
|
t.Fatalf("episodes = %d, want 2", len(episodes))
|
|
}
|
|
}
|
|
|
|
func TestRefreshChannelIsIdempotent(t *testing.T) {
|
|
m, _ := newTestManager(t)
|
|
var feedURL string
|
|
mux := http.NewServeMux()
|
|
mux.HandleFunc("/feed.xml", func(w http.ResponseWriter, r *http.Request) {
|
|
fmt.Fprintf(w, testFeedXML, feedURL, feedURL)
|
|
})
|
|
srv := httptest.NewServer(mux)
|
|
defer srv.Close()
|
|
feedURL = srv.URL
|
|
|
|
channel, err := database.CreatePodcastChannel(m.DB, srv.URL+"/feed.xml")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
ctx := context.Background()
|
|
if err := m.RefreshChannel(ctx, channel.ID); err != nil {
|
|
t.Fatalf("RefreshChannel (1st): %v", err)
|
|
}
|
|
episodes1, _ := database.ListPodcastEpisodes(m.DB, channel.ID)
|
|
|
|
if err := m.RefreshChannel(ctx, channel.ID); err != nil {
|
|
t.Fatalf("RefreshChannel (2nd): %v", err)
|
|
}
|
|
episodes2, _ := database.ListPodcastEpisodes(m.DB, channel.ID)
|
|
|
|
if len(episodes2) != len(episodes1) {
|
|
t.Fatalf("refresh duplicated episodes: %d -> %d", len(episodes1), len(episodes2))
|
|
}
|
|
if episodes1[0].ID != episodes2[0].ID {
|
|
t.Error("episode ID changed across refresh")
|
|
}
|
|
}
|
|
|
|
func TestDownloadEpisode(t *testing.T) {
|
|
if _, err := exec.LookPath("ffprobe"); err != nil {
|
|
t.Skip("ffprobe not installed")
|
|
}
|
|
m, _ := newTestManager(t)
|
|
var feedURL string
|
|
mux := http.NewServeMux()
|
|
mux.HandleFunc("/feed.xml", func(w http.ResponseWriter, r *http.Request) {
|
|
fmt.Fprintf(w, testFeedXML, feedURL, feedURL)
|
|
})
|
|
mux.HandleFunc("/audio.mp3", func(w http.ResponseWriter, r *http.Request) {
|
|
w.Write([]byte("not really mp3 but bytes"))
|
|
})
|
|
srv := httptest.NewServer(mux)
|
|
defer srv.Close()
|
|
feedURL = srv.URL
|
|
|
|
channel, err := database.CreatePodcastChannel(m.DB, srv.URL+"/feed.xml")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
ctx := context.Background()
|
|
if err := m.RefreshChannel(ctx, channel.ID); err != nil {
|
|
t.Fatalf("RefreshChannel: %v", err)
|
|
}
|
|
episodes, _ := database.ListPodcastEpisodes(m.DB, channel.ID)
|
|
|
|
if err := m.DownloadEpisode(ctx, episodes[0].ID); err != nil {
|
|
t.Fatalf("DownloadEpisode: %v", err)
|
|
}
|
|
|
|
ep, err := database.GetPodcastEpisode(m.DB, episodes[0].ID)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if ep.Status != "completed" {
|
|
t.Fatalf("episode status = %q, want completed", ep.Status)
|
|
}
|
|
if !ep.MediaFileID.Valid {
|
|
t.Fatal("expected episode to have a media_file_id after download")
|
|
}
|
|
|
|
mf, err := database.GetMediaFile(m.DB, ep.MediaFileID.String)
|
|
if err != nil {
|
|
t.Fatalf("GetMediaFile: %v", err)
|
|
}
|
|
if mf.SizeBytes == 0 {
|
|
t.Error("downloaded media file has zero size")
|
|
}
|
|
}
|
|
|
|
func TestRecoverInterruptedDownloads(t *testing.T) {
|
|
m, _ := newTestManager(t)
|
|
channel, err := database.CreatePodcastChannel(m.DB, "http://example.com/feed.xml")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
ep, err := database.UpsertPodcastEpisode(m.DB, channel.ID, "guid1", "Ep", "", "", "http://example.com/a.mp3", 0)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := database.SetPodcastEpisodeStatus(m.DB, ep.ID, "downloading"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if err := m.RecoverInterruptedDownloads(); err != nil {
|
|
t.Fatalf("RecoverInterruptedDownloads: %v", err)
|
|
}
|
|
|
|
got, err := database.GetPodcastEpisode(m.DB, ep.ID)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got.Status != "new" {
|
|
t.Fatalf("status after recovery = %q, want new", got.Status)
|
|
}
|
|
}
|