[
  {
    "path": ".gitignore",
    "content": "# Binaries for programs and plugins\n*.exe\n*.exe~\n*.dll\n*.so\n*.dylib\n\n# binary\npodcast-cli*\nmain*\n!main.go\n\n# Test binary, built with `go test -c`\n*.test\n\n# Output of the go coverage tool, specifically when used with LiteIDE\n*.out\n\n# Dependency directories (remove the comment below to include it)\n# vendor/\n\n# IDE\n.idea/\n\n# Project\n.cache/\n\n# Vim\n*.swp\n*.swo\n*.swn\n*.un~\n"
  },
  {
    "path": ".vscode/launch.json",
    "content": "{\n    // Use IntelliSense to learn about possible attributes.\n    // Hover to view descriptions of existing attributes.\n    // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387\n    \"version\": \"0.2.0\",\n    \"configurations\": [\n        {\n            \"name\": \"Launch file\",\n            \"type\": \"go\",\n            \"request\": \"launch\",\n            \"mode\": \"auto\",\n            \"program\": \"${workspaceFolder}\"\n        }\n    ]\n}"
  },
  {
    "path": "README.md",
    "content": "<p align=\"center\"><img width=\"200px\" src=\"/resources/img/logo.png\" alt=\"podcast-cli\"/></p>\n\n\n___\n\nTop-like interface for listening to podcasts\n`podcast-cli` lets you play your favourite podcasts from the terminal:\n<p align=\"center\"><img src=\"/resources/img/demo.gif\" alt=\"podcast-cli\"/></p>\n\n`podcast-cli` is entirely built with Go, you can run it on `Linux`, `Mac OS` and `Windows`.\n\n## Install\nFetch the [latest release](https://github.com/goulinkh/podcast-cli/releases)\n\n#### Linux\n\n```bash\nsudo wget https://github.com/goulinkh/podcast-cli/releases/download/1.3.1/podcast-cli-1.3.1-linux-amd64 -O /usr/local/bin/podcast-cli\nsudo chmod +x /usr/local/bin/podcast-cli\n```\n\n#### OS X\n\n```bash\nsudo curl -Lo /usr/local/bin/podcast-cli https://github.com/goulinkh/podcast-cli/releases/download/1.3.1/podcast-cli-1.3.1-darwin-amd64\nsudo chmod +x /usr/local/bin/podcast-cli\n```\n\n## Usage\n`podcast-cli` requires no arguments and uses your default internet settings to access the internet.\n\n### Options\n\n| Options                  | Description                                 |\n| ------------------------ | ------------------------------------------- |\n| `-h or  --help`          | Print help information                      |\n| `-s or --search <query>` | List podcasts that matches the search query |\n| `-r or --rss <url>`    | Custom podcast rss url source               |\n| `-o or --offset <episode number starting with 0>` | Play episode number                         |\n\n### Keybindings\n\n| Key        | Action   |\n| ---------- | -------- |\n| `Enter`    | Select   |\n| `p, Space` | Pause    |\n| `Esc`      | Back     |\n| `Right`    | +10s     |\n| `Left`     | -10s     |\n| `u`        | Slowdown |\n| `d`        | Speedup  |\n| `q`        | Exit     |\n\n\n## Issues\n\n* Unable to get audio length of a remote content, I have to download the audio file before playing it\n\n"
  },
  {
    "path": "audio-player/player.go",
    "content": "package audioplayer\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"io/ioutil\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"os\"\n\t\"path\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/faiface/beep\"\n\t\"github.com/faiface/beep/effects\"\n\t\"github.com/faiface/beep/mp3\"\n\t\"github.com/faiface/beep/speaker\"\n\t\"github.com/faiface/beep/wav\"\n\t\"github.com/goulinkh/podcast-cli/config\"\n\titunesapi \"github.com/goulinkh/podcast-cli/itunes-api\"\n)\n\ntype AudioPlayer struct {\n\tStreamer beep.StreamSeekCloser\n\tFormat   beep.Format\n}\n\nvar (\n\tMainCtrl  *beep.Ctrl\n\tVolume    *effects.Volume\n\tresampler *beep.Resampler\n\n\tStreamer beep.StreamSeekCloser\n\tFormat   beep.Format\n)\n\nfunc init() {\n\tVolume = &effects.Volume{Base: 2}\n}\n\nfunc fetchContent(URL string, filepath string, directory string) error {\n\n\t_, err := ioutil.ReadFile(filepath)\n\tif err == nil {\n\t\treturn nil\n\t}\n\tresponse, err := http.Get(URL)\n\tif err != nil {\n\t\treturn err\n\t}\n\taudio, err := ioutil.ReadAll(response.Body)\n\tif err != nil {\n\t\treturn err\n\t}\n\tos.MkdirAll(directory, 0755)\n\t// download content if not in .cache\n\terr = ioutil.WriteFile(filepath, audio, 0755)\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n\n// PlaySound play the given audio url, supported Formats: mp3, wav\nfunc PlaySound(e *itunesapi.Episode) error {\n\tif Streamer != nil {\n\t\tspeaker.Lock()\n\t\tStreamer.Close()\n\t\tspeaker.Unlock()\n\t}\n\tURL := e.AudioURL\n\tfilename := fmt.Sprintf(\"%s.mp3\", e.Id)\n\tdirectory := config.CachePath\n\tfilename = url.PathEscape(path.Clean(strings.ReplaceAll(filename, \":\", \"\")))\n\tfilepath := path.Join(directory, filename)\n\tfile, err := os.Open(filepath)\n\tif err != nil {\n\t\terr = fetchContent(URL, filepath, directory)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\tfile, err = os.Open(filepath)\n\tif err != nil {\n\t\treturn err\n\t}\n\tStreamer, Format, err = mp3.Decode(file)\n\tif err != nil {\n\t\tStreamer, Format, err = wav.Decode(file)\n\t}\n\tif err != nil {\n\t\treturn errors.New(\"Unsupported audio format\")\n\t}\n\tsr := Format.SampleRate * 2\n\tspeaker.Init(sr, sr.N(time.Millisecond*500))\n\n\tstreamer := beep.Resample(4, Format.SampleRate, sr, Streamer)\n\tMainCtrl = &beep.Ctrl{Streamer: streamer}\n\tresampler = beep.ResampleRatio(4, 1, MainCtrl)\n\tVolume = &effects.Volume{Streamer: resampler, Base: 2}\n\tspeaker.Play(Volume)\n\te.DurationInMilliseconds = int(float32(Streamer.Len())/float32(Format.SampleRate)) * 1000\n\treturn nil\n}\n\nfunc PauseSong(state bool) {\n\tspeaker.Lock()\n\tMainCtrl.Paused = state\n\tspeaker.Unlock()\n}\n\nfunc IncreaseSpeed() {\n\tspeed := resampler.Ratio() * 1.100000e+000\n\tif speed >= 1.800000e+000 {\n\t\treturn\n\t}\n\tspeaker.Lock()\n\tresampler.SetRatio(speed)\n\tspeaker.Unlock()\n}\n\nfunc DecreaseSpeed() {\n\tspeed := resampler.Ratio() * 0.900000e+000\n\tif speed <= 1.000000e+000 {\n\t\treturn\n\t}\n\tspeaker.Lock()\n\tresampler.SetRatio(speed)\n\tspeaker.Unlock()\n}\n\nfunc Seek(pos int) error {\n\tif MainCtrl != nil {\n\t\tspeaker.Lock()\n\t\terr := Streamer.Seek(Format.SampleRate.N(time.Second) * pos)\n\t\tspeaker.Unlock()\n\t\treturn err\n\t}\n\treturn nil\n}\n\nfunc SetVolume(percent int) {\n\tif percent > 100 {\n\t\treturn\n\t}\n\n\tif percent == 0 {\n\t\tVolume.Silent = true\n\t} else {\n\t\tVolume.Silent = false\n\t\tVolume.Volume = -float64(100-percent) / 100.0 * 5\n\t}\n}\n\nfunc Position() int {\n\treturn int(Format.SampleRate.D(Streamer.Position()).Round(time.Second).Seconds())\n}\n"
  },
  {
    "path": "config/main.go",
    "content": "package config\n\nimport (\n\t\"os\"\n\t\"path\"\n)\n\nvar CachePath = path.Join(os.TempDir(), \"podcast-cli/\")\n"
  },
  {
    "path": "go.mod",
    "content": "module github.com/goulinkh/podcast-cli\n\ngo 1.14\n\nrequire (\n\tgithub.com/PuerkitoBio/goquery v1.5.1\n\tgithub.com/akamensky/argparse v1.2.1 // indirect\n\tgithub.com/faiface/beep v1.0.2\n\tgithub.com/gizak/termui/v3 v3.1.0\n\tgithub.com/tidwall/gjson v1.6.0\n)\n"
  },
  {
    "path": "go.sum",
    "content": "github.com/PuerkitoBio/goquery v1.5.1 h1:PSPBGne8NIUWw+/7vFBV+kG2J/5MOjbzc7154OaKCSE=\ngithub.com/PuerkitoBio/goquery v1.5.1/go.mod h1:GsLWisAFVj4WgDibEWF4pvYnkVQBpKBKeU+7zCJoLcc=\ngithub.com/akamensky/argparse v1.2.1 h1:YMYF1VMku+dnz7TVTJpYhsCXHSYCVMAIcKaBbjwbvZo=\ngithub.com/akamensky/argparse v1.2.1/go.mod h1:S5kwC7IuDcEr5VeXtGPRVZ5o/FdhcMlQz4IZQuw64xA=\ngithub.com/andybalholm/cascadia v1.1.0 h1:BuuO6sSfQNFRu1LppgbD25Hr2vLYW25JvxHs5zzsLTo=\ngithub.com/andybalholm/cascadia v1.1.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y=\ngithub.com/faiface/beep v1.0.2 h1:UB5DiRNmA4erfUYnHbgU4UB6DlBOrsdEFRtcc8sCkdQ=\ngithub.com/faiface/beep v1.0.2/go.mod h1:1yLb5yRdHMsovYYWVqYLioXkVuziCSITW1oarTeduQM=\ngithub.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg=\ngithub.com/gdamore/tcell v1.1.1/go.mod h1:K1udHkiR3cOtlpKG5tZPD5XxrF7v2y7lDq7Whcj+xkQ=\ngithub.com/gizak/termui/v3 v3.1.0 h1:ZZmVDgwHl7gR7elfKf1xc4IudXZ5qqfDh4wExk4Iajc=\ngithub.com/gizak/termui/v3 v3.1.0/go.mod h1:bXQEBkJpzxUAKf0+xq9MSWAvWZlE7c+aidmyFlkYTrY=\ngithub.com/gopherjs/gopherjs v0.0.0-20180628210949-0892b62f0d9f/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=\ngithub.com/gopherjs/gopherjs v0.0.0-20180825215210-0210a2f0f73c/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=\ngithub.com/gopherjs/gopherwasm v0.1.1/go.mod h1:kx4n9a+MzHH0BJJhvlsQ65hqLFXDO/m256AsaDPQ+/4=\ngithub.com/gopherjs/gopherwasm v1.0.0/go.mod h1:SkZ8z7CWBz5VXbhJel8TxCmAcsQqzgWGR/8nMhyhZSI=\ngithub.com/hajimehoshi/go-mp3 v0.1.1 h1:Y33fAdTma70fkrxnc9u50Uq0lV6eZ+bkAlssdMmCwUc=\ngithub.com/hajimehoshi/go-mp3 v0.1.1/go.mod h1:4i+c5pDNKDrxl1iu9iG90/+fhP37lio6gNhjCx9WBJw=\ngithub.com/hajimehoshi/oto v0.1.1/go.mod h1:hUiLWeBQnbDu4pZsAhOnGqMI1ZGibS6e2qhQdfpwz04=\ngithub.com/hajimehoshi/oto v0.3.1 h1:cpf/uIv4Q0oc5uf9loQn7PIehv+mZerh+0KKma6gzMk=\ngithub.com/hajimehoshi/oto v0.3.1/go.mod h1:e9eTLBB9iZto045HLbzfHJIc+jP3xaKrjZTghvb6fdM=\ngithub.com/jfreymuth/oggvorbis v1.0.0/go.mod h1:abe6F9QRjuU9l+2jek3gj46lu40N4qlYxh2grqkLEDM=\ngithub.com/jfreymuth/vorbis v1.0.0/go.mod h1:8zy3lUAm9K/rJJk223RKy6vjCZTWC61NA2QD06bfOE0=\ngithub.com/lucasb-eyer/go-colorful v0.0.0-20181028223441-12d3b2882a08/go.mod h1:NXg0ArsFk0Y01623LgUqoqcouGDB+PwCCQlrwrG6xJ4=\ngithub.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=\ngithub.com/mattn/go-runewidth v0.0.4 h1:2BvfKmzob6Bmd4YsL0zygOqfdFnK7GR4QL06Do4/p7Y=\ngithub.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=\ngithub.com/mewkiz/flac v1.0.5/go.mod h1:EHZNU32dMF6alpurYyKHDLYpW1lYpBZ5WrXi/VuNIGs=\ngithub.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7 h1:DpOJ2HYzCv8LZP15IdmG+YdwD2luVPHITV96TkirNBM=\ngithub.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo=\ngithub.com/nsf/termbox-go v0.0.0-20190121233118-02980233997d h1:x3S6kxmy49zXVVyhcnrFqxvNVCBPb2KZ9hV2RBdS840=\ngithub.com/nsf/termbox-go v0.0.0-20190121233118-02980233997d/go.mod h1:IuKpRQcYE1Tfu+oAQqaLisqDeXgjyyltCfsaoYN18NQ=\ngithub.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=\ngithub.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=\ngithub.com/tidwall/gjson v1.6.0 h1:9VEQWz6LLMUsUl6PueE49ir4Ka6CzLymOAZDxpFsTDc=\ngithub.com/tidwall/gjson v1.6.0/go.mod h1:P256ACg0Mn+j1RXIDXoss50DeIABTYK1PULOJHhxOls=\ngithub.com/tidwall/match v1.0.1 h1:PnKP62LPNxHKTwvHHZZzdOAOCtsJTjo6dZLCwpKm5xc=\ngithub.com/tidwall/match v1.0.1/go.mod h1:LujAq0jyVjBy028G1WhWfIzbpQfMO8bBZ6Tyb0+pL9E=\ngithub.com/tidwall/pretty v1.0.0 h1:HsD+QiTn7sK6flMKIvNmpqz1qrpP3Ps6jOKIKMooyg4=\ngithub.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=\ngolang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=\ngolang.org/x/exp v0.0.0-20180710024300-14dda7b62fcd/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=\ngolang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs=\ngolang.org/x/mobile v0.0.0-20180806140643-507816974b79/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=\ngolang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20200202094626-16171245cfb2 h1:CCH4IOTTfewWjGOlSp+zGcjutRKlBEZQ6wTn8ozI/nI=\ngolang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/sys v0.0.0-20181228144115-9a3f9b0469bb/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a h1:1BGLXjeY4akVXGgbC9HugT3Jv3hCI0z56oJR5vAMgBU=\ngolang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=\ngopkg.in/DATA-DOG/go-sqlmock.v1 v1.3.0/go.mod h1:OdE7CF6DbADk7lN8LIKRzRJTTZXIjtWgA5THM5lhBAw=\n"
  },
  {
    "path": "itunes-api/episodes.go",
    "content": "package itunesapi\n\ntype Episode struct {\n\tId                     string `json:\"id\"`\n\tArtwork                string `json:\"artwork\"`\n\tTitle                  string `json:\"title\"`\n\tAudioURL               string `json:\"audiourl\"`\n\tReleaseDate            string `json:\"releasedate\"`\n\tDurationInMilliseconds int    `json:\"duratioInMilliseconds\"`\n\tDescription            string `json:\"description\"`\n}\n"
  },
  {
    "path": "itunes-api/genres.go",
    "content": "package itunesapi\n\nimport (\n\t\"net/http\"\n\t\"regexp\"\n\n\t\"github.com/PuerkitoBio/goquery\"\n)\n\nconst applePodcastsMainPage = \"https://podcasts.apple.com/genre/podcasts/id26\"\n\nvar (\n\tauthorization string\n)\n\ntype Genre struct {\n\tText     string   `json:\"text\"`\n\tURL      string   `json:\"url\"`\n\tId       string   `json:\"url\"`\n\tSubGenre []*Genre `json:\"sub-genres\"`\n}\n\nfunc getGenreId(url string) string {\n\tidRegExp := regexp.MustCompile(`\\d+$`)\n\treturn idRegExp.FindString(url)\n}\n\nfunc GetGenres() ([]*Genre, error) {\n\tresp, err := http.Get(applePodcastsMainPage)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdoc, err := goquery.NewDocumentFromResponse(resp)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tgenres := make([]*Genre, 0)\n\tdoc.Find(\".list.column > li\").Each(func(i int, s *goquery.Selection) {\n\t\tgenreSelection := s.Find(\".top-level-genre\")\n\t\thref, exists := genreSelection.Attr(\"href\")\n\t\tif !exists {\n\t\t\treturn\n\t\t}\n\n\t\tgenre := &Genre{Text: genreSelection.Text(), URL: href, SubGenre: make([]*Genre, 0), Id: getGenreId(href)}\n\n\t\ts.Find(\".top-level-subgenres > li > a\").Each(func(i int, s *goquery.Selection) {\n\t\t\thref, exists := genreSelection.Attr(\"href\")\n\t\t\tif !exists {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tgenre.SubGenre = append(genre.SubGenre, &Genre{Text: s.Text(), URL: href, Id: getGenreId(href)})\n\t\t})\n\t\tgenres = append(genres, genre)\n\n\t})\n\treturn genres, nil\n}\n"
  },
  {
    "path": "itunes-api/podcasts.go",
    "content": "package itunesapi\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"io/ioutil\"\n\t\"net/http\"\n\t\"regexp\"\n\n\t\"github.com/tidwall/gjson\"\n)\n\ntype Podcast struct {\n\tTitle       string `json:\"title\"`\n\tURL         string `json:\"url`\n\tId          string `json:\"id\"`\n\tDescription string `json:\"description\"`\n\tAuthor      string `json:\"author\"`\n}\n\nfunc FindPodcasts(query string) ([]*Podcast, error) {\n\tauthorization, err := getAuthorization()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\trequest, err := http.NewRequest(\"GET\", fmt.Sprintf(\"https://itunes.apple.com/search?country=us&entity=podcast&term=%s\", query), nil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\trequest.Header.Add(\"Authorization\", authorization)\n\tresp, err := http.DefaultClient.Do(request)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tdata, err := ioutil.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tpodcastsJSON := gjson.Get(string(data), \"results\").Array()\n\tpodcasts := make([]*Podcast, 0)\n\tfor _, podcast := range podcastsJSON {\n\t\tif podcast.Get(\"kind\").String() == \"podcast\" && podcast.Get(\"wrapperType\").String() == \"track\" {\n\t\t\tpodcasts = append(podcasts, &Podcast{\n\t\t\t\tAuthor:      podcast.Get(\"artistName\").String(),\n\t\t\t\tDescription: \"\",\n\t\t\t\tId:          podcast.Get(\"trackId\").String(),\n\t\t\t\tTitle:       podcast.Get(\"collectionName\").String(),\n\t\t\t\tURL:         regexp.MustCompile(`\\?.*$`).ReplaceAllString(podcast.Get(\"collectionViewUrl\").String(), \"\"),\n\t\t\t})\n\t\t}\n\t}\n\treturn podcasts, nil\n}\nfunc (p *Podcast) GetEpisodes() ([]*Episode, error) {\n\treq, err := http.NewRequest(\"GET\", fmt.Sprintf(\"https://amp-api.podcasts.apple.com/v1/catalog/us/podcasts/%s/episodes?offset=0&limit=300\", p.Id), nil)\n\tif err != nil {\n\t\treturn nil, err\n\n\t}\n\tauthorization, err := getAuthorization()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treq.Header.Add(\"Authorization\", authorization)\n\tresponse, err := http.DefaultClient.Do(req)\n\tif err != nil {\n\t\treturn nil, err\n\n\t}\n\tdata, err := ioutil.ReadAll(response.Body)\n\tif err != nil {\n\t\treturn nil, err\n\n\t}\n\tepisodesJSON := gjson.Get(string(data), \"data\").Array()\n\tepisodes := make([]*Episode, 0)\n\tfor _, episode := range episodesJSON {\n\t\tif episode.Get(`attributes.mediaKind`).String() == \"audio\" {\n\n\t\t\tepisodes = append(episodes, &Episode{\n\t\t\t\tId:                     episode.Get(`id`).String(),\n\t\t\t\tArtwork:                episode.Get(`attributes.artwork.url`).String(),\n\t\t\t\tTitle:                  episode.Get(`attributes.name`).String(),\n\t\t\t\tAudioURL:               episode.Get(`attributes.assetUrl`).String(),\n\t\t\t\tReleaseDate:            episode.Get(`attributes.releaseDateTime`).String(),\n\t\t\t\tDurationInMilliseconds: int(episode.Get(`attributes.durationInMilliseconds`).Int()),\n\t\t\t\tDescription:            episode.Get(`attributes.description.standard`).String(),\n\t\t\t})\n\t\t}\n\t}\n\treturn episodes, nil\n}\n\nfunc (g *Genre) GetPodcasts() ([]*Podcast, error) {\n\trequest, err := http.NewRequest(\"GET\", \"https://amp-api.podcasts.apple.com/v1/catalog/us/charts?types=podcasts&limit=200&genre=\"+g.Id, nil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tauthorization, err := getAuthorization()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\trequest.Header.Add(\"Authorization\", authorization)\n\tresp, err := http.DefaultClient.Do(request)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tdata, err := ioutil.ReadAll(resp.Body)\n\tpodcastsJSON := gjson.Get(string(data), \"results.podcasts.0.data\").Array()\n\tpodcasts := make([]*Podcast, len(podcastsJSON))\n\tfor i, podcast := range podcastsJSON {\n\t\tpodcasts[i] = &Podcast{\n\t\t\tId:          podcast.Get(\"id\").String(),\n\t\t\tDescription: podcast.Get(\"attributes.description.standard\").String(),\n\t\t\tTitle:       podcast.Get(\"attributes.name\").String(),\n\t\t\tURL:         \"https://amp-api.podcasts.apple.com\" + podcast.Get(\"href\").String(),\n\t\t\tAuthor:      podcast.Get(\"attributes.artistName\").String(),\n\t\t}\n\t}\n\treturn podcasts, nil\n}\n\nfunc getAuthorization() (string, error) {\n\tif authorization != \"\" {\n\t\treturn authorization, nil\n\t}\n\tauthRegEx := regexp.MustCompile(\"privateKeyPath.+token%22%3A%22(?P<Bearer>.*?)%22%7D%2C%22\")\n\tresp, err := http.Get(\"https://podcasts.apple.com/us/podcast/the-joe-rogan-experience/id360084272\")\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tdata, err := ioutil.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tmatch := authRegEx.FindStringSubmatch(string(data))\n\tif len(match) != 2 {\n\t\treturn \"\", errors.New(\"Authorization access token is not found\")\n\t}\n\tauthorization = \"Bearer \" + match[1]\n\treturn authorization, nil\n}\n"
  },
  {
    "path": "main.go",
    "content": "package main\n\nimport (\n\t\"log\"\n\t\"os\"\n\n\t\"github.com/akamensky/argparse\"\n\tui \"github.com/gizak/termui/v3\"\n\n\titunesapi \"github.com/goulinkh/podcast-cli/itunes-api\"\n\t\"github.com/goulinkh/podcast-cli/rss\"\n\tpodcastcliui \"github.com/goulinkh/podcast-cli/ui\"\n)\n\nfunc main() {\n\n\tparser := argparse.NewParser(\"podcast-cli\", \"CLI podcast player\")\n\tpodcastSearchQuery := parser.String(\"s\", \"search\", &argparse.Options{Required: false, Help: \"your podcast's name\"})\n\trssUrl := parser.String(\"r\", \"rss\", &argparse.Options{Required: false, Help: \"custom podcast rss source\"})\n\toffset := parser.Int(\"o\", \"offset\", &argparse.Options{Required: false, Help: \"play episode number\"})\n\terr := parser.Parse(os.Args)\n\tif err != nil {\n\t\tlog.Fatalln(\"Error:\", parser.Usage(err))\n\t\treturn\n\t}\n\terr = podcastcliui.InitUI()\n\tif err != nil {\n\t\tlog.Fatal(err)\n\t}\n\n\tif podcastSearchQuery != nil && *podcastSearchQuery != \"\" {\n\t\tpodcasts, err := itunesapi.FindPodcasts(*podcastSearchQuery)\n\t\tif err != nil {\n\t\t\tlog.Fatalln(\"Error: Failed to search for podcasts\")\n\t\t}\n\t\tpodcastsWidget := &podcastcliui.PodcastsUI{Podcasts: podcasts}\n\t\tpodcastsWidget.InitComponents()\n\t\tpodcastcliui.Show(podcastsWidget)\n\t} else if rssUrl != nil && *rssUrl != \"\" {\n\t\tepisodes, err := rss.ParseEpisodes(*rssUrl)\n\t\tif err != nil {\n\t\t\tlog.Fatalln(\"Error: Failed to get episodes from the url: \" + *rssUrl)\n\t\t}\n\t\tepisodesWidget := &podcastcliui.EpisodesUI{Episodes: episodes}\n\t\tepisodesWidget.InitComponents()\n\t\tpodcastcliui.Show(episodesWidget)\n\t\tif offset != nil {\n\t\t\tepisodesWidget.Play(*offset)\n\t\t}\n\t} else {\n\t\tgenres, err := itunesapi.GetGenres()\n\t\tif err != nil {\n\t\t\tlog.Fatal(err)\n\t\t}\n\t\tgenreWidget := &podcastcliui.GenresUI{\n\t\t\tGenres: genres,\n\t\t}\n\t\tgenreWidget.InitComponents()\n\t\tpodcastcliui.Show(genreWidget)\n\n\t}\n\n\tuiEvents := ui.PollEvents()\n\tfor {\n\t\tselect {\n\t\tcase e := <-uiEvents:\n\t\t\tcmd, err := podcastcliui.HandleKeyEvent(&e)\n\t\t\tif err != nil {\n\t\t\t\tlog.Fatal(err)\n\t\t\t}\n\t\t\tif cmd == podcastcliui.Exit {\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "rss/parser.go",
    "content": "package rss\n\nimport (\n\t\"encoding/xml\"\n\t\"io/ioutil\"\n\t\"net/http\"\n\t\"strconv\"\n\n\titunesapi \"github.com/goulinkh/podcast-cli/itunes-api\"\n)\n\ntype Rss struct {\n\tChannel struct {\n\t\tItem []struct {\n\t\t\tTitle       string `xml:\"title\"`\n\t\t\tPubDate     string `xml:\"pubDate\"`\n\t\t\tAuthor      string `xml:\"author\"`\n\t\t\tDescription string `xml:\"description\"`\n\t\t\tImage       struct {\n\t\t\t\tHref string `xml:\"href,attr\"`\n\t\t\t} `xml:\"image\"`\n\t\t\tEnclosure struct {\n\t\t\t\tURL    string `xml:\"url,attr\"`\n\t\t\t\tLength int    `xml:\"length,attr\"`\n\t\t\t\tType   string `xml:\"type,attr\"`\n\t\t\t} `xml:\"enclosure\"`\n\t\t\tDuration int `xml:\"duration\"`\n\t\t} `xml:\"item\"`\n\t} `xml:\"channel\"`\n}\n\nfunc ParseEpisodes(rssUrl string) ([]*itunesapi.Episode, error) {\n\tresp, err := http.Get(rssUrl)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\trss, err := ioutil.ReadAll(resp.Body)\n\tvar podcast Rss\n\terr = xml.Unmarshal(rss, &podcast)\n\tepisodes := make([]*itunesapi.Episode, len(podcast.Channel.Item))\n\tfor i, e := range podcast.Channel.Item {\n\t\tepisodes[i] =\n\t\t\t&itunesapi.Episode{\n\t\t\t\tArtwork:                e.Image.Href,\n\t\t\t\tAudioURL:               e.Enclosure.URL,\n\t\t\t\tDescription:            e.Description,\n\t\t\t\tDurationInMilliseconds: e.Duration * 1000,\n\t\t\t\tId:                     strconv.Itoa(i),\n\t\t\t\tReleaseDate:            e.PubDate,\n\t\t\t\tTitle:                  e.Title,\n\t\t\t}\n\t}\n\treturn episodes, nil\n}\n"
  },
  {
    "path": "ui/Podcasts.go",
    "content": "package ui\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"strings\"\n\n\tui \"github.com/gizak/termui/v3\"\n\t\"github.com/gizak/termui/v3/widgets\"\n\titunesapi \"github.com/goulinkh/podcast-cli/itunes-api\"\n)\n\ntype PodcastsUI struct {\n\tPodcasts      []*itunesapi.Podcast\n\tlistWidget    *widgets.List\n\tdetailsWidget *widgets.Paragraph\n\tgridWidget    *ui.Grid\n}\n\nfunc (p *PodcastsUI) InitComponents() error {\n\tp.initListWidget()\n\tp.initDetailsWidget()\n\terr := p.initGridWidget()\n\treturn err\n}\nfunc (p *PodcastsUI) MainUI() *ui.Grid {\n\treturn p.gridWidget\n}\nfunc (p *PodcastsUI) HandleEvent(event *ui.Event) (Command, error) {\n\tswitch event.ID {\n\tcase \"j\", \"<Down>\":\n\t\tp.listWidget.ScrollDown()\n\t\tp.updateDetailsWidget()\n\tcase \"k\", \"<Up>\":\n\t\tp.listWidget.ScrollUp()\n\t\tp.updateDetailsWidget()\n\n\tcase \"<Enter>\":\n\t\tepisodes, err := p.Podcasts[p.listWidget.SelectedRow].GetEpisodes()\n\t\tif err != nil {\n\t\t\treturn Nothing, err\n\t\t}\n\t\tepisodesUI := &EpisodesUI{Episodes: episodes}\n\t\terr = episodesUI.InitComponents()\n\t\tif err != nil {\n\t\t\treturn Nothing, err\n\t\t}\n\t\tShow(episodesUI)\n\t}\n\treturn Nothing, nil\n}\nfunc (p *PodcastsUI) initGridWidget() error {\n\tif p.listWidget == nil {\n\t\treturn errors.New(\"Uninitialized podcasts list widget\")\n\t}\n\tif p.detailsWidget == nil {\n\t\treturn errors.New(\"Uninitialized details widget\")\n\t}\n\tp.gridWidget = ui.NewGrid()\n\ttermWidth, termHeight := ui.TerminalDimensions()\n\tp.gridWidget.SetRect(0, 0, termWidth, termHeight-1)\n\tp.gridWidget.Set(\n\t\tui.NewRow(1.0,\n\t\t\tui.NewCol(1.0/2, p.listWidget),\n\t\t\tui.NewCol(1.0/2,\n\t\t\t\tui.NewRow(6.0/10, p.detailsWidget),\n\t\t\t\tui.NewRow(4.0/10, audioPlayerWidget.MainUI()))))\n\treturn nil\n}\nfunc (p *PodcastsUI) initListWidget() {\n\tp.listWidget = widgets.NewList()\n\tp.listWidget.Title = \"Podcasts List\"\n\tp.listWidget.TextStyle.Fg = FgColor\n\tp.listWidget.SelectedRowStyle.Fg = ui.ColorBlack\n\tp.listWidget.SelectedRowStyle.Bg = AccentColor\n\tp.listWidget.BorderStyle.Fg = AccentColor\n\tp.listWidget.Rows = make([]string, len(p.Podcasts))\n\tfor i, podcast := range p.Podcasts {\n\t\tp.listWidget.Rows[i] = podcast.Title\n\t}\n}\nfunc (p *PodcastsUI) initDetailsWidget() {\n\tp.detailsWidget = widgets.NewParagraph()\n\tp.detailsWidget.Title = \"Details\"\n\tp.detailsWidget.BorderStyle.Fg = AccentColor\n\tp.detailsWidget.BorderLeft = false\n\tp.detailsWidget.BorderBottom = false\n\tp.updateDetailsWidget()\n}\nfunc (p *PodcastsUI) updateDetailsWidget() {\n\tif p.Podcasts == nil || len(p.Podcasts) == 0 {\n\t\treturn\n\t}\n\tcurrentPodcast := p.Podcasts[p.listWidget.SelectedRow]\n\ttitle := fmt.Sprintf(\"[Title](fg:magenta)        %s\", currentPodcast.Title)\n\tdescription := fmt.Sprintf(\"[Description](fg:magenta)  %s\", currentPodcast.Description)\n\tauthor := fmt.Sprintf(\"[Author](fg:magenta)       %s\", currentPodcast.Author)\n\tp.detailsWidget.Text = strings.Join([]string{title, description, author}, \"\\n\")\n}\n"
  },
  {
    "path": "ui/audio_player.go",
    "content": "package ui\n\nimport (\n\t\"fmt\"\n\t\"time\"\n\n\tui \"github.com/gizak/termui/v3\"\n\t\"github.com/gizak/termui/v3/widgets\"\n\taudioplayer \"github.com/goulinkh/podcast-cli/audio-player\"\n\titunesapi \"github.com/goulinkh/podcast-cli/itunes-api\"\n)\n\ntype AudioPlayerWidget struct {\n\tplaylist            []*itunesapi.Episode\n\tnowPlaying          *itunesapi.Episode\n\tnowPlayingIndex     int\n\tpaused              bool\n\taudioPositionWidget *widgets.Gauge\n\tplayerStatusWidget  *widgets.Paragraph\n\tgrid                *ui.Grid\n\tplaySpeed           float32\n}\n\nfunc (ap *AudioPlayerWidget) InitComponents() {\n\tap.paused = true\n\tap.initAudipPositionWidget()\n\tap.initPlayerStatusWidget()\n\tap.initGrid()\n}\n\nfunc (ap *AudioPlayerWidget) MainUI() *ui.Grid {\n\treturn ap.grid\n}\n\nfunc (ap *AudioPlayerWidget) HandleEvent(e *ui.Event) (Command, error) {\n\tswitch e.ID {\n\tcase \"p\", \"<Space>\":\n\t\tap.Pause()\n\tcase \"<Right>\":\n\t\tif audioplayer.MainCtrl != nil && ap.nowPlaying != nil {\n\t\t\tposition := audioplayer.Position() + 10\n\n\t\t\tif position < ap.nowPlaying.DurationInMilliseconds/1000 {\n\t\t\t\taudioplayer.Seek(position)\n\t\t\t}\n\t\t}\n\tcase \"<Left>\":\n\t\tif audioplayer.MainCtrl != nil && ap.nowPlaying != nil {\n\t\t\tposition := audioplayer.Position() - 10\n\n\t\t\tif position > 0 {\n\t\t\t\taudioplayer.Seek(position)\n\t\t\t}\n\t\t}\n\tcase \"u\":\n\t\tif audioplayer.MainCtrl != nil && ap.nowPlaying != nil {\n\t\t\taudioplayer.IncreaseSpeed()\n\t\t}\n\tcase \"d\":\n\t\tif audioplayer.MainCtrl != nil && ap.nowPlaying != nil {\n\t\t\taudioplayer.DecreaseSpeed()\n\t\t}\n\n\t}\n\treturn Nothing, nil\n}\n\nfunc (ap *AudioPlayerWidget) Play(playlist []*itunesapi.Episode, index int) {\n\te := playlist[index]\n\tif e == nil || (ap.nowPlaying != nil && ap.nowPlaying.Id == e.Id) {\n\t\treturn\n\t}\n\tap.playerStatusWidget.Title = \"Downloading audio ...\"\n\tRefreshUI()\n\tgo func() {\n\t\tap.nowPlaying = e\n\t\tap.playlist = playlist\n\t\tap.nowPlayingIndex = index\n\t\tap.paused = false\n\t\tap.playerStatusWidget.Title = \"Now Playing\"\n\t\tap.playAudio(ap.nowPlaying)\n\t\tfor {\n\t\t\tselect {\n\t\t\tcase <-time.After(time.Millisecond * 100):\n\t\t\t\tif ap.paused {\n\t\t\t\t\tap.audioPositionWidget.Title = \"Paused\"\n\t\t\t\t} else {\n\t\t\t\t\tap.playerStatusWidget.Text = ap.nowPlaying.Title\n\t\t\t\t\tposition := audioplayer.Position()\n\t\t\t\t\tap.audioPositionWidget.Title = \"Running\"\n\t\t\t\t\tap.audioPositionWidget.Label = fmt.Sprintf(\"%d:%d\", position/60, position%60)\n\t\t\t\t\taudioDuration := e.DurationInMilliseconds / 1000\n\t\t\t\t\tif audioDuration > 0 {\n\t\t\t\t\t\tap.audioPositionWidget.Percent = (position * 100) / audioDuration\n\t\t\t\t\t}\n\t\t\t\t\tif ap.audioPositionWidget.Percent == 100 {\n\t\t\t\t\t\tap.Play(playlist, index+1)\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tRefreshUI()\n\t\t\t}\n\t\t}\n\t}()\n\treturn\n}\n\nfunc (ap *AudioPlayerWidget) playAudio(e *itunesapi.Episode) {\n\tif err := audioplayer.PlaySound(e); err != nil {\n\t\tap.playerStatusWidget.Title = \"Failed to play audio\"\n\t\tRefreshUI()\n\t}\n}\n\nfunc (ap *AudioPlayerWidget) Pause() {\n\tap.paused = !ap.paused\n\taudioplayer.PauseSong(ap.paused)\n}\n\nfunc (ap *AudioPlayerWidget) initAudipPositionWidget() {\n\tap.audioPositionWidget = widgets.NewGauge()\n\tap.audioPositionWidget.BorderLeft = false\n\tap.audioPositionWidget.BarColor = AccentColor\n\tap.audioPositionWidget.BorderStyle.Fg = AccentColor\n}\nfunc (ap *AudioPlayerWidget) initPlayerStatusWidget() {\n\tap.playerStatusWidget = widgets.NewParagraph()\n\tap.playerStatusWidget.BorderLeft = false\n\tap.playerStatusWidget.BorderBottom = false\n\tap.playerStatusWidget.TextStyle.Fg = AccentColor\n\tap.playerStatusWidget.Title = \"Now Playing\"\n\tap.playerStatusWidget.TitleStyle.Fg = FgColor\n\tap.playerStatusWidget.BorderStyle.Fg = AccentColor\n}\nfunc (ap *AudioPlayerWidget) initGrid() {\n\tap.grid = ui.NewGrid()\n\tap.grid.Border = false\n\tap.grid.Set(\n\t\tui.NewRow(1.0,\n\t\t\tui.NewRow(1.0/2, ap.playerStatusWidget),\n\t\t\tui.NewRow(1.0/2, ap.audioPositionWidget),\n\t\t),\n\t)\n}\n"
  },
  {
    "path": "ui/commands.go",
    "content": "package ui\n\ntype Command int\n\nconst (\n\tNothing Command = 0\n\tExit    Command = 1\n)\n"
  },
  {
    "path": "ui/episodes.go",
    "content": "package ui\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"strings\"\n\n\tui \"github.com/gizak/termui/v3\"\n\t\"github.com/gizak/termui/v3/widgets\"\n\titunesapi \"github.com/goulinkh/podcast-cli/itunes-api\"\n)\n\ntype EpisodesUI struct {\n\tEpisodes      []*itunesapi.Episode\n\tlistWidget    *widgets.List\n\tdetailsWidget *widgets.Paragraph\n\tgridWidget    *ui.Grid\n}\n\nfunc (e *EpisodesUI) InitComponents() error {\n\te.initListWidget()\n\te.initDetailsWidget()\n\terr := e.initGridWidget()\n\treturn err\n}\n\nfunc (e *EpisodesUI) MainUI() *ui.Grid {\n\treturn e.gridWidget\n}\n\nfunc (e *EpisodesUI) HandleEvent(event *ui.Event) (Command, error) {\n\tswitch event.ID {\n\tcase \"j\", \"<Down>\":\n\t\te.listWidget.ScrollDown()\n\t\te.updateDetailsWidget()\n\tcase \"k\", \"<Up>\":\n\t\te.listWidget.ScrollUp()\n\t\te.updateDetailsWidget()\n\n\tcase \"<Enter>\":\n\t\taudioPlayerWidget.Play(e.Episodes, e.listWidget.SelectedRow)\n\t}\n\treturn Nothing, nil\n}\nfunc (e *EpisodesUI) Play(index int) {\n\taudioPlayerWidget.Play(e.Episodes, index)\n\n}\nfunc (e *EpisodesUI) initGridWidget() error {\n\tif e.listWidget == nil {\n\t\treturn errors.New(\"List widget is not initialized\")\n\t}\n\tif e.detailsWidget == nil {\n\t\treturn errors.New(\"Details widget is not initialized\")\n\t}\n\te.gridWidget = ui.NewGrid()\n\ttermWidth, termHeight := ui.TerminalDimensions()\n\te.gridWidget.SetRect(0, 0, termWidth, termHeight-1)\n\te.gridWidget.Set(\n\t\tui.NewRow(1.0,\n\t\t\tui.NewCol(1.0/2, e.listWidget),\n\t\t\tui.NewCol(1.0/2,\n\t\t\t\tui.NewRow(6.0/10, e.detailsWidget),\n\t\t\t\tui.NewRow(4.0/10, audioPlayerWidget.MainUI()))))\n\treturn nil\n}\n\nfunc (e *EpisodesUI) initListWidget() {\n\te.listWidget = widgets.NewList()\n\te.listWidget.Title = \"Episodes\"\n\te.listWidget.SelectedRowStyle.Modifier = ui.ModifierClear\n\te.listWidget.TextStyle.Fg = FgColor\n\te.listWidget.SelectedRowStyle.Fg = ui.ColorBlack\n\te.listWidget.SelectedRowStyle.Bg = AccentColor\n\te.listWidget.BorderStyle.Fg = AccentColor\n\te.listWidget.Rows = make([]string, len(e.Episodes))\n\tfor i, episode := range e.Episodes {\n\t\te.listWidget.Rows[i] = episode.Title\n\t}\n}\nfunc (e *EpisodesUI) initDetailsWidget() {\n\te.detailsWidget = widgets.NewParagraph()\n\te.detailsWidget.Title = \"Details\"\n\te.detailsWidget.BorderStyle.Fg = AccentColor\n\te.detailsWidget.BorderLeft = false\n\te.detailsWidget.BorderBottom = false\n\te.updateDetailsWidget()\n}\nfunc (e *EpisodesUI) updateDetailsWidget() {\n\tif e.Episodes == nil || len(e.Episodes) == 0 {\n\t\treturn\n\t}\n\tcurrentEpisode := e.Episodes[e.listWidget.SelectedRow]\n\ttitle := fmt.Sprintf(\"[Title](fg:magenta)        %s\", currentEpisode.Title)\n\tdescription := fmt.Sprintf(\"[Description](fg:magenta)  %s\", currentEpisode.Description)\n\tdate := fmt.Sprintf(\"[Release Date](fg:magenta) %s\", currentEpisode.ReleaseDate)\n\tduration := fmt.Sprintf(\"[Duration](fg:magenta)     %d min\", currentEpisode.DurationInMilliseconds/60000)\n\te.detailsWidget.Text = strings.Join([]string{title, duration, date, description}, \"\\n\")\n}\n"
  },
  {
    "path": "ui/genres.go",
    "content": "package ui\n\nimport (\n\t\"errors\"\n\n\tui \"github.com/gizak/termui/v3\"\n\t\"github.com/gizak/termui/v3/widgets\"\n\titunesapi \"github.com/goulinkh/podcast-cli/itunes-api\"\n)\n\ntype GenresUI struct {\n\tGenres     []*itunesapi.Genre\n\tgridWidget *ui.Grid\n\tlistWidget *widgets.List\n}\n\nfunc (g *GenresUI) InitComponents() error {\n\tg.newGenresListWidget()\n\terr := g.newGridWidget()\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\nfunc (g *GenresUI) MainUI() *ui.Grid {\n\treturn g.gridWidget\n}\nfunc (g *GenresUI) HandleEvent(event *ui.Event) (Command, error) {\n\tswitch event.ID {\n\tcase \"j\", \"<Down>\":\n\t\tg.listWidget.ScrollDown()\n\n\tcase \"k\", \"<Up>\":\n\t\tg.listWidget.ScrollUp()\n\tcase \"<Enter>\":\n\t\tsubGenres := g.Genres[g.listWidget.SelectedRow].SubGenre\n\t\tvar subGenreUI *SubGenresUI\n\t\tif subGenres == nil || len(subGenres) == 0 {\n\t\t\tsubGenreUI = &SubGenresUI{Genres: []*itunesapi.Genre{g.Genres[g.listWidget.SelectedRow]}}\n\t\t} else {\n\t\t\tsubGenreUI = &SubGenresUI{Genres: g.Genres[g.listWidget.SelectedRow].SubGenre}\n\t\t}\n\t\tsubGenreUI.InitComponents()\n\t\tShow(subGenreUI)\n\t}\n\treturn Nothing, nil\n}\nfunc (g *GenresUI) newGenresListWidget() error {\n\tg.listWidget = widgets.NewList()\n\tg.listWidget.Title = \"Select a Genre\"\n\tg.listWidget.TextStyle = ui.NewStyle(FgColor)\n\tg.listWidget.SelectedRowStyle.Fg = ui.ColorBlack\n\tg.listWidget.SelectedRowStyle.Bg = AccentColor\n\tg.listWidget.BorderStyle.Fg = AccentColor\n\tif g.Genres == nil {\n\t\treturn errors.New(\"Missing Genres array\")\n\t}\n\tg.listWidget.Rows = make([]string, len(g.Genres))\n\tfor i, genre := range g.Genres {\n\t\tg.listWidget.Rows[i] = genre.Text\n\t}\n\treturn nil\n}\nfunc (g *GenresUI) newGridWidget() error {\n\tif g.listWidget == nil {\n\t\treturn errors.New(\"Uninitialized genres list widget\")\n\t}\n\tg.gridWidget = ui.NewGrid()\n\ttermWidth, termHeight := ui.TerminalDimensions()\n\tg.gridWidget.SetRect(0, 0, termWidth, termHeight-1)\n\tplaceholder := ui.NewBlock()\n\tplaceholder.BorderBottom = false\n\tplaceholder.BorderLeft = false\n\tplaceholder.BorderStyle.Fg = AccentColor\n\tg.gridWidget.Set(\n\t\tui.NewRow(1.0,\n\t\t\tui.NewCol(1.0/2, g.listWidget),\n\t\t\tui.NewCol(1.0/2,\n\t\t\t\tui.NewRow(6.0/10, placeholder),\n\t\t\t\tui.NewRow(4.0/10, audioPlayerWidget.MainUI()))))\n\treturn nil\n}\n"
  },
  {
    "path": "ui/page.go",
    "content": "package ui\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\n\tui \"github.com/gizak/termui/v3\"\n\t\"github.com/gizak/termui/v3/widgets\"\n)\n\nvar (\n\tFgColor           = ui.ColorWhite\n\tAccentColor       = ui.ColorMagenta\n\tpagesHistory      = make([]Page, 0)\n\tcurrentPage       Page\n\thelpBarWidget     *widgets.Paragraph\n\taudioPlayerWidget = &AudioPlayerWidget{}\n)\n\ntype Page interface {\n\tMainUI() *ui.Grid\n\tHandleEvent(*ui.Event) (Command, error)\n}\n\nfunc InitUI() error {\n\tif err := ui.Init(); err != nil {\n\t\terrors.New(fmt.Sprintf(\"failed to initialize the UI: %v\", err))\n\t}\n\thelpBarWidget = newHelpBarWidget()\n\taudioPlayerWidget.InitComponents()\n\treturn nil\n}\nfunc Show(p Page) {\n\tif currentPage != nil {\n\t\tpagesHistory = append(pagesHistory, currentPage)\n\t}\n\n\tshow(p)\n}\n\nfunc show(p Page) {\n\tcurrentPage = p\n\tRefreshUI()\n}\n\nfunc RefreshUI() {\n\tui.Clear()\n\tui.Render(currentPage.MainUI(), helpBarWidget)\n}\n\nfunc GoBack() {\n\tif len(pagesHistory) == 0 {\n\t\treturn\n\t}\n\tpreviousPage := pagesHistory[len(pagesHistory)-1]\n\tpagesHistory = pagesHistory[:len(pagesHistory)-1]\n\tshow(previousPage)\n}\nfunc HandleKeyEvent(e *ui.Event) (Command, error) {\n\tswitch e.ID {\n\tcase \"q\", \"<C-c>\":\n\t\tui.Close()\n\t\treturn Exit, nil\n\n\tcase \"<Escape>\", \"<C-<Backspace>>\", \"<Backspace>\":\n\t\tGoBack()\n\t\tRefreshUI()\n\tcase \"<Resize>\":\n\t\tpayload := e.Payload.(ui.Resize)\n\t\thelpBarWidget.SetRect(0, payload.Height-1, payload.Width, payload.Height)\n\t\tcurrentPage.MainUI().SetRect(0, 0, payload.Width, payload.Height-1)\n\t\tRefreshUI()\n\tdefault:\n\t\tcmd, err := currentPage.HandleEvent(e)\n\t\tif err != nil {\n\t\t\treturn cmd, err\n\t\t}\n\t\tcmd, err = audioPlayerWidget.HandleEvent(e)\n\t\tRefreshUI()\n\t\treturn cmd, err\n\t}\n\treturn Nothing, nil\n\n}\n\nfunc newHelpBarWidget() *widgets.Paragraph {\n\thelpBarWidget := widgets.NewParagraph()\n\thelpBarWidget.Text = \"[ Enter ](fg:black)[Select](fg:black,bg:green) \" +\n\t\t\"[ p, Space ](fg:black)[Play/Pause](fg:black,bg:green) \" +\n\t\t\"[Esc ](fg:black)[Back](fg:black,bg:green) \" +\n\t\t\"[Right ](fg:black)[+10s](fg:black,bg:green) \" +\n\t\t\"[Left ](fg:black)[-10s](fg:black,bg:green) \" +\n\t\t\"[ d ](fg:black)[Slowdown](fg:black,bg:green)\" +\n\t\t\"[ u ](fg:black)[Speedup](fg:black,bg:green)\" +\n\t\t\"[ q ](fg:black)[Exit](fg:black,bg:green)\"\n\thelpBarWidget.Border = false\n\thelpBarWidget.WrapText = true\n\thelpBarWidget.TextStyle = ui.Style{Modifier: ui.ModifierBold, Bg: ui.ColorWhite}\n\ttermWidth, termHeight := ui.TerminalDimensions()\n\thelpBarWidget.SetRect(0, termHeight-1, termWidth, termHeight)\n\treturn helpBarWidget\n}\n"
  },
  {
    "path": "ui/sub_genres.go",
    "content": "package ui\n\nimport (\n\t\"errors\"\n\n\tui \"github.com/gizak/termui/v3\"\n\t\"github.com/gizak/termui/v3/widgets\"\n\titunesapi \"github.com/goulinkh/podcast-cli/itunes-api\"\n)\n\ntype SubGenresUI struct {\n\tGenres     []*itunesapi.Genre\n\tgridWidget *ui.Grid\n\tlistWidget *widgets.List\n}\n\nfunc (g *SubGenresUI) InitComponents() error {\n\tg.newGenresListWidget()\n\terr := g.newGridWidget()\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\nfunc (g *SubGenresUI) MainUI() *ui.Grid {\n\treturn g.gridWidget\n}\nfunc (g *SubGenresUI) HandleEvent(event *ui.Event) (Command, error) {\n\tswitch event.ID {\n\tcase \"j\", \"<Down>\":\n\t\tg.listWidget.ScrollDown()\n\n\tcase \"k\", \"<Up>\":\n\t\tg.listWidget.ScrollUp()\n\tcase \"<Enter>\":\n\t\tpodcasts, err := g.Genres[g.listWidget.SelectedRow].GetPodcasts()\n\t\tif err != nil {\n\t\t\treturn Nothing, err\n\t\t}\n\t\tpodcastsUI := &PodcastsUI{Podcasts: podcasts}\n\t\terr = podcastsUI.InitComponents()\n\t\tif err != nil {\n\t\t\treturn Nothing, err\n\t\t}\n\t\tShow(podcastsUI)\n\t}\n\treturn Nothing, nil\n}\nfunc (g *SubGenresUI) newGenresListWidget() error {\n\tg.listWidget = widgets.NewList()\n\tg.listWidget.Title = \"Select a Sub Genre\"\n\tg.listWidget.TextStyle = ui.NewStyle(FgColor)\n\tg.listWidget.SelectedRowStyle.Fg = ui.ColorBlack\n\tg.listWidget.SelectedRowStyle.Bg = AccentColor\n\tg.listWidget.BorderStyle.Fg = AccentColor\n\tif g.Genres == nil {\n\t\treturn errors.New(\"Missing Sub Genres array\")\n\t}\n\tg.listWidget.Rows = make([]string, len(g.Genres))\n\tfor i, genre := range g.Genres {\n\t\tg.listWidget.Rows[i] = genre.Text\n\t}\n\treturn nil\n}\nfunc (g *SubGenresUI) newGridWidget() error {\n\tif g.listWidget == nil {\n\t\treturn errors.New(\"Uninitialized sub genres list widget\")\n\t}\n\tg.gridWidget = ui.NewGrid()\n\ttermWidth, termHeight := ui.TerminalDimensions()\n\tg.gridWidget.SetRect(0, 0, termWidth, termHeight-1)\n\tplaceholder := ui.NewBlock()\n\tplaceholder.BorderBottom = false\n\tplaceholder.BorderLeft = false\n\tplaceholder.BorderStyle.Fg = AccentColor\n\tg.gridWidget.Set(\n\t\tui.NewRow(1.0,\n\t\t\tui.NewCol(1.0/2, g.listWidget),\n\t\t\tui.NewCol(1.0/2,\n\t\t\t\tui.NewRow(6.0/10, placeholder),\n\t\t\t\tui.NewRow(4.0/10, audioPlayerWidget.MainUI()))))\n\treturn nil\n}\nfunc (g *SubGenresUI) refreshComponents() {\n\tg.newGenresListWidget()\n}\n"
  }
]