feedparser.go
1975 bytes
1package feedparser
2
3import (
4 "encoding/xml"
5 "errors"
6 "fmt"
7 "io"
8 "log/slog"
9 "net/http"
10 "strings"
11
12 "github.com/paulrosania/go-charset/charset"
13)
14
15var (
16 ErrInvalidChannelID = errors.New("invalid channel ID")
17 ErrParseFailed = errors.New("failed to parse feed")
18)
19
20var urlFormat = "https://www.youtube.com/feeds/videos.xml?channel_id=%s"
21
22type Parser interface {
23 Parse(channelID string) (*Channel, error)
24}
25
26type parser struct {
27 log *slog.Logger
28}
29
30func NewParser(l *slog.Logger) *parser {
31 return &parser{
32 log: l,
33 }
34}
35
36func (p *parser) fetch(url string) (io.ReadCloser, error) {
37 req, err := http.NewRequest(http.MethodGet, url, nil)
38 if err != nil {
39 p.log.Error("Failed to create request", "call", "http.NewRequest", "error", err)
40 return nil, err
41 }
42
43 response, err := http.DefaultClient.Do(req)
44 if err != nil {
45 p.log.Error("Failed to fetch feed", "call", "http.Do", "error", err)
46 return nil, err
47 }
48
49 if response.StatusCode == http.StatusNotFound {
50 response.Body.Close()
51 return nil, fmt.Errorf("%w: %s", ErrInvalidChannelID, url)
52 } else if response.StatusCode != http.StatusOK {
53 response.Body.Close()
54 return nil, fmt.Errorf("failed to get feed with status %d", response.StatusCode)
55 }
56
57 return response.Body, nil
58}
59
60// Parse parses a YouTube channel XML feed from a channel ID
61func (p *parser) Parse(channelID string) (*Channel, error) {
62 url := fmt.Sprintf(urlFormat, channelID)
63 reader, err := p.fetch(url)
64 if err != nil {
65 return nil, err
66 }
67
68 defer reader.Close()
69 xmlDecoder := xml.NewDecoder(reader)
70 xmlDecoder.CharsetReader = charset.NewReader
71
72 var channel Channel
73 if err := xmlDecoder.Decode(&channel); err != nil {
74 p.log.Error("Failed to decode XML for RSS feed", "call", "xml.Decode", "error", err)
75 return nil, fmt.Errorf("%w: %s", ErrParseFailed, err.Error())
76 }
77 channel.ID = channelID
78 for _, video := range channel.Videos {
79 video.IsShort = strings.Contains(video.Link.Href, "/shorts/")
80 }
81
82 return &channel, nil
83}