Parent directory

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}