Full Code of goulinkh/podcast-cli for AI

master 26832a448dff cached
19 files
36.1 KB
12.1k tokens
74 symbols
1 requests
Download .txt
Repository: goulinkh/podcast-cli
Branch: master
Commit: 26832a448dff
Files: 19
Total size: 36.1 KB

Directory structure:
gitextract_b8au3yeh/

├── .gitignore
├── .vscode/
│   └── launch.json
├── README.md
├── audio-player/
│   └── player.go
├── config/
│   └── main.go
├── go.mod
├── go.sum
├── itunes-api/
│   ├── episodes.go
│   ├── genres.go
│   └── podcasts.go
├── main.go
├── rss/
│   └── parser.go
└── ui/
    ├── Podcasts.go
    ├── audio_player.go
    ├── commands.go
    ├── episodes.go
    ├── genres.go
    ├── page.go
    └── sub_genres.go

================================================
FILE CONTENTS
================================================

================================================
FILE: .gitignore
================================================
# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib

# binary
podcast-cli*
main*
!main.go

# Test binary, built with `go test -c`
*.test

# Output of the go coverage tool, specifically when used with LiteIDE
*.out

# Dependency directories (remove the comment below to include it)
# vendor/

# IDE
.idea/

# Project
.cache/

# Vim
*.swp
*.swo
*.swn
*.un~


================================================
FILE: .vscode/launch.json
================================================
{
    // Use IntelliSense to learn about possible attributes.
    // Hover to view descriptions of existing attributes.
    // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
    "version": "0.2.0",
    "configurations": [
        {
            "name": "Launch file",
            "type": "go",
            "request": "launch",
            "mode": "auto",
            "program": "${workspaceFolder}"
        }
    ]
}

================================================
FILE: README.md
================================================
<p align="center"><img width="200px" src="/resources/img/logo.png" alt="podcast-cli"/></p>


___

Top-like interface for listening to podcasts
`podcast-cli` lets you play your favourite podcasts from the terminal:
<p align="center"><img src="/resources/img/demo.gif" alt="podcast-cli"/></p>

`podcast-cli` is entirely built with Go, you can run it on `Linux`, `Mac OS` and `Windows`.

## Install
Fetch the [latest release](https://github.com/goulinkh/podcast-cli/releases)

#### Linux

```bash
sudo 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
sudo chmod +x /usr/local/bin/podcast-cli
```

#### OS X

```bash
sudo 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
sudo chmod +x /usr/local/bin/podcast-cli
```

## Usage
`podcast-cli` requires no arguments and uses your default internet settings to access the internet.

### Options

| Options                  | Description                                 |
| ------------------------ | ------------------------------------------- |
| `-h or  --help`          | Print help information                      |
| `-s or --search <query>` | List podcasts that matches the search query |
| `-r or --rss <url>`    | Custom podcast rss url source               |
| `-o or --offset <episode number starting with 0>` | Play episode number                         |

### Keybindings

| Key        | Action   |
| ---------- | -------- |
| `Enter`    | Select   |
| `p, Space` | Pause    |
| `Esc`      | Back     |
| `Right`    | +10s     |
| `Left`     | -10s     |
| `u`        | Slowdown |
| `d`        | Speedup  |
| `q`        | Exit     |


## Issues

* Unable to get audio length of a remote content, I have to download the audio file before playing it



================================================
FILE: audio-player/player.go
================================================
package audioplayer

import (
	"errors"
	"fmt"
	"io/ioutil"
	"net/http"
	"net/url"
	"os"
	"path"
	"strings"
	"time"

	"github.com/faiface/beep"
	"github.com/faiface/beep/effects"
	"github.com/faiface/beep/mp3"
	"github.com/faiface/beep/speaker"
	"github.com/faiface/beep/wav"
	"github.com/goulinkh/podcast-cli/config"
	itunesapi "github.com/goulinkh/podcast-cli/itunes-api"
)

type AudioPlayer struct {
	Streamer beep.StreamSeekCloser
	Format   beep.Format
}

var (
	MainCtrl  *beep.Ctrl
	Volume    *effects.Volume
	resampler *beep.Resampler

	Streamer beep.StreamSeekCloser
	Format   beep.Format
)

func init() {
	Volume = &effects.Volume{Base: 2}
}

func fetchContent(URL string, filepath string, directory string) error {

	_, err := ioutil.ReadFile(filepath)
	if err == nil {
		return nil
	}
	response, err := http.Get(URL)
	if err != nil {
		return err
	}
	audio, err := ioutil.ReadAll(response.Body)
	if err != nil {
		return err
	}
	os.MkdirAll(directory, 0755)
	// download content if not in .cache
	err = ioutil.WriteFile(filepath, audio, 0755)
	if err != nil {
		return err
	}
	return nil
}

// PlaySound play the given audio url, supported Formats: mp3, wav
func PlaySound(e *itunesapi.Episode) error {
	if Streamer != nil {
		speaker.Lock()
		Streamer.Close()
		speaker.Unlock()
	}
	URL := e.AudioURL
	filename := fmt.Sprintf("%s.mp3", e.Id)
	directory := config.CachePath
	filename = url.PathEscape(path.Clean(strings.ReplaceAll(filename, ":", "")))
	filepath := path.Join(directory, filename)
	file, err := os.Open(filepath)
	if err != nil {
		err = fetchContent(URL, filepath, directory)
		if err != nil {
			return err
		}
	}
	file, err = os.Open(filepath)
	if err != nil {
		return err
	}
	Streamer, Format, err = mp3.Decode(file)
	if err != nil {
		Streamer, Format, err = wav.Decode(file)
	}
	if err != nil {
		return errors.New("Unsupported audio format")
	}
	sr := Format.SampleRate * 2
	speaker.Init(sr, sr.N(time.Millisecond*500))

	streamer := beep.Resample(4, Format.SampleRate, sr, Streamer)
	MainCtrl = &beep.Ctrl{Streamer: streamer}
	resampler = beep.ResampleRatio(4, 1, MainCtrl)
	Volume = &effects.Volume{Streamer: resampler, Base: 2}
	speaker.Play(Volume)
	e.DurationInMilliseconds = int(float32(Streamer.Len())/float32(Format.SampleRate)) * 1000
	return nil
}

func PauseSong(state bool) {
	speaker.Lock()
	MainCtrl.Paused = state
	speaker.Unlock()
}

func IncreaseSpeed() {
	speed := resampler.Ratio() * 1.100000e+000
	if speed >= 1.800000e+000 {
		return
	}
	speaker.Lock()
	resampler.SetRatio(speed)
	speaker.Unlock()
}

func DecreaseSpeed() {
	speed := resampler.Ratio() * 0.900000e+000
	if speed <= 1.000000e+000 {
		return
	}
	speaker.Lock()
	resampler.SetRatio(speed)
	speaker.Unlock()
}

func Seek(pos int) error {
	if MainCtrl != nil {
		speaker.Lock()
		err := Streamer.Seek(Format.SampleRate.N(time.Second) * pos)
		speaker.Unlock()
		return err
	}
	return nil
}

func SetVolume(percent int) {
	if percent > 100 {
		return
	}

	if percent == 0 {
		Volume.Silent = true
	} else {
		Volume.Silent = false
		Volume.Volume = -float64(100-percent) / 100.0 * 5
	}
}

func Position() int {
	return int(Format.SampleRate.D(Streamer.Position()).Round(time.Second).Seconds())
}


================================================
FILE: config/main.go
================================================
package config

import (
	"os"
	"path"
)

var CachePath = path.Join(os.TempDir(), "podcast-cli/")


================================================
FILE: go.mod
================================================
module github.com/goulinkh/podcast-cli

go 1.14

require (
	github.com/PuerkitoBio/goquery v1.5.1
	github.com/akamensky/argparse v1.2.1 // indirect
	github.com/faiface/beep v1.0.2
	github.com/gizak/termui/v3 v3.1.0
	github.com/tidwall/gjson v1.6.0
)


================================================
FILE: go.sum
================================================
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/akamensky/argparse v1.2.1 h1:YMYF1VMku+dnz7TVTJpYhsCXHSYCVMAIcKaBbjwbvZo=
github.com/akamensky/argparse v1.2.1/go.mod h1:S5kwC7IuDcEr5VeXtGPRVZ5o/FdhcMlQz4IZQuw64xA=
github.com/andybalholm/cascadia v1.1.0 h1:BuuO6sSfQNFRu1LppgbD25Hr2vLYW25JvxHs5zzsLTo=
github.com/andybalholm/cascadia v1.1.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y=
github.com/faiface/beep v1.0.2 h1:UB5DiRNmA4erfUYnHbgU4UB6DlBOrsdEFRtcc8sCkdQ=
github.com/faiface/beep v1.0.2/go.mod h1:1yLb5yRdHMsovYYWVqYLioXkVuziCSITW1oarTeduQM=
github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg=
github.com/gdamore/tcell v1.1.1/go.mod h1:K1udHkiR3cOtlpKG5tZPD5XxrF7v2y7lDq7Whcj+xkQ=
github.com/gizak/termui/v3 v3.1.0 h1:ZZmVDgwHl7gR7elfKf1xc4IudXZ5qqfDh4wExk4Iajc=
github.com/gizak/termui/v3 v3.1.0/go.mod h1:bXQEBkJpzxUAKf0+xq9MSWAvWZlE7c+aidmyFlkYTrY=
github.com/gopherjs/gopherjs v0.0.0-20180628210949-0892b62f0d9f/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gopherjs/gopherjs v0.0.0-20180825215210-0210a2f0f73c/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gopherjs/gopherwasm v0.1.1/go.mod h1:kx4n9a+MzHH0BJJhvlsQ65hqLFXDO/m256AsaDPQ+/4=
github.com/gopherjs/gopherwasm v1.0.0/go.mod h1:SkZ8z7CWBz5VXbhJel8TxCmAcsQqzgWGR/8nMhyhZSI=
github.com/hajimehoshi/go-mp3 v0.1.1 h1:Y33fAdTma70fkrxnc9u50Uq0lV6eZ+bkAlssdMmCwUc=
github.com/hajimehoshi/go-mp3 v0.1.1/go.mod h1:4i+c5pDNKDrxl1iu9iG90/+fhP37lio6gNhjCx9WBJw=
github.com/hajimehoshi/oto v0.1.1/go.mod h1:hUiLWeBQnbDu4pZsAhOnGqMI1ZGibS6e2qhQdfpwz04=
github.com/hajimehoshi/oto v0.3.1 h1:cpf/uIv4Q0oc5uf9loQn7PIehv+mZerh+0KKma6gzMk=
github.com/hajimehoshi/oto v0.3.1/go.mod h1:e9eTLBB9iZto045HLbzfHJIc+jP3xaKrjZTghvb6fdM=
github.com/jfreymuth/oggvorbis v1.0.0/go.mod h1:abe6F9QRjuU9l+2jek3gj46lu40N4qlYxh2grqkLEDM=
github.com/jfreymuth/vorbis v1.0.0/go.mod h1:8zy3lUAm9K/rJJk223RKy6vjCZTWC61NA2QD06bfOE0=
github.com/lucasb-eyer/go-colorful v0.0.0-20181028223441-12d3b2882a08/go.mod h1:NXg0ArsFk0Y01623LgUqoqcouGDB+PwCCQlrwrG6xJ4=
github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
github.com/mattn/go-runewidth v0.0.4 h1:2BvfKmzob6Bmd4YsL0zygOqfdFnK7GR4QL06Do4/p7Y=
github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
github.com/mewkiz/flac v1.0.5/go.mod h1:EHZNU32dMF6alpurYyKHDLYpW1lYpBZ5WrXi/VuNIGs=
github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7 h1:DpOJ2HYzCv8LZP15IdmG+YdwD2luVPHITV96TkirNBM=
github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo=
github.com/nsf/termbox-go v0.0.0-20190121233118-02980233997d h1:x3S6kxmy49zXVVyhcnrFqxvNVCBPb2KZ9hV2RBdS840=
github.com/nsf/termbox-go v0.0.0-20190121233118-02980233997d/go.mod h1:IuKpRQcYE1Tfu+oAQqaLisqDeXgjyyltCfsaoYN18NQ=
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/tidwall/gjson v1.6.0 h1:9VEQWz6LLMUsUl6PueE49ir4Ka6CzLymOAZDxpFsTDc=
github.com/tidwall/gjson v1.6.0/go.mod h1:P256ACg0Mn+j1RXIDXoss50DeIABTYK1PULOJHhxOls=
github.com/tidwall/match v1.0.1 h1:PnKP62LPNxHKTwvHHZZzdOAOCtsJTjo6dZLCwpKm5xc=
github.com/tidwall/match v1.0.1/go.mod h1:LujAq0jyVjBy028G1WhWfIzbpQfMO8bBZ6Tyb0+pL9E=
github.com/tidwall/pretty v1.0.0 h1:HsD+QiTn7sK6flMKIvNmpqz1qrpP3Ps6jOKIKMooyg4=
github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/exp v0.0.0-20180710024300-14dda7b62fcd/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs=
golang.org/x/mobile v0.0.0-20180806140643-507816974b79/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20200202094626-16171245cfb2 h1:CCH4IOTTfewWjGOlSp+zGcjutRKlBEZQ6wTn8ozI/nI=
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/sys v0.0.0-20181228144115-9a3f9b0469bb/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a h1:1BGLXjeY4akVXGgbC9HugT3Jv3hCI0z56oJR5vAMgBU=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
gopkg.in/DATA-DOG/go-sqlmock.v1 v1.3.0/go.mod h1:OdE7CF6DbADk7lN8LIKRzRJTTZXIjtWgA5THM5lhBAw=


================================================
FILE: itunes-api/episodes.go
================================================
package itunesapi

type Episode struct {
	Id                     string `json:"id"`
	Artwork                string `json:"artwork"`
	Title                  string `json:"title"`
	AudioURL               string `json:"audiourl"`
	ReleaseDate            string `json:"releasedate"`
	DurationInMilliseconds int    `json:"duratioInMilliseconds"`
	Description            string `json:"description"`
}


================================================
FILE: itunes-api/genres.go
================================================
package itunesapi

import (
	"net/http"
	"regexp"

	"github.com/PuerkitoBio/goquery"
)

const applePodcastsMainPage = "https://podcasts.apple.com/genre/podcasts/id26"

var (
	authorization string
)

type Genre struct {
	Text     string   `json:"text"`
	URL      string   `json:"url"`
	Id       string   `json:"url"`
	SubGenre []*Genre `json:"sub-genres"`
}

func getGenreId(url string) string {
	idRegExp := regexp.MustCompile(`\d+$`)
	return idRegExp.FindString(url)
}

func GetGenres() ([]*Genre, error) {
	resp, err := http.Get(applePodcastsMainPage)
	if err != nil {
		return nil, err
	}
	doc, err := goquery.NewDocumentFromResponse(resp)
	if err != nil {
		return nil, err
	}
	genres := make([]*Genre, 0)
	doc.Find(".list.column > li").Each(func(i int, s *goquery.Selection) {
		genreSelection := s.Find(".top-level-genre")
		href, exists := genreSelection.Attr("href")
		if !exists {
			return
		}

		genre := &Genre{Text: genreSelection.Text(), URL: href, SubGenre: make([]*Genre, 0), Id: getGenreId(href)}

		s.Find(".top-level-subgenres > li > a").Each(func(i int, s *goquery.Selection) {
			href, exists := genreSelection.Attr("href")
			if !exists {
				return
			}
			genre.SubGenre = append(genre.SubGenre, &Genre{Text: s.Text(), URL: href, Id: getGenreId(href)})
		})
		genres = append(genres, genre)

	})
	return genres, nil
}


================================================
FILE: itunes-api/podcasts.go
================================================
package itunesapi

import (
	"errors"
	"fmt"
	"io/ioutil"
	"net/http"
	"regexp"

	"github.com/tidwall/gjson"
)

type Podcast struct {
	Title       string `json:"title"`
	URL         string `json:"url`
	Id          string `json:"id"`
	Description string `json:"description"`
	Author      string `json:"author"`
}

func FindPodcasts(query string) ([]*Podcast, error) {
	authorization, err := getAuthorization()
	if err != nil {
		return nil, err
	}

	request, err := http.NewRequest("GET", fmt.Sprintf("https://itunes.apple.com/search?country=us&entity=podcast&term=%s", query), nil)
	if err != nil {
		return nil, err
	}
	request.Header.Add("Authorization", authorization)
	resp, err := http.DefaultClient.Do(request)
	if err != nil {
		return nil, err
	}

	data, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		return nil, err
	}

	podcastsJSON := gjson.Get(string(data), "results").Array()
	podcasts := make([]*Podcast, 0)
	for _, podcast := range podcastsJSON {
		if podcast.Get("kind").String() == "podcast" && podcast.Get("wrapperType").String() == "track" {
			podcasts = append(podcasts, &Podcast{
				Author:      podcast.Get("artistName").String(),
				Description: "",
				Id:          podcast.Get("trackId").String(),
				Title:       podcast.Get("collectionName").String(),
				URL:         regexp.MustCompile(`\?.*$`).ReplaceAllString(podcast.Get("collectionViewUrl").String(), ""),
			})
		}
	}
	return podcasts, nil
}
func (p *Podcast) GetEpisodes() ([]*Episode, error) {
	req, 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)
	if err != nil {
		return nil, err

	}
	authorization, err := getAuthorization()
	if err != nil {
		return nil, err
	}
	req.Header.Add("Authorization", authorization)
	response, err := http.DefaultClient.Do(req)
	if err != nil {
		return nil, err

	}
	data, err := ioutil.ReadAll(response.Body)
	if err != nil {
		return nil, err

	}
	episodesJSON := gjson.Get(string(data), "data").Array()
	episodes := make([]*Episode, 0)
	for _, episode := range episodesJSON {
		if episode.Get(`attributes.mediaKind`).String() == "audio" {

			episodes = append(episodes, &Episode{
				Id:                     episode.Get(`id`).String(),
				Artwork:                episode.Get(`attributes.artwork.url`).String(),
				Title:                  episode.Get(`attributes.name`).String(),
				AudioURL:               episode.Get(`attributes.assetUrl`).String(),
				ReleaseDate:            episode.Get(`attributes.releaseDateTime`).String(),
				DurationInMilliseconds: int(episode.Get(`attributes.durationInMilliseconds`).Int()),
				Description:            episode.Get(`attributes.description.standard`).String(),
			})
		}
	}
	return episodes, nil
}

func (g *Genre) GetPodcasts() ([]*Podcast, error) {
	request, err := http.NewRequest("GET", "https://amp-api.podcasts.apple.com/v1/catalog/us/charts?types=podcasts&limit=200&genre="+g.Id, nil)
	if err != nil {
		return nil, err
	}
	authorization, err := getAuthorization()
	if err != nil {
		return nil, err
	}
	request.Header.Add("Authorization", authorization)
	resp, err := http.DefaultClient.Do(request)
	if err != nil {
		return nil, err
	}

	data, err := ioutil.ReadAll(resp.Body)
	podcastsJSON := gjson.Get(string(data), "results.podcasts.0.data").Array()
	podcasts := make([]*Podcast, len(podcastsJSON))
	for i, podcast := range podcastsJSON {
		podcasts[i] = &Podcast{
			Id:          podcast.Get("id").String(),
			Description: podcast.Get("attributes.description.standard").String(),
			Title:       podcast.Get("attributes.name").String(),
			URL:         "https://amp-api.podcasts.apple.com" + podcast.Get("href").String(),
			Author:      podcast.Get("attributes.artistName").String(),
		}
	}
	return podcasts, nil
}

func getAuthorization() (string, error) {
	if authorization != "" {
		return authorization, nil
	}
	authRegEx := regexp.MustCompile("privateKeyPath.+token%22%3A%22(?P<Bearer>.*?)%22%7D%2C%22")
	resp, err := http.Get("https://podcasts.apple.com/us/podcast/the-joe-rogan-experience/id360084272")
	if err != nil {
		return "", err
	}
	data, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		return "", err
	}
	match := authRegEx.FindStringSubmatch(string(data))
	if len(match) != 2 {
		return "", errors.New("Authorization access token is not found")
	}
	authorization = "Bearer " + match[1]
	return authorization, nil
}


================================================
FILE: main.go
================================================
package main

import (
	"log"
	"os"

	"github.com/akamensky/argparse"
	ui "github.com/gizak/termui/v3"

	itunesapi "github.com/goulinkh/podcast-cli/itunes-api"
	"github.com/goulinkh/podcast-cli/rss"
	podcastcliui "github.com/goulinkh/podcast-cli/ui"
)

func main() {

	parser := argparse.NewParser("podcast-cli", "CLI podcast player")
	podcastSearchQuery := parser.String("s", "search", &argparse.Options{Required: false, Help: "your podcast's name"})
	rssUrl := parser.String("r", "rss", &argparse.Options{Required: false, Help: "custom podcast rss source"})
	offset := parser.Int("o", "offset", &argparse.Options{Required: false, Help: "play episode number"})
	err := parser.Parse(os.Args)
	if err != nil {
		log.Fatalln("Error:", parser.Usage(err))
		return
	}
	err = podcastcliui.InitUI()
	if err != nil {
		log.Fatal(err)
	}

	if podcastSearchQuery != nil && *podcastSearchQuery != "" {
		podcasts, err := itunesapi.FindPodcasts(*podcastSearchQuery)
		if err != nil {
			log.Fatalln("Error: Failed to search for podcasts")
		}
		podcastsWidget := &podcastcliui.PodcastsUI{Podcasts: podcasts}
		podcastsWidget.InitComponents()
		podcastcliui.Show(podcastsWidget)
	} else if rssUrl != nil && *rssUrl != "" {
		episodes, err := rss.ParseEpisodes(*rssUrl)
		if err != nil {
			log.Fatalln("Error: Failed to get episodes from the url: " + *rssUrl)
		}
		episodesWidget := &podcastcliui.EpisodesUI{Episodes: episodes}
		episodesWidget.InitComponents()
		podcastcliui.Show(episodesWidget)
		if offset != nil {
			episodesWidget.Play(*offset)
		}
	} else {
		genres, err := itunesapi.GetGenres()
		if err != nil {
			log.Fatal(err)
		}
		genreWidget := &podcastcliui.GenresUI{
			Genres: genres,
		}
		genreWidget.InitComponents()
		podcastcliui.Show(genreWidget)

	}

	uiEvents := ui.PollEvents()
	for {
		select {
		case e := <-uiEvents:
			cmd, err := podcastcliui.HandleKeyEvent(&e)
			if err != nil {
				log.Fatal(err)
			}
			if cmd == podcastcliui.Exit {
				return
			}
		}
	}
}


================================================
FILE: rss/parser.go
================================================
package rss

import (
	"encoding/xml"
	"io/ioutil"
	"net/http"
	"strconv"

	itunesapi "github.com/goulinkh/podcast-cli/itunes-api"
)

type Rss struct {
	Channel struct {
		Item []struct {
			Title       string `xml:"title"`
			PubDate     string `xml:"pubDate"`
			Author      string `xml:"author"`
			Description string `xml:"description"`
			Image       struct {
				Href string `xml:"href,attr"`
			} `xml:"image"`
			Enclosure struct {
				URL    string `xml:"url,attr"`
				Length int    `xml:"length,attr"`
				Type   string `xml:"type,attr"`
			} `xml:"enclosure"`
			Duration int `xml:"duration"`
		} `xml:"item"`
	} `xml:"channel"`
}

func ParseEpisodes(rssUrl string) ([]*itunesapi.Episode, error) {
	resp, err := http.Get(rssUrl)
	if err != nil {
		return nil, err
	}

	rss, err := ioutil.ReadAll(resp.Body)
	var podcast Rss
	err = xml.Unmarshal(rss, &podcast)
	episodes := make([]*itunesapi.Episode, len(podcast.Channel.Item))
	for i, e := range podcast.Channel.Item {
		episodes[i] =
			&itunesapi.Episode{
				Artwork:                e.Image.Href,
				AudioURL:               e.Enclosure.URL,
				Description:            e.Description,
				DurationInMilliseconds: e.Duration * 1000,
				Id:                     strconv.Itoa(i),
				ReleaseDate:            e.PubDate,
				Title:                  e.Title,
			}
	}
	return episodes, nil
}


================================================
FILE: ui/Podcasts.go
================================================
package ui

import (
	"errors"
	"fmt"
	"strings"

	ui "github.com/gizak/termui/v3"
	"github.com/gizak/termui/v3/widgets"
	itunesapi "github.com/goulinkh/podcast-cli/itunes-api"
)

type PodcastsUI struct {
	Podcasts      []*itunesapi.Podcast
	listWidget    *widgets.List
	detailsWidget *widgets.Paragraph
	gridWidget    *ui.Grid
}

func (p *PodcastsUI) InitComponents() error {
	p.initListWidget()
	p.initDetailsWidget()
	err := p.initGridWidget()
	return err
}
func (p *PodcastsUI) MainUI() *ui.Grid {
	return p.gridWidget
}
func (p *PodcastsUI) HandleEvent(event *ui.Event) (Command, error) {
	switch event.ID {
	case "j", "<Down>":
		p.listWidget.ScrollDown()
		p.updateDetailsWidget()
	case "k", "<Up>":
		p.listWidget.ScrollUp()
		p.updateDetailsWidget()

	case "<Enter>":
		episodes, err := p.Podcasts[p.listWidget.SelectedRow].GetEpisodes()
		if err != nil {
			return Nothing, err
		}
		episodesUI := &EpisodesUI{Episodes: episodes}
		err = episodesUI.InitComponents()
		if err != nil {
			return Nothing, err
		}
		Show(episodesUI)
	}
	return Nothing, nil
}
func (p *PodcastsUI) initGridWidget() error {
	if p.listWidget == nil {
		return errors.New("Uninitialized podcasts list widget")
	}
	if p.detailsWidget == nil {
		return errors.New("Uninitialized details widget")
	}
	p.gridWidget = ui.NewGrid()
	termWidth, termHeight := ui.TerminalDimensions()
	p.gridWidget.SetRect(0, 0, termWidth, termHeight-1)
	p.gridWidget.Set(
		ui.NewRow(1.0,
			ui.NewCol(1.0/2, p.listWidget),
			ui.NewCol(1.0/2,
				ui.NewRow(6.0/10, p.detailsWidget),
				ui.NewRow(4.0/10, audioPlayerWidget.MainUI()))))
	return nil
}
func (p *PodcastsUI) initListWidget() {
	p.listWidget = widgets.NewList()
	p.listWidget.Title = "Podcasts List"
	p.listWidget.TextStyle.Fg = FgColor
	p.listWidget.SelectedRowStyle.Fg = ui.ColorBlack
	p.listWidget.SelectedRowStyle.Bg = AccentColor
	p.listWidget.BorderStyle.Fg = AccentColor
	p.listWidget.Rows = make([]string, len(p.Podcasts))
	for i, podcast := range p.Podcasts {
		p.listWidget.Rows[i] = podcast.Title
	}
}
func (p *PodcastsUI) initDetailsWidget() {
	p.detailsWidget = widgets.NewParagraph()
	p.detailsWidget.Title = "Details"
	p.detailsWidget.BorderStyle.Fg = AccentColor
	p.detailsWidget.BorderLeft = false
	p.detailsWidget.BorderBottom = false
	p.updateDetailsWidget()
}
func (p *PodcastsUI) updateDetailsWidget() {
	if p.Podcasts == nil || len(p.Podcasts) == 0 {
		return
	}
	currentPodcast := p.Podcasts[p.listWidget.SelectedRow]
	title := fmt.Sprintf("[Title](fg:magenta)        %s", currentPodcast.Title)
	description := fmt.Sprintf("[Description](fg:magenta)  %s", currentPodcast.Description)
	author := fmt.Sprintf("[Author](fg:magenta)       %s", currentPodcast.Author)
	p.detailsWidget.Text = strings.Join([]string{title, description, author}, "\n")
}


================================================
FILE: ui/audio_player.go
================================================
package ui

import (
	"fmt"
	"time"

	ui "github.com/gizak/termui/v3"
	"github.com/gizak/termui/v3/widgets"
	audioplayer "github.com/goulinkh/podcast-cli/audio-player"
	itunesapi "github.com/goulinkh/podcast-cli/itunes-api"
)

type AudioPlayerWidget struct {
	playlist            []*itunesapi.Episode
	nowPlaying          *itunesapi.Episode
	nowPlayingIndex     int
	paused              bool
	audioPositionWidget *widgets.Gauge
	playerStatusWidget  *widgets.Paragraph
	grid                *ui.Grid
	playSpeed           float32
}

func (ap *AudioPlayerWidget) InitComponents() {
	ap.paused = true
	ap.initAudipPositionWidget()
	ap.initPlayerStatusWidget()
	ap.initGrid()
}

func (ap *AudioPlayerWidget) MainUI() *ui.Grid {
	return ap.grid
}

func (ap *AudioPlayerWidget) HandleEvent(e *ui.Event) (Command, error) {
	switch e.ID {
	case "p", "<Space>":
		ap.Pause()
	case "<Right>":
		if audioplayer.MainCtrl != nil && ap.nowPlaying != nil {
			position := audioplayer.Position() + 10

			if position < ap.nowPlaying.DurationInMilliseconds/1000 {
				audioplayer.Seek(position)
			}
		}
	case "<Left>":
		if audioplayer.MainCtrl != nil && ap.nowPlaying != nil {
			position := audioplayer.Position() - 10

			if position > 0 {
				audioplayer.Seek(position)
			}
		}
	case "u":
		if audioplayer.MainCtrl != nil && ap.nowPlaying != nil {
			audioplayer.IncreaseSpeed()
		}
	case "d":
		if audioplayer.MainCtrl != nil && ap.nowPlaying != nil {
			audioplayer.DecreaseSpeed()
		}

	}
	return Nothing, nil
}

func (ap *AudioPlayerWidget) Play(playlist []*itunesapi.Episode, index int) {
	e := playlist[index]
	if e == nil || (ap.nowPlaying != nil && ap.nowPlaying.Id == e.Id) {
		return
	}
	ap.playerStatusWidget.Title = "Downloading audio ..."
	RefreshUI()
	go func() {
		ap.nowPlaying = e
		ap.playlist = playlist
		ap.nowPlayingIndex = index
		ap.paused = false
		ap.playerStatusWidget.Title = "Now Playing"
		ap.playAudio(ap.nowPlaying)
		for {
			select {
			case <-time.After(time.Millisecond * 100):
				if ap.paused {
					ap.audioPositionWidget.Title = "Paused"
				} else {
					ap.playerStatusWidget.Text = ap.nowPlaying.Title
					position := audioplayer.Position()
					ap.audioPositionWidget.Title = "Running"
					ap.audioPositionWidget.Label = fmt.Sprintf("%d:%d", position/60, position%60)
					audioDuration := e.DurationInMilliseconds / 1000
					if audioDuration > 0 {
						ap.audioPositionWidget.Percent = (position * 100) / audioDuration
					}
					if ap.audioPositionWidget.Percent == 100 {
						ap.Play(playlist, index+1)
						return
					}
				}
				RefreshUI()
			}
		}
	}()
	return
}

func (ap *AudioPlayerWidget) playAudio(e *itunesapi.Episode) {
	if err := audioplayer.PlaySound(e); err != nil {
		ap.playerStatusWidget.Title = "Failed to play audio"
		RefreshUI()
	}
}

func (ap *AudioPlayerWidget) Pause() {
	ap.paused = !ap.paused
	audioplayer.PauseSong(ap.paused)
}

func (ap *AudioPlayerWidget) initAudipPositionWidget() {
	ap.audioPositionWidget = widgets.NewGauge()
	ap.audioPositionWidget.BorderLeft = false
	ap.audioPositionWidget.BarColor = AccentColor
	ap.audioPositionWidget.BorderStyle.Fg = AccentColor
}
func (ap *AudioPlayerWidget) initPlayerStatusWidget() {
	ap.playerStatusWidget = widgets.NewParagraph()
	ap.playerStatusWidget.BorderLeft = false
	ap.playerStatusWidget.BorderBottom = false
	ap.playerStatusWidget.TextStyle.Fg = AccentColor
	ap.playerStatusWidget.Title = "Now Playing"
	ap.playerStatusWidget.TitleStyle.Fg = FgColor
	ap.playerStatusWidget.BorderStyle.Fg = AccentColor
}
func (ap *AudioPlayerWidget) initGrid() {
	ap.grid = ui.NewGrid()
	ap.grid.Border = false
	ap.grid.Set(
		ui.NewRow(1.0,
			ui.NewRow(1.0/2, ap.playerStatusWidget),
			ui.NewRow(1.0/2, ap.audioPositionWidget),
		),
	)
}


================================================
FILE: ui/commands.go
================================================
package ui

type Command int

const (
	Nothing Command = 0
	Exit    Command = 1
)


================================================
FILE: ui/episodes.go
================================================
package ui

import (
	"errors"
	"fmt"
	"strings"

	ui "github.com/gizak/termui/v3"
	"github.com/gizak/termui/v3/widgets"
	itunesapi "github.com/goulinkh/podcast-cli/itunes-api"
)

type EpisodesUI struct {
	Episodes      []*itunesapi.Episode
	listWidget    *widgets.List
	detailsWidget *widgets.Paragraph
	gridWidget    *ui.Grid
}

func (e *EpisodesUI) InitComponents() error {
	e.initListWidget()
	e.initDetailsWidget()
	err := e.initGridWidget()
	return err
}

func (e *EpisodesUI) MainUI() *ui.Grid {
	return e.gridWidget
}

func (e *EpisodesUI) HandleEvent(event *ui.Event) (Command, error) {
	switch event.ID {
	case "j", "<Down>":
		e.listWidget.ScrollDown()
		e.updateDetailsWidget()
	case "k", "<Up>":
		e.listWidget.ScrollUp()
		e.updateDetailsWidget()

	case "<Enter>":
		audioPlayerWidget.Play(e.Episodes, e.listWidget.SelectedRow)
	}
	return Nothing, nil
}
func (e *EpisodesUI) Play(index int) {
	audioPlayerWidget.Play(e.Episodes, index)

}
func (e *EpisodesUI) initGridWidget() error {
	if e.listWidget == nil {
		return errors.New("List widget is not initialized")
	}
	if e.detailsWidget == nil {
		return errors.New("Details widget is not initialized")
	}
	e.gridWidget = ui.NewGrid()
	termWidth, termHeight := ui.TerminalDimensions()
	e.gridWidget.SetRect(0, 0, termWidth, termHeight-1)
	e.gridWidget.Set(
		ui.NewRow(1.0,
			ui.NewCol(1.0/2, e.listWidget),
			ui.NewCol(1.0/2,
				ui.NewRow(6.0/10, e.detailsWidget),
				ui.NewRow(4.0/10, audioPlayerWidget.MainUI()))))
	return nil
}

func (e *EpisodesUI) initListWidget() {
	e.listWidget = widgets.NewList()
	e.listWidget.Title = "Episodes"
	e.listWidget.SelectedRowStyle.Modifier = ui.ModifierClear
	e.listWidget.TextStyle.Fg = FgColor
	e.listWidget.SelectedRowStyle.Fg = ui.ColorBlack
	e.listWidget.SelectedRowStyle.Bg = AccentColor
	e.listWidget.BorderStyle.Fg = AccentColor
	e.listWidget.Rows = make([]string, len(e.Episodes))
	for i, episode := range e.Episodes {
		e.listWidget.Rows[i] = episode.Title
	}
}
func (e *EpisodesUI) initDetailsWidget() {
	e.detailsWidget = widgets.NewParagraph()
	e.detailsWidget.Title = "Details"
	e.detailsWidget.BorderStyle.Fg = AccentColor
	e.detailsWidget.BorderLeft = false
	e.detailsWidget.BorderBottom = false
	e.updateDetailsWidget()
}
func (e *EpisodesUI) updateDetailsWidget() {
	if e.Episodes == nil || len(e.Episodes) == 0 {
		return
	}
	currentEpisode := e.Episodes[e.listWidget.SelectedRow]
	title := fmt.Sprintf("[Title](fg:magenta)        %s", currentEpisode.Title)
	description := fmt.Sprintf("[Description](fg:magenta)  %s", currentEpisode.Description)
	date := fmt.Sprintf("[Release Date](fg:magenta) %s", currentEpisode.ReleaseDate)
	duration := fmt.Sprintf("[Duration](fg:magenta)     %d min", currentEpisode.DurationInMilliseconds/60000)
	e.detailsWidget.Text = strings.Join([]string{title, duration, date, description}, "\n")
}


================================================
FILE: ui/genres.go
================================================
package ui

import (
	"errors"

	ui "github.com/gizak/termui/v3"
	"github.com/gizak/termui/v3/widgets"
	itunesapi "github.com/goulinkh/podcast-cli/itunes-api"
)

type GenresUI struct {
	Genres     []*itunesapi.Genre
	gridWidget *ui.Grid
	listWidget *widgets.List
}

func (g *GenresUI) InitComponents() error {
	g.newGenresListWidget()
	err := g.newGridWidget()
	if err != nil {
		return err
	}
	return nil
}
func (g *GenresUI) MainUI() *ui.Grid {
	return g.gridWidget
}
func (g *GenresUI) HandleEvent(event *ui.Event) (Command, error) {
	switch event.ID {
	case "j", "<Down>":
		g.listWidget.ScrollDown()

	case "k", "<Up>":
		g.listWidget.ScrollUp()
	case "<Enter>":
		subGenres := g.Genres[g.listWidget.SelectedRow].SubGenre
		var subGenreUI *SubGenresUI
		if subGenres == nil || len(subGenres) == 0 {
			subGenreUI = &SubGenresUI{Genres: []*itunesapi.Genre{g.Genres[g.listWidget.SelectedRow]}}
		} else {
			subGenreUI = &SubGenresUI{Genres: g.Genres[g.listWidget.SelectedRow].SubGenre}
		}
		subGenreUI.InitComponents()
		Show(subGenreUI)
	}
	return Nothing, nil
}
func (g *GenresUI) newGenresListWidget() error {
	g.listWidget = widgets.NewList()
	g.listWidget.Title = "Select a Genre"
	g.listWidget.TextStyle = ui.NewStyle(FgColor)
	g.listWidget.SelectedRowStyle.Fg = ui.ColorBlack
	g.listWidget.SelectedRowStyle.Bg = AccentColor
	g.listWidget.BorderStyle.Fg = AccentColor
	if g.Genres == nil {
		return errors.New("Missing Genres array")
	}
	g.listWidget.Rows = make([]string, len(g.Genres))
	for i, genre := range g.Genres {
		g.listWidget.Rows[i] = genre.Text
	}
	return nil
}
func (g *GenresUI) newGridWidget() error {
	if g.listWidget == nil {
		return errors.New("Uninitialized genres list widget")
	}
	g.gridWidget = ui.NewGrid()
	termWidth, termHeight := ui.TerminalDimensions()
	g.gridWidget.SetRect(0, 0, termWidth, termHeight-1)
	placeholder := ui.NewBlock()
	placeholder.BorderBottom = false
	placeholder.BorderLeft = false
	placeholder.BorderStyle.Fg = AccentColor
	g.gridWidget.Set(
		ui.NewRow(1.0,
			ui.NewCol(1.0/2, g.listWidget),
			ui.NewCol(1.0/2,
				ui.NewRow(6.0/10, placeholder),
				ui.NewRow(4.0/10, audioPlayerWidget.MainUI()))))
	return nil
}


================================================
FILE: ui/page.go
================================================
package ui

import (
	"errors"
	"fmt"

	ui "github.com/gizak/termui/v3"
	"github.com/gizak/termui/v3/widgets"
)

var (
	FgColor           = ui.ColorWhite
	AccentColor       = ui.ColorMagenta
	pagesHistory      = make([]Page, 0)
	currentPage       Page
	helpBarWidget     *widgets.Paragraph
	audioPlayerWidget = &AudioPlayerWidget{}
)

type Page interface {
	MainUI() *ui.Grid
	HandleEvent(*ui.Event) (Command, error)
}

func InitUI() error {
	if err := ui.Init(); err != nil {
		errors.New(fmt.Sprintf("failed to initialize the UI: %v", err))
	}
	helpBarWidget = newHelpBarWidget()
	audioPlayerWidget.InitComponents()
	return nil
}
func Show(p Page) {
	if currentPage != nil {
		pagesHistory = append(pagesHistory, currentPage)
	}

	show(p)
}

func show(p Page) {
	currentPage = p
	RefreshUI()
}

func RefreshUI() {
	ui.Clear()
	ui.Render(currentPage.MainUI(), helpBarWidget)
}

func GoBack() {
	if len(pagesHistory) == 0 {
		return
	}
	previousPage := pagesHistory[len(pagesHistory)-1]
	pagesHistory = pagesHistory[:len(pagesHistory)-1]
	show(previousPage)
}
func HandleKeyEvent(e *ui.Event) (Command, error) {
	switch e.ID {
	case "q", "<C-c>":
		ui.Close()
		return Exit, nil

	case "<Escape>", "<C-<Backspace>>", "<Backspace>":
		GoBack()
		RefreshUI()
	case "<Resize>":
		payload := e.Payload.(ui.Resize)
		helpBarWidget.SetRect(0, payload.Height-1, payload.Width, payload.Height)
		currentPage.MainUI().SetRect(0, 0, payload.Width, payload.Height-1)
		RefreshUI()
	default:
		cmd, err := currentPage.HandleEvent(e)
		if err != nil {
			return cmd, err
		}
		cmd, err = audioPlayerWidget.HandleEvent(e)
		RefreshUI()
		return cmd, err
	}
	return Nothing, nil

}

func newHelpBarWidget() *widgets.Paragraph {
	helpBarWidget := widgets.NewParagraph()
	helpBarWidget.Text = "[ Enter ](fg:black)[Select](fg:black,bg:green) " +
		"[ p, Space ](fg:black)[Play/Pause](fg:black,bg:green) " +
		"[Esc ](fg:black)[Back](fg:black,bg:green) " +
		"[Right ](fg:black)[+10s](fg:black,bg:green) " +
		"[Left ](fg:black)[-10s](fg:black,bg:green) " +
		"[ d ](fg:black)[Slowdown](fg:black,bg:green)" +
		"[ u ](fg:black)[Speedup](fg:black,bg:green)" +
		"[ q ](fg:black)[Exit](fg:black,bg:green)"
	helpBarWidget.Border = false
	helpBarWidget.WrapText = true
	helpBarWidget.TextStyle = ui.Style{Modifier: ui.ModifierBold, Bg: ui.ColorWhite}
	termWidth, termHeight := ui.TerminalDimensions()
	helpBarWidget.SetRect(0, termHeight-1, termWidth, termHeight)
	return helpBarWidget
}


================================================
FILE: ui/sub_genres.go
================================================
package ui

import (
	"errors"

	ui "github.com/gizak/termui/v3"
	"github.com/gizak/termui/v3/widgets"
	itunesapi "github.com/goulinkh/podcast-cli/itunes-api"
)

type SubGenresUI struct {
	Genres     []*itunesapi.Genre
	gridWidget *ui.Grid
	listWidget *widgets.List
}

func (g *SubGenresUI) InitComponents() error {
	g.newGenresListWidget()
	err := g.newGridWidget()
	if err != nil {
		return err
	}
	return nil
}
func (g *SubGenresUI) MainUI() *ui.Grid {
	return g.gridWidget
}
func (g *SubGenresUI) HandleEvent(event *ui.Event) (Command, error) {
	switch event.ID {
	case "j", "<Down>":
		g.listWidget.ScrollDown()

	case "k", "<Up>":
		g.listWidget.ScrollUp()
	case "<Enter>":
		podcasts, err := g.Genres[g.listWidget.SelectedRow].GetPodcasts()
		if err != nil {
			return Nothing, err
		}
		podcastsUI := &PodcastsUI{Podcasts: podcasts}
		err = podcastsUI.InitComponents()
		if err != nil {
			return Nothing, err
		}
		Show(podcastsUI)
	}
	return Nothing, nil
}
func (g *SubGenresUI) newGenresListWidget() error {
	g.listWidget = widgets.NewList()
	g.listWidget.Title = "Select a Sub Genre"
	g.listWidget.TextStyle = ui.NewStyle(FgColor)
	g.listWidget.SelectedRowStyle.Fg = ui.ColorBlack
	g.listWidget.SelectedRowStyle.Bg = AccentColor
	g.listWidget.BorderStyle.Fg = AccentColor
	if g.Genres == nil {
		return errors.New("Missing Sub Genres array")
	}
	g.listWidget.Rows = make([]string, len(g.Genres))
	for i, genre := range g.Genres {
		g.listWidget.Rows[i] = genre.Text
	}
	return nil
}
func (g *SubGenresUI) newGridWidget() error {
	if g.listWidget == nil {
		return errors.New("Uninitialized sub genres list widget")
	}
	g.gridWidget = ui.NewGrid()
	termWidth, termHeight := ui.TerminalDimensions()
	g.gridWidget.SetRect(0, 0, termWidth, termHeight-1)
	placeholder := ui.NewBlock()
	placeholder.BorderBottom = false
	placeholder.BorderLeft = false
	placeholder.BorderStyle.Fg = AccentColor
	g.gridWidget.Set(
		ui.NewRow(1.0,
			ui.NewCol(1.0/2, g.listWidget),
			ui.NewCol(1.0/2,
				ui.NewRow(6.0/10, placeholder),
				ui.NewRow(4.0/10, audioPlayerWidget.MainUI()))))
	return nil
}
func (g *SubGenresUI) refreshComponents() {
	g.newGenresListWidget()
}
Download .txt
gitextract_b8au3yeh/

├── .gitignore
├── .vscode/
│   └── launch.json
├── README.md
├── audio-player/
│   └── player.go
├── config/
│   └── main.go
├── go.mod
├── go.sum
├── itunes-api/
│   ├── episodes.go
│   ├── genres.go
│   └── podcasts.go
├── main.go
├── rss/
│   └── parser.go
└── ui/
    ├── Podcasts.go
    ├── audio_player.go
    ├── commands.go
    ├── episodes.go
    ├── genres.go
    ├── page.go
    └── sub_genres.go
Download .txt
SYMBOL INDEX (74 symbols across 13 files)

FILE: audio-player/player.go
  type AudioPlayer (line 23) | type AudioPlayer struct
  function init (line 37) | func init() {
  function fetchContent (line 41) | func fetchContent(URL string, filepath string, directory string) error {
  function PlaySound (line 65) | func PlaySound(e *itunesapi.Episode) error {
  function PauseSong (line 106) | func PauseSong(state bool) {
  function IncreaseSpeed (line 112) | func IncreaseSpeed() {
  function DecreaseSpeed (line 122) | func DecreaseSpeed() {
  function Seek (line 132) | func Seek(pos int) error {
  function SetVolume (line 142) | func SetVolume(percent int) {
  function Position (line 155) | func Position() int {

FILE: itunes-api/episodes.go
  type Episode (line 3) | type Episode struct

FILE: itunes-api/genres.go
  constant applePodcastsMainPage (line 10) | applePodcastsMainPage = "https://podcasts.apple.com/genre/podcasts/id26"
  type Genre (line 16) | type Genre struct
  function getGenreId (line 23) | func getGenreId(url string) string {
  function GetGenres (line 28) | func GetGenres() ([]*Genre, error) {

FILE: itunes-api/podcasts.go
  type Podcast (line 13) | type Podcast struct
    method GetEpisodes (line 57) | func (p *Podcast) GetEpisodes() ([]*Episode, error) {
  function FindPodcasts (line 21) | func FindPodcasts(query string) ([]*Podcast, error) {
  method GetPodcasts (line 97) | func (g *Genre) GetPodcasts() ([]*Podcast, error) {
  function getAuthorization (line 127) | func getAuthorization() (string, error) {

FILE: main.go
  function main (line 15) | func main() {

FILE: rss/parser.go
  type Rss (line 12) | type Rss struct
  function ParseEpisodes (line 32) | func ParseEpisodes(rssUrl string) ([]*itunesapi.Episode, error) {

FILE: ui/Podcasts.go
  type PodcastsUI (line 13) | type PodcastsUI struct
    method InitComponents (line 20) | func (p *PodcastsUI) InitComponents() error {
    method MainUI (line 26) | func (p *PodcastsUI) MainUI() *ui.Grid {
    method HandleEvent (line 29) | func (p *PodcastsUI) HandleEvent(event *ui.Event) (Command, error) {
    method initGridWidget (line 52) | func (p *PodcastsUI) initGridWidget() error {
    method initListWidget (line 70) | func (p *PodcastsUI) initListWidget() {
    method initDetailsWidget (line 82) | func (p *PodcastsUI) initDetailsWidget() {
    method updateDetailsWidget (line 90) | func (p *PodcastsUI) updateDetailsWidget() {

FILE: ui/audio_player.go
  type AudioPlayerWidget (line 13) | type AudioPlayerWidget struct
    method InitComponents (line 24) | func (ap *AudioPlayerWidget) InitComponents() {
    method MainUI (line 31) | func (ap *AudioPlayerWidget) MainUI() *ui.Grid {
    method HandleEvent (line 35) | func (ap *AudioPlayerWidget) HandleEvent(e *ui.Event) (Command, error) {
    method Play (line 68) | func (ap *AudioPlayerWidget) Play(playlist []*itunesapi.Episode, index...
    method playAudio (line 108) | func (ap *AudioPlayerWidget) playAudio(e *itunesapi.Episode) {
    method Pause (line 115) | func (ap *AudioPlayerWidget) Pause() {
    method initAudipPositionWidget (line 120) | func (ap *AudioPlayerWidget) initAudipPositionWidget() {
    method initPlayerStatusWidget (line 126) | func (ap *AudioPlayerWidget) initPlayerStatusWidget() {
    method initGrid (line 135) | func (ap *AudioPlayerWidget) initGrid() {

FILE: ui/commands.go
  type Command (line 3) | type Command
  constant Nothing (line 6) | Nothing Command = 0
  constant Exit (line 7) | Exit    Command = 1

FILE: ui/episodes.go
  type EpisodesUI (line 13) | type EpisodesUI struct
    method InitComponents (line 20) | func (e *EpisodesUI) InitComponents() error {
    method MainUI (line 27) | func (e *EpisodesUI) MainUI() *ui.Grid {
    method HandleEvent (line 31) | func (e *EpisodesUI) HandleEvent(event *ui.Event) (Command, error) {
    method Play (line 45) | func (e *EpisodesUI) Play(index int) {
    method initGridWidget (line 49) | func (e *EpisodesUI) initGridWidget() error {
    method initListWidget (line 68) | func (e *EpisodesUI) initListWidget() {
    method initDetailsWidget (line 81) | func (e *EpisodesUI) initDetailsWidget() {
    method updateDetailsWidget (line 89) | func (e *EpisodesUI) updateDetailsWidget() {

FILE: ui/genres.go
  type GenresUI (line 11) | type GenresUI struct
    method InitComponents (line 17) | func (g *GenresUI) InitComponents() error {
    method MainUI (line 25) | func (g *GenresUI) MainUI() *ui.Grid {
    method HandleEvent (line 28) | func (g *GenresUI) HandleEvent(event *ui.Event) (Command, error) {
    method newGenresListWidget (line 48) | func (g *GenresUI) newGenresListWidget() error {
    method newGridWidget (line 64) | func (g *GenresUI) newGridWidget() error {

FILE: ui/page.go
  type Page (line 20) | type Page interface
  function InitUI (line 25) | func InitUI() error {
  function Show (line 33) | func Show(p Page) {
  function show (line 41) | func show(p Page) {
  function RefreshUI (line 46) | func RefreshUI() {
  function GoBack (line 51) | func GoBack() {
  function HandleKeyEvent (line 59) | func HandleKeyEvent(e *ui.Event) (Command, error) {
  function newHelpBarWidget (line 86) | func newHelpBarWidget() *widgets.Paragraph {

FILE: ui/sub_genres.go
  type SubGenresUI (line 11) | type SubGenresUI struct
    method InitComponents (line 17) | func (g *SubGenresUI) InitComponents() error {
    method MainUI (line 25) | func (g *SubGenresUI) MainUI() *ui.Grid {
    method HandleEvent (line 28) | func (g *SubGenresUI) HandleEvent(event *ui.Event) (Command, error) {
    method newGenresListWidget (line 49) | func (g *SubGenresUI) newGenresListWidget() error {
    method newGridWidget (line 65) | func (g *SubGenresUI) newGridWidget() error {
    method refreshComponents (line 84) | func (g *SubGenresUI) refreshComponents() {
Condensed preview — 19 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (41K chars).
[
  {
    "path": ".gitignore",
    "chars": 371,
    "preview": "# 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"
  },
  {
    "path": ".vscode/launch.json",
    "chars": 444,
    "preview": "{\n    // Use IntelliSense to learn about possible attributes.\n    // Hover to view descriptions of existing attributes.\n"
  },
  {
    "path": "README.md",
    "chars": 1865,
    "preview": "<p align=\"center\"><img width=\"200px\" src=\"/resources/img/logo.png\" alt=\"podcast-cli\"/></p>\n\n\n___\n\nTop-like interface for"
  },
  {
    "path": "audio-player/player.go",
    "chars": 3211,
    "preview": "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\"g"
  },
  {
    "path": "config/main.go",
    "chars": 98,
    "preview": "package config\n\nimport (\n\t\"os\"\n\t\"path\"\n)\n\nvar CachePath = path.Join(os.TempDir(), \"podcast-cli/\")\n"
  },
  {
    "path": "go.mod",
    "chars": 250,
    "preview": "module github.com/goulinkh/podcast-cli\n\ngo 1.14\n\nrequire (\n\tgithub.com/PuerkitoBio/goquery v1.5.1\n\tgithub.com/akamensky/"
  },
  {
    "path": "go.sum",
    "chars": 4939,
    "preview": "github.com/PuerkitoBio/goquery v1.5.1 h1:PSPBGne8NIUWw+/7vFBV+kG2J/5MOjbzc7154OaKCSE=\ngithub.com/PuerkitoBio/goquery v1."
  },
  {
    "path": "itunes-api/episodes.go",
    "chars": 395,
    "preview": "package itunesapi\n\ntype Episode struct {\n\tId                     string `json:\"id\"`\n\tArtwork                string `json"
  },
  {
    "path": "itunes-api/genres.go",
    "chars": 1342,
    "preview": "package itunesapi\n\nimport (\n\t\"net/http\"\n\t\"regexp\"\n\n\t\"github.com/PuerkitoBio/goquery\"\n)\n\nconst applePodcastsMainPage = \"h"
  },
  {
    "path": "itunes-api/podcasts.go",
    "chars": 4429,
    "preview": "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 Pod"
  },
  {
    "path": "main.go",
    "chars": 1985,
    "preview": "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 \"gith"
  },
  {
    "path": "rss/parser.go",
    "chars": 1350,
    "preview": "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/i"
  },
  {
    "path": "ui/Podcasts.go",
    "chars": 2794,
    "preview": "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\""
  },
  {
    "path": "ui/audio_player.go",
    "chars": 3751,
    "preview": "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"
  },
  {
    "path": "ui/commands.go",
    "chars": 82,
    "preview": "package ui\n\ntype Command int\n\nconst (\n\tNothing Command = 0\n\tExit    Command = 1\n)\n"
  },
  {
    "path": "ui/episodes.go",
    "chars": 2855,
    "preview": "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\""
  },
  {
    "path": "ui/genres.go",
    "chars": 2178,
    "preview": "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 \"githu"
  },
  {
    "path": "ui/page.go",
    "chars": 2465,
    "preview": "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\t"
  },
  {
    "path": "ui/sub_genres.go",
    "chars": 2165,
    "preview": "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 \"githu"
  }
]

About this extraction

This page contains the full source code of the goulinkh/podcast-cli GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 19 files (36.1 KB), approximately 12.1k tokens, and a symbol index with 74 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!