diff --git a/content/media.go b/content/media.go index 7bdd7dd..79629dd 100644 --- a/content/media.go +++ b/content/media.go @@ -8,21 +8,19 @@ import ( // content type should be able to be set from the configuration const ( - podcastContent MediaType = "podcast" - announcementContent MediaType = "announcement" - webRadioContent MediaType = "web_radio" - fileContent MediaType = "file" - folderContent MediaType = "folder" + podcastContent MediaType = "podcast" + webRadioContent MediaType = "web_radio" + fileContent MediaType = "file" + folderContent MediaType = "folder" ) type MediaType string var MediaTypeMap = map[MediaType]Media{ - podcastContent: new(Podcast), - announcementContent: new(Announcement), - webRadioContent: new(WebRadio), - fileContent: new(LocalFile), - folderContent: new(Folder), + podcastContent: new(Podcast), + webRadioContent: new(WebRadio), + fileContent: new(LocalFile), + folderContent: new(Folder), } // Media is the interface to represent playing any type of audio. diff --git a/content/podcast.go b/content/podcast.go index bfdaafd..6ad581b 100644 --- a/content/podcast.go +++ b/content/podcast.go @@ -1,23 +1,114 @@ package content -import log "github.com/sirupsen/logrus" +import ( + "github.com/mmcdole/gofeed" + "github.com/pkg/errors" + log "github.com/sirupsen/logrus" + "os/exec" +) + +const ( + playOrderNewest PlayOrder = "newest" + playOrderOldest PlayOrder = "oldest" + playOrderRandom PlayOrder = "random" +) + +var pods podcasts // holds the feed data for podcasts +var podcastStream streamPlayer type Podcast struct { - Name string - URL string - Path string - Content []byte + Name string + URL string + Player streamPlayer + PlayOrder PlayOrder // options: newest, oldest, random } +type PlayOrder string + +// Get parses a podcast feed and sets the most recent episode as the Podcast content. func (p *Podcast) Get() error { - panic("implement me") + var ep episode + parser := gofeed.NewParser() + feed, err := parser.ParseURL(p.URL) + if err != nil { + return err + } + // traverse links + for _, item := range feed.Items { + pods.Episodes = append(pods.Episodes, item) + } + + switch p.PlayOrder { + case playOrderNewest: + ep = pods.getNewestEpisode() + break + case playOrderOldest: + log.Panic("implement me") + //ep = pods.getOldestEpisode() + case playOrderRandom: + log.Panic("implement me") + //ep = pods.getRandomEpisode() + } + + // setup podcast stream + log.Infof("extension: %v", ep.EpExtension) + podcastStream.playerName = streamPlayerName + podcastStream.url = ep.EpURL + podcastStream.command = exec.Command(podcastStream.playerName, "-quiet", podcastStream.url) + podcastStream.in, err = podcastStream.command.StdinPipe() + if err != nil { + return errors.Wrap(err, "error creating standard pipe in") + } + podcastStream.out, err = podcastStream.command.StdoutPipe() + if err != nil { + return errors.Wrap(err, "error creating standard pipe out") + } + + podcastStream.isPlaying = false + + p.Player = podcastStream + + return nil } +// Play sends the audio to the output. It caches a played episode in the cache ofr later checks. func (p *Podcast) Play() error { - panic("implement me") + log.Infof("streaming from %v ", p.URL) + if !p.Player.isPlaying { + err := p.Player.command.Start() + if err != nil { + return errors.Wrap(err, "error starting podcast streamPlayer") + } + p.Player.isPlaying = true + done := make(chan bool) + func() { + p.Player.pipeChan <- p.Player.out + done <- true + }() + <-done + } + return nil } func (p *Podcast) Stop() error { - log.Infof("Stopping stream from %v ", p.Path) + log.Infof("Stopping stream from %v ", p.URL) + if p.Player.isPlaying { + p.Player.isPlaying = false + _, err := p.Player.in.Write([]byte("q")) + if err != nil { + log.WithError(err).Error("error stopping web radio streamPlayerName: w.Player.in.Write()") + } + err = p.Player.in.Close() + if err != nil { + log.WithError(err).Error("error stopping web radio streamPlayerName: w.Player.in.Close()") + } + err = p.Player.out.Close() + if err != nil { + log.WithError(err).Error("error stopping web radio streamPlayerName: w.Player.out.Close()") + } + p.Player.command = nil + + p.Player.url = "" + } return nil } diff --git a/content/podcast_episodes.go b/content/podcast_episodes.go new file mode 100644 index 0000000..001ac89 --- /dev/null +++ b/content/podcast_episodes.go @@ -0,0 +1,65 @@ +package content + +import ( + "github.com/mmcdole/gofeed" + "math/rand" + "time" +) + +type podcasts struct { + Episodes []*gofeed.Item + // add podcast cache +} + +func (p *podcasts) getNewestEpisode() episode { + var newestEpisode episode + var date *time.Time + date = p.Episodes[0].PublishedParsed + for _, ep := range p.Episodes { + // TODO if played, log that it's in the cache, and skip to the next episode + if ep.PublishedParsed.After(*date) || ep.PublishedParsed.Equal(*date) { + date = ep.PublishedParsed + newestEpisode.Item = ep + newestEpisode.EpURL = ep.Enclosures[0].URL + newestEpisode.EpExtension = ep.Enclosures[0].Type + } + } + return newestEpisode +} + +func (p *podcasts) getOldestEpisode() *episode { + var oldestEpisode *episode + var date *time.Time + // TODO if played, log that it's in the cache, and skip to the next episode + for _, ep := range p.Episodes { + date = p.Episodes[0].PublishedParsed + if ep.PublishedParsed.Before(*date) || ep.PublishedParsed.Equal(*date) { + date = ep.PublishedParsed + oldestEpisode.Item = ep + oldestEpisode.EpURL = ep.Enclosures[0].URL + oldestEpisode.EpExtension = ep.Enclosures[0].Type + } + } + return oldestEpisode +} + +func (p *podcasts) getRandomEpisode() *episode { + var randomEpisode *episode + rand.Seed(time.Now().UnixNano()) + item := p.Episodes[rand.Intn(len(p.Episodes))] + // TODO if played, log that it's in the cache, and skip to the next episode + randomEpisode.Item = item + randomEpisode.EpExtension = item.Enclosures[0].Type + randomEpisode.EpURL = item.Enclosures[0].URL + return randomEpisode +} + +func (p *podcasts) checkIfPlayed(guid string) bool { + return false +} + +type episode struct { + Item *gofeed.Item // Keep this to hold the additional data + EpExtension string + EpURL string +} diff --git a/content/podcast_test.go b/content/podcast_test.go index 5a945dc..2466109 100644 --- a/content/podcast_test.go +++ b/content/podcast_test.go @@ -22,10 +22,8 @@ func TestPodcast_Get(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { p := &Podcast{ - Name: tt.fields.Name, - URL: tt.fields.URL, - Path: tt.fields.Path, - Content: tt.fields.Content, + Name: tt.fields.Name, + URL: tt.fields.URL, } if err := p.Get(); (err != nil) != tt.wantErr { t.Errorf("Get() error = %v, wantErr %v", err, tt.wantErr) @@ -51,10 +49,8 @@ func TestPodcast_Play(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { p := &Podcast{ - Name: tt.fields.Name, - URL: tt.fields.URL, - Path: tt.fields.Path, - Content: tt.fields.Content, + Name: tt.fields.Name, + URL: tt.fields.URL, } if err := p.Play(); (err != nil) != tt.wantErr { t.Errorf("Play() error = %v, wantErr %v", err, tt.wantErr) @@ -79,10 +75,8 @@ func TestPodcast_Stop(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { p := &Podcast{ - Name: tt.fields.Name, - URL: tt.fields.URL, - Path: tt.fields.Path, - Content: tt.fields.Content, + Name: tt.fields.Name, + URL: tt.fields.URL, } p.Stop() }) diff --git a/content/program.go b/content/program.go index 24d9bb3..cf30e0c 100644 --- a/content/program.go +++ b/content/program.go @@ -20,8 +20,6 @@ func (p *Program) GetMedia() Media { func (p *Program) mediaFactory() Media { m := MediaTypeMap[p.Type] switch m.(type) { - case *Announcement: - panic("implement me") case *Folder: folder := m.(*Folder) folder.Name = p.Name @@ -35,7 +33,12 @@ func (p *Program) mediaFactory() Media { log.Debugf("returning LocalFile: %v", formatter.StructToString(file)) return file case *Podcast: - panic("implement me") + podcast := m.(*Podcast) + podcast.Name = p.Name + podcast.URL = p.Source + podcast.PlayOrder = "newest" // TODO: Add support for random, oldest, and set from PlayOrder from config. + log.Debugf("returning podcast: %v", formatter.StructToString(podcast)) + return podcast case *WebRadio: radio := m.(*WebRadio) radio.Name = p.Name diff --git a/content/program_test.go b/content/program_test.go index c1f9c68..f1ddf20 100644 --- a/content/program_test.go +++ b/content/program_test.go @@ -62,6 +62,21 @@ func TestProgram_GetMedia(t *testing.T) { Type: "file", }).GetMedia(), }, + { + name: "Success: returns podcast", + fields: fields{ + program: &Program{ + Name: "Tech Won't Save Us", + Source: "https://feeds.buzzsprout.com/1004689.rss", + Timeslot: &Timeslot{ + Begin: "11:00PM", + End: "11:30PM", + }, + Type: "podcast", + }, + }, + want: nil, + }, { name: "Success: returns web radio", fields: fields{ diff --git a/content/stream.go b/content/stream.go new file mode 100644 index 0000000..9b7d75d --- /dev/null +++ b/content/stream.go @@ -0,0 +1,16 @@ +package content + +import ( + "io" + "os/exec" +) + +type streamPlayer struct { + playerName string + url string + isPlaying bool + command *exec.Cmd + in io.WriteCloser + out io.ReadCloser + pipeChan chan io.ReadCloser +} diff --git a/content/web_radio.go b/content/web_radio.go index e3f4579..64a8416 100644 --- a/content/web_radio.go +++ b/content/web_radio.go @@ -7,45 +7,40 @@ package content import ( "github.com/pkg/errors" log "github.com/sirupsen/logrus" - "io" "os/exec" ) -const player = "mpv" +const streamPlayerName = "mpv" type WebRadio struct { Name string URL string - Player webRadioCommand + Player streamPlayer } -type webRadioCommand struct { - playerName string - url string - isPlaying bool - command *exec.Cmd - in io.WriteCloser - out io.ReadCloser - pipeChan chan io.ReadCloser -} - -var wrc webRadioCommand +var webRadioStream streamPlayer func (w *WebRadio) Get() error { var err error - wrc.playerName = player - wrc.url = w.URL - wrc.command = exec.Command(wrc.playerName, "-quiet", "-playlist", wrc.url) - wrc.in, err = wrc.command.StdinPipe() + + // setup web radio stream + webRadioStream.playerName = streamPlayerName + webRadioStream.url = w.URL + webRadioStream.command = exec.Command(webRadioStream.playerName, "-quiet", "-playlist", webRadioStream.url) + + webRadioStream.in, err = webRadioStream.command.StdinPipe() if err != nil { return errors.Wrap(err, "error creating standard pipe in") } - wrc.out, err = wrc.command.StdoutPipe() + + webRadioStream.out, err = webRadioStream.command.StdoutPipe() if err != nil { return errors.Wrap(err, "error creating standard pipe out") } - wrc.isPlaying = false - w.Player = wrc + + webRadioStream.isPlaying = false + + w.Player = webRadioStream return nil } @@ -54,7 +49,7 @@ func (w *WebRadio) Play() error { if !w.Player.isPlaying { err := w.Player.command.Start() if err != nil { - return errors.Wrap(err, "error starting web radio player") + return errors.Wrap(err, "error starting web radio streamPlayer") } w.Player.isPlaying = true done := make(chan bool) @@ -73,15 +68,15 @@ func (w *WebRadio) Stop() error { w.Player.isPlaying = false _, err := w.Player.in.Write([]byte("q")) if err != nil { - log.WithError(err).Error("error stopping web radio player: w.Player.in.Write()") + log.WithError(err).Error("error stopping web radio streamPlayerName: w.Player.in.Write()") } err = w.Player.in.Close() if err != nil { - log.WithError(err).Error("error stopping web radio player: w.Player.in.Close()") + log.WithError(err).Error("error stopping web radio streamPlayerName: w.Player.in.Close()") } err = w.Player.out.Close() if err != nil { - log.WithError(err).Error("error stopping web radio player: w.Player.out.Close()") + log.WithError(err).Error("error stopping web radio streamPlayerName: w.Player.out.Close()") } w.Player.command = nil diff --git a/go.mod b/go.mod index 6dcd215..df8b419 100644 --- a/go.mod +++ b/go.mod @@ -7,6 +7,7 @@ require ( github.com/faiface/beep v1.1.0 github.com/h2non/filetype v1.1.3 github.com/jmillerv/go-utilities v0.0.0-20211009175413-077cd5304cea + github.com/mmcdole/gofeed v1.1.3 github.com/pkg/errors v0.9.1 github.com/sirupsen/logrus v1.8.1 github.com/spf13/viper v1.8.1 @@ -15,6 +16,8 @@ require ( ) require ( + github.com/PuerkitoBio/goquery v1.5.1 // indirect + github.com/andybalholm/cascadia v1.1.0 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/fsnotify/fsnotify v1.4.9 // indirect @@ -24,10 +27,14 @@ require ( github.com/icza/bitio v1.0.0 // indirect github.com/jfreymuth/oggvorbis v1.0.1 // indirect github.com/jfreymuth/vorbis v1.0.0 // indirect + github.com/json-iterator/go v1.1.11 // indirect github.com/magiconair/properties v1.8.5 // indirect github.com/mewkiz/flac v1.0.7 // indirect github.com/mewkiz/pkg v0.0.0-20190919212034-518ade7978e2 // indirect github.com/mitchellh/mapstructure v1.4.1 // indirect + github.com/mmcdole/goxpp v0.0.0-20181012175147-0068e33feabf // indirect + github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect + github.com/modern-go/reflect2 v1.0.1 // indirect github.com/pelletier/go-toml v1.9.3 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/russross/blackfriday/v2 v2.0.1 // indirect @@ -40,6 +47,7 @@ require ( golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6 // indirect golang.org/x/image v0.0.0-20190802002840-cff245a6509b // indirect golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028 // indirect + golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4 // indirect golang.org/x/sys v0.1.0 // indirect golang.org/x/text v0.3.5 // indirect gopkg.in/ini.v1 v1.62.0 // indirect diff --git a/go.sum b/go.sum index ca3830d..a74490e 100644 --- a/go.sum +++ b/go.sum @@ -40,6 +40,10 @@ dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7 github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/DATA-DOG/go-sqlmock v1.3.3/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM= +github.com/PuerkitoBio/goquery v1.5.1 h1:PSPBGne8NIUWw+/7vFBV+kG2J/5MOjbzc7154OaKCSE= +github.com/PuerkitoBio/goquery v1.5.1/go.mod h1:GsLWisAFVj4WgDibEWF4pvYnkVQBpKBKeU+7zCJoLcc= +github.com/andybalholm/cascadia v1.1.0 h1:BuuO6sSfQNFRu1LppgbD25Hr2vLYW25JvxHs5zzsLTo= +github.com/andybalholm/cascadia v1.1.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y= github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de h1:FxWPpzIjnTlhPwqqXc4/vE0f7GvRjuAsbW+HOIe8KnA= github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de/go.mod h1:DCaWoUhZrYW9p1lxo/cm8EmUOOzAPSEZNGF2DK1dJgw= @@ -191,6 +195,8 @@ github.com/jfreymuth/vorbis v1.0.0 h1:SmDf783s82lIjGZi8EGUUaS7YxPHgRj4ZXW/h7rUi7 github.com/jfreymuth/vorbis v1.0.0/go.mod h1:8zy3lUAm9K/rJJk223RKy6vjCZTWC61NA2QD06bfOE0= github.com/jmillerv/go-utilities v0.0.0-20211009175413-077cd5304cea h1:FJhMTpZMEcqorl+oKIbhgOFCwoSpkS+2y6KeUz/PIb0= github.com/jmillerv/go-utilities v0.0.0-20211009175413-077cd5304cea/go.mod h1:okN4LUTlx7FrTlYcVKXZs059xgpOldGWZupW2MeJXuE= +github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.11 h1:uVUAXhF2To8cbw/3xN3pxj6kk7TYKs98NIrTqPlMWAQ= github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= @@ -225,8 +231,14 @@ github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:F github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.4.1 h1:CpVNEelQCZBooIPDn+AR3NpivK/TIKU8bDxdASFVQag= github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mmcdole/gofeed v1.1.3 h1:pdrvMb18jMSLidGp8j0pLvc9IGziX4vbmvVqmLH6z8o= +github.com/mmcdole/gofeed v1.1.3/go.mod h1:QQO3maftbOu+hiVOGOZDRLymqGQCos4zxbA4j89gMrE= +github.com/mmcdole/goxpp v0.0.0-20181012175147-0068e33feabf h1:sWGE2v+hO0Nd4yFU/S/mDBM5plIU8v/Qhfz41hkDIAI= +github.com/mmcdole/goxpp v0.0.0-20181012175147-0068e33feabf/go.mod h1:pasqhqstspkosTneA62Nc+2p9SOBBYAPbnmRRWPQ0V8= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pelletier/go-toml v1.9.3 h1:zeC5b1GviRUyKYd6OJPvBU/mcVDVoL1OhT17FCt5dSQ= @@ -342,6 +354,7 @@ golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -377,6 +390,7 @@ golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc= +golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4 h1:4nGaVu0QrbjT/AK2PRLuQfQuh6DJve+pELhqTdAj3x0= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=