aboutsummaryrefslogtreecommitdiff
path: root/main.go
blob: b6842ce67b65b70a5350503da5058f999994de2c (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
package main

import (
	"bufio"
	"encoding/xml"
	"fmt"
	"io"
	"net/http"
	"os"
	"strings"
)

type Item struct {
	GUID        string `xml:"guid"`
	PubDate     string `xml:"pubDate"`
	Title       string `xml:"title"`
	Description string `xml:"description"`
	Link        string `xml:"link"`
	Author      string `xml:"author"`
}

type Feed struct {
	XMLName xml.Name `xml:"rss"`
	Version string   `xml:"version,attr"`
	Items   []Item   `xml:"channel>item"`
	Title   string   `xml:"channel>title"`
}

func Parse(r io.Reader) ([]Item, error) {
	var f Feed
	dec := xml.NewDecoder(r)
	err := dec.Decode(&f)
	return f.Items, err
}

func subscribedTo() ([]string, error) {
	var chans []string

	p := os.ExpandEnv("$HOME/.youtube-feed")
	f, err := os.Open(p)
	if err != nil {
		return nil, fmt.Errorf("failed reading %s: %s", p, err)
	}
	defer f.Close()

	scan := bufio.NewScanner(f)

	for scan.Scan() {
		chans = append(chans, strings.TrimSpace(scan.Text()))
	}
	if err := scan.Err(); err != nil {
		return nil, fmt.Errorf("failed reading %s: %s", p, err)
	}

	return chans, err
}

const retries = 5

func getLatestVideos(ytChan string, itemChan chan<- Item, status chan<- error) {
	var resp *http.Response
	var err error

	for i := 0; i < retries; i++ {
		resp, err = http.Get("http://gdata.youtube.com/feeds/base/users/" + ytChan + "/uploads?alt=rss&v=2&orderby=published&client=ytapi-youtube-profile")
		if err == nil {
			if resp.StatusCode == http.StatusOK {
				break
			} else {
				resp.Body.Close()
				err = fmt.Errorf("Received status %d, expected %d", resp.StatusCode, http.StatusOK)
			}
		}
	}

	if err != nil {
		status <- fmt.Errorf("Failed getting channel feed for %s: %s", ytChan, err)
		return
	}
	defer resp.Body.Close()

	items, err := Parse(resp.Body)
	if err != nil {
		err = fmt.Errorf("Failed parsing feed of %s: %s", ytChan, err)
	}

	for _, it := range items {
		itemChan <- it
	}

	status <- err
}

func main() {
	subs, err := subscribedTo()
	if err != nil {
		fmt.Fprintf(os.Stderr, "Failed reading channel list: %s\n", err)
		os.Exit(1)
	}

	itemChan := make(chan Item)
	status := make(chan error)

	var items []Item
	go func() {
		for it := range itemChan {
			it.Title = fmt.Sprintf("[%s] %s", it.Author, it.Title)
			items = append(items, it)
		}
	}()

	for _, ytChan := range subs {
		go getLatestVideos(ytChan, itemChan, status)
	}

	for _ = range subs {
		if err := <-status; err != nil {
			fmt.Fprintln(os.Stderr, "Warning:", err)
		}
	}
	close(itemChan)

	feed := Feed{Title: "Combined YouTube Feed", Items: items, Version: "2.0"}
	enc := xml.NewEncoder(os.Stdout)
	if err := enc.Encode(feed); err != nil {
		fmt.Fprintf(os.Stderr, "Failed writing feed: %s\n", err)
		os.Exit(1)
	}
}