Parent directory

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}