clone

a desktop tv and program guide for over-the-air broadcasts: what's on air, what your tuner gets, live tv, recordings

main
airwaves / internal / vchan / ytstatus.go
ytstatus.go 3.8K
1package vchan23import (4 "path/filepath"5 "sync"6 "time"78 "airwaves/internal/youtube"9)1011// YouTubeConfig is a YouTube channel's youtube.json, with the settings it12// leaves out filled in.13type YouTubeConfig = ytConfig1415// ReadYouTubeConfig reads the youtube.json in a YouTube channel's folder.16// The settings come back, defaults included, even with an error.17func ReadYouTubeConfig(dir string) (YouTubeConfig, error) {18 return readYtConfig(filepath.Join(dir, youtubeConfig))19}2021// YouTubeDefaults are the settings a youtube.json may leave out.22func YouTubeDefaults() YouTubeConfig {23 c, _ := readYtConfig("") // the defaults, and an error for the missing file24 return c25}2627// YouTubeFiles are the files a YouTube channel keeps in its folder: its28// youtube.json first, then the hidden catalog and schedule, with their29// copies being written.30func YouTubeFiles() []string {31 return []string{youtubeConfig, ytCatalogFile, ytCatalogFile + ".tmp", ytPlayoutFile, ytPlayoutFile + ".tmp"}32}3334// YouTubeStatus is how a YouTube channel is getting on.35type YouTubeStatus struct {36 Config YouTubeConfig37 Err error // the config's38 Sources []YouTubeSource // as configured39 Busy bool // a refresh is running40}4142// YouTubeSource is one of the YouTube channels a channel plays, or the43// playlist it plays.44type YouTubeSource struct {45 Channel string // as in youtube.json46 URL string // its uploads tab, or the playlist's page47 ID, Name string // once listed: a playlist's ID and title48 Videos int // in the catalog49 Playable int // of those, the ones that fit the channel's settings now, age included; a playlist's that can play50 // Owner and OwnerID are a playlist's channel, and Image its picture,51 // its first video's thumbnail.52 Owner, OwnerID string53 Image string54 Listed time.Time55 // Listing is set while it's being listed, or waiting its turn to be;56 // Since is when that was first seen.57 Listing bool58 Since time.Time59 // Failed is when the last listing failed, if none has worked since.60 Failed time.Time61}6263// Status reports the channel's sources: what their catalogs hold, and64// listings under way or failed. Like the guide, it starts a refresh65// that's due.66func (y *YouTube) Status() YouTubeStatus {67 y.mu.Lock()68 defer y.mu.Unlock()69 y.tend()70 now := y.clock()71 st := YouTubeStatus{Config: y.cfg, Err: y.err, Busy: y.busy}72 if y.err != nil {73 return st74 }75 // tend has started a refresh for any source due a listing.76 add := func(src YouTubeSource) {77 if t := y.tried[src.URL]; t.After(src.Listed) {78 src.Failed = t79 }80 src.Since = listingSince(y.Dir+"\x00"+src.URL, src.Listing, now)81 st.Sources = append(st.Sources, src)82 }83 if y.isPlaylist() {84 url, _ := youtube.Playlist(y.cfg.Playlist)85 if s := y.source(url); s != nil {86 seq, _ := y.sequence()87 src := YouTubeSource{Channel: y.cfg.Playlist, URL: s.URL, ID: s.ID, Name: s.Name, Videos: len(s.Videos),88 Playable: len(seq), Owner: s.Owner, OwnerID: s.OwnerID, Listed: s.Listed, Listing: y.listDue(s, now)}89 if len(seq) > 0 {90 src.Image = youtube.Thumbnail(seq[0].ID)91 }92 add(src)93 }94 return st95 }96 seen := map[string]bool{}97 for _, c := range y.cfg.Channels {98 s := y.source(youtube.VideosURL(c))99 if s == nil || seen[s.URL] {100 continue101 }102 seen[s.URL] = true103 add(YouTubeSource{Channel: c, URL: s.URL, ID: s.ID, Name: s.Name, Videos: len(s.Videos),104 Playable: y.fits(s), Listed: s.Listed, Listing: y.listDue(s, now)})105 }106 return st107}108109// listings holds when each listing under way was first seen, by folder110// and source, so it survives the channel being made again after an edit.111var listings sync.Map112113func listingSince(key string, on bool, now time.Time) time.Time {114 if !on {115 listings.Delete(key)116 return time.Time{}117 }118 t, _ := listings.LoadOrStore(key, now)119 return t.(time.Time)120}