channels.go
3023 bytes
1package handler
2
3import (
4 "context"
5 "errors"
6 "net/url"
7 "strings"
8
9 "github.com/TheEdgeOfRage/ytrssil-api/db"
10 "github.com/TheEdgeOfRage/ytrssil-api/models"
11)
12
13var ErrInvalidChannelURL = errors.New("invalid YouTube channel URL")
14
15// isChannelID reports whether s looks like a raw YouTube channel ID (UCxxxxxxxx…).
16func isChannelID(s string) bool {
17 return strings.HasPrefix(s, "UC") && len(s) == 24
18}
19
20func channelIDFromInput(input string) (string, error) {
21 parsedURL, err := url.ParseRequestURI(input)
22 if err != nil {
23 if strings.Contains(input, "://") {
24 return "", ErrInvalidChannelURL
25 }
26 return input, nil
27 }
28 if parsedURL.Scheme == "" {
29 return input, nil
30 }
31
32 host := strings.ToLower(parsedURL.Hostname())
33 if (parsedURL.Scheme != "http" && parsedURL.Scheme != "https") ||
34 (host != "youtube.com" && host != "www.youtube.com" && host != "m.youtube.com") {
35 return "", ErrInvalidChannelURL
36 }
37
38 path := strings.TrimSuffix(parsedURL.Path, "/")
39 if strings.HasPrefix(path, "/@") && !strings.Contains(path[2:], "/") && len(path) > 2 {
40 return path[1:], nil
41 }
42
43 const channelPathPrefix = "/channel/"
44 if strings.HasPrefix(path, channelPathPrefix) {
45 channelID := path[len(channelPathPrefix):]
46 if channelID != "" && !strings.Contains(channelID, "/") {
47 return channelID, nil
48 }
49 }
50
51 return "", ErrInvalidChannelURL
52}
53
54func (h *handler) SubscribeToChannel(ctx context.Context, channelID string) (*models.Channel, error) {
55 channelID, err := channelIDFromInput(channelID)
56 if err != nil {
57 return nil, err
58 }
59
60 if !isChannelID(channelID) {
61 // Treat input as a handle; normalise to @handle form for the API.
62 handle := channelID
63 if !strings.HasPrefix(handle, "@") {
64 handle = "@" + handle
65 }
66 resolved, err := h.youTubeClient.ResolveChannelID(ctx, handle)
67 if err != nil {
68 return nil, err
69 }
70 channelID = resolved
71 }
72
73 parsedChannel, err := h.parser.Parse(channelID)
74 if err != nil {
75 return nil, err
76 }
77
78 imageURL, err := h.youTubeClient.GetChannelImageURL(ctx, channelID)
79 if err != nil {
80 h.log.Warn("Failed to fetch channel image URL", "channelID", channelID, "error", err)
81 imageURL = ""
82 }
83
84 channel := models.Channel{
85 ID: channelID,
86 Name: parsedChannel.Name,
87 Subscribed: true,
88 ImageURL: imageURL,
89 EnableShorts: true,
90 }
91
92 err = h.db.SubscribeToChannel(ctx, channel)
93 if err != nil && !errors.Is(err, db.ErrChannelExists) {
94 return nil, err
95 }
96
97 return &channel, nil
98}
99
100func (h *handler) UnsubscribeFromChannel(ctx context.Context, channelID string) error {
101 return h.db.UnsubscribeFromChannel(ctx, channelID)
102}
103
104func (h *handler) ListChannels(ctx context.Context) ([]models.Channel, error) {
105 return h.db.ListChannels(ctx)
106}
107
108func (h *handler) GetChannelByID(ctx context.Context, channelID string) (*models.Channel, error) {
109 return h.db.GetChannelByID(ctx, channelID)
110}
111
112func (h *handler) ToggleChannelShorts(ctx context.Context, channelID string, enableShorts bool) error {
113 return h.db.ToggleChannelShorts(ctx, channelID, enableShorts)
114}