podcast-go

TUI podcast downloader for Apple Podcasts
Log | Files | Refs | README | LICENSE

commit a8cd27fd24a5276ed9fab3b05d571aba541b97bf
parent 27ab48db56062f29ef98755c7309e04d6618ee2d
Author: Erik Loualiche <eloualic@umn.edu>
Date:   Thu,  5 Feb 2026 20:08:40 -0600

Merge remote-tracking branch 'origin/fyne-gui'

# Conflicts:
#	.gitignore

Diffstat:
M.gitignore | 8++++----
Acmd/gui/main.go | 869+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mgo.mod | 34++++++++++++++++++++++++++++++++--
Mgo.sum | 78++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------
Mjustfile | 17++++++++++++++---
5 files changed, 991 insertions(+), 15 deletions(-)

diff --git a/.gitignore b/.gitignore @@ -1,10 +1,10 @@ # Build output podcastdownload podcast-gui -podcastdownload -# Claude Code local settings -.claude/ +# Claude Code local settings (but keep claude.md for project docs) +.claude/* +!.claude/claude.md + .DS_Store *.tape -podcastdownload diff --git a/cmd/gui/main.go b/cmd/gui/main.go @@ -0,0 +1,869 @@ +package main + +import ( + "crypto/sha1" + "encoding/hex" + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" + "os" + "path/filepath" + "regexp" + "strconv" + "strings" + "sync" + "time" + + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/app" + "fyne.io/fyne/v2/container" + "fyne.io/fyne/v2/dialog" + "fyne.io/fyne/v2/theme" + "fyne.io/fyne/v2/widget" + "github.com/bogem/id3v2" + "github.com/mmcdole/gofeed" +) + +// Data structures (shared with TUI) + +type PodcastInfo struct { + Name string + Artist string + FeedURL string + ArtworkURL string + ID string +} + +type SearchResult struct { + ID string + Name string + Artist string + FeedURL string + ArtworkURL string + Source SearchProvider +} + +type Episode struct { + Index int + Title string + Description string + AudioURL string + PubDate time.Time + Duration string + Selected bool +} + +type iTunesResponse struct { + ResultCount int `json:"resultCount"` + Results []struct { + CollectionID int `json:"collectionId"` + CollectionName string `json:"collectionName"` + ArtistName string `json:"artistName"` + FeedURL string `json:"feedUrl"` + ArtworkURL600 string `json:"artworkUrl600"` + ArtworkURL100 string `json:"artworkUrl100"` + } `json:"results"` +} + +type podcastIndexResponse struct { + Status string `json:"status"` + Feeds []struct { + ID int `json:"id"` + Title string `json:"title"` + Author string `json:"author"` + URL string `json:"url"` + Image string `json:"image"` + Description string `json:"description"` + } `json:"feeds"` + Count int `json:"count"` +} + +type SearchProvider string + +const ( + ProviderApple SearchProvider = "apple" + ProviderPodcastIndex SearchProvider = "podcastindex" +) + +// App holds the application state +type App struct { + fyneApp fyne.App + mainWindow fyne.Window + + // UI components + searchEntry *widget.Entry + searchButton *widget.Button + resultsList *widget.List + episodeList *widget.List + progressBar *widget.ProgressBar + statusLabel *widget.Label + downloadButton *widget.Button + selectAllCheck *widget.Check + backButton *widget.Button + outputDirEntry *widget.Entry + browseButton *widget.Button + + // Containers for switching views + mainContainer *fyne.Container + searchView *fyne.Container + episodeView *fyne.Container + downloadView *fyne.Container + + // Header label for episode view + podcastHeader *widget.Label + + // Data + searchResults []SearchResult + episodes []Episode + podcastInfo PodcastInfo + outputDir string + downloading bool +} + +func main() { + podApp := &App{ + outputDir: ".", + } + podApp.Run() +} + +func (a *App) Run() { + a.fyneApp = app.New() + a.mainWindow = a.fyneApp.NewWindow("Podcast Downloader") + a.mainWindow.Resize(fyne.NewSize(800, 600)) + + a.buildUI() + a.showSearchView() + + a.mainWindow.ShowAndRun() +} + +func (a *App) buildUI() { + // Search view components + a.searchEntry = widget.NewEntry() + a.searchEntry.SetPlaceHolder("Search podcasts or enter Apple Podcast ID...") + a.searchEntry.OnSubmitted = func(_ string) { a.doSearch() } + + a.searchButton = widget.NewButtonWithIcon("Search", theme.SearchIcon(), a.doSearch) + + a.resultsList = widget.NewList( + func() int { return len(a.searchResults) }, + func() fyne.CanvasObject { + return container.NewVBox( + widget.NewLabel("Podcast Name"), + widget.NewLabel("Artist"), + ) + }, + func(id widget.ListItemID, obj fyne.CanvasObject) { + if id >= len(a.searchResults) { + return + } + result := a.searchResults[id] + vbox := obj.(*fyne.Container) + nameLabel := vbox.Objects[0].(*widget.Label) + artistLabel := vbox.Objects[1].(*widget.Label) + nameLabel.SetText(result.Name) + sourceTag := "" + if result.Source == ProviderPodcastIndex { + sourceTag = " [PI]" + } + artistLabel.SetText(result.Artist + sourceTag) + }, + ) + a.resultsList.OnSelected = func(id widget.ListItemID) { + if id < len(a.searchResults) { + a.loadPodcast(a.searchResults[id]) + } + } + + // Output directory selection + a.outputDirEntry = widget.NewEntry() + a.outputDirEntry.SetText(a.outputDir) + a.outputDirEntry.OnChanged = func(s string) { a.outputDir = s } + + a.browseButton = widget.NewButtonWithIcon("Browse", theme.FolderOpenIcon(), func() { + dialog.ShowFolderOpen(func(uri fyne.ListableURI, err error) { + if err != nil || uri == nil { + return + } + a.outputDir = uri.Path() + a.outputDirEntry.SetText(a.outputDir) + }, a.mainWindow) + }) + + outputRow := container.NewBorder(nil, nil, widget.NewLabel("Output:"), a.browseButton, a.outputDirEntry) + + searchRow := container.NewBorder(nil, nil, nil, a.searchButton, a.searchEntry) + + a.searchView = container.NewBorder( + container.NewVBox( + widget.NewLabel("Podcast Downloader"), + searchRow, + outputRow, + widget.NewSeparator(), + ), + nil, nil, nil, + a.resultsList, + ) + + // Episode view components + a.backButton = widget.NewButtonWithIcon("Back", theme.NavigateBackIcon(), func() { + a.showSearchView() + }) + + a.selectAllCheck = widget.NewCheck("Select All", func(checked bool) { + for i := range a.episodes { + a.episodes[i].Selected = checked + } + a.episodeList.Refresh() + a.updateDownloadButton() + }) + + a.episodeList = widget.NewList( + func() int { return len(a.episodes) }, + func() fyne.CanvasObject { + check := widget.NewCheck("", nil) + titleLabel := widget.NewLabel("Episode Title") + dateLabel := widget.NewLabel("2024-01-01") + durationLabel := widget.NewLabel("00:00") + return container.NewBorder( + nil, nil, + check, + container.NewHBox(dateLabel, durationLabel), + titleLabel, + ) + }, + func(id widget.ListItemID, obj fyne.CanvasObject) { + if id >= len(a.episodes) { + return + } + ep := a.episodes[id] + border := obj.(*fyne.Container) + check := border.Objects[1].(*widget.Check) + titleLabel := border.Objects[0].(*widget.Label) + rightBox := border.Objects[2].(*fyne.Container) + dateLabel := rightBox.Objects[0].(*widget.Label) + durationLabel := rightBox.Objects[1].(*widget.Label) + + check.SetChecked(ep.Selected) + check.OnChanged = func(checked bool) { + a.episodes[id].Selected = checked + a.updateDownloadButton() + } + + title := ep.Title + if len(title) > 60 { + title = title[:57] + "..." + } + titleLabel.SetText(fmt.Sprintf("[%d] %s", ep.Index, title)) + + if !ep.PubDate.IsZero() { + dateLabel.SetText(ep.PubDate.Format("2006-01-02")) + } else { + dateLabel.SetText("") + } + durationLabel.SetText(ep.Duration) + }, + ) + + a.downloadButton = widget.NewButtonWithIcon("Download Selected", theme.DownloadIcon(), a.startDownload) + a.downloadButton.Importance = widget.HighImportance + + a.podcastHeader = widget.NewLabel("") + a.podcastHeader.TextStyle = fyne.TextStyle{Bold: true} + + a.episodeView = container.NewBorder( + container.NewVBox( + container.NewHBox(a.backButton, a.podcastHeader), + widget.NewSeparator(), + a.selectAllCheck, + ), + container.NewVBox( + widget.NewSeparator(), + container.NewHBox(a.downloadButton), + ), + nil, nil, + a.episodeList, + ) + + // Download view components + a.progressBar = widget.NewProgressBar() + a.statusLabel = widget.NewLabel("Ready") + + a.downloadView = container.NewVBox( + widget.NewLabel("Downloading..."), + a.progressBar, + a.statusLabel, + ) + + // Main container with all views + a.mainContainer = container.NewStack(a.searchView, a.episodeView, a.downloadView) + a.mainWindow.SetContent(a.mainContainer) +} + +func (a *App) showSearchView() { + a.searchView.Show() + a.episodeView.Hide() + a.downloadView.Hide() +} + +func (a *App) showEpisodeView() { + a.searchView.Hide() + a.episodeView.Show() + a.downloadView.Hide() + + // Update header + a.podcastHeader.SetText(fmt.Sprintf("%s - %d episodes", a.podcastInfo.Name, len(a.episodes))) +} + +func (a *App) showDownloadView() { + a.searchView.Hide() + a.episodeView.Hide() + a.downloadView.Show() +} + +func (a *App) updateDownloadButton() { + count := 0 + for _, ep := range a.episodes { + if ep.Selected { + count++ + } + } + if count > 0 { + a.downloadButton.SetText(fmt.Sprintf("Download %d Episode(s)", count)) + a.downloadButton.Enable() + } else { + a.downloadButton.SetText("Download Selected") + a.downloadButton.Disable() + } +} + +func (a *App) doSearch() { + query := strings.TrimSpace(a.searchEntry.Text) + if query == "" { + return + } + + a.searchButton.Disable() + a.statusLabel.SetText("Searching...") + + go func() { + var results []SearchResult + var err error + + if isNumeric(query) { + // Direct podcast ID lookup + info, episodes, loadErr := loadPodcastByID(query) + if loadErr != nil { + fyne.Do(func() { + a.showError("Failed to load podcast", loadErr) + a.searchButton.Enable() + }) + return + } + fyne.Do(func() { + a.podcastInfo = info + a.episodes = episodes + a.searchButton.Enable() + a.episodeList.Refresh() + a.updateDownloadButton() + a.showEpisodeView() + }) + return + } + + // Search both sources if credentials available + if hasPodcastIndexCredentials() { + results, err = searchBoth(query) + } else { + results, err = searchAppleResults(query) + } + + if err != nil { + fyne.Do(func() { + a.showError("Search failed", err) + a.searchButton.Enable() + }) + return + } + + fyne.Do(func() { + a.searchResults = results + a.resultsList.Refresh() + a.searchButton.Enable() + a.statusLabel.SetText(fmt.Sprintf("Found %d podcasts", len(results))) + }) + }() +} + +func (a *App) loadPodcast(result SearchResult) { + a.statusLabel.SetText(fmt.Sprintf("Loading %s...", result.Name)) + + go func() { + var info PodcastInfo + var episodes []Episode + var err error + + if result.Source == ProviderPodcastIndex { + info, episodes, err = loadPodcastFromFeed(result.FeedURL, result.Name, result.Artist, result.ArtworkURL) + } else { + info, episodes, err = loadPodcastByID(result.ID) + } + + if err != nil { + fyne.Do(func() { + a.showError("Failed to load podcast", err) + }) + return + } + + fyne.Do(func() { + a.podcastInfo = info + a.episodes = episodes + a.selectAllCheck.SetChecked(false) + a.episodeList.Refresh() + a.updateDownloadButton() + a.showEpisodeView() + a.statusLabel.SetText("Ready") + }) + }() +} + +func (a *App) startDownload() { + if a.downloading { + return + } + + selected := a.getSelectedEpisodes() + if len(selected) == 0 { + return + } + + a.downloading = true + a.showDownloadView() + + // Create output directory + podcastFolder := sanitizeFilename(a.podcastInfo.Name) + outputDir := filepath.Join(a.outputDir, podcastFolder) + os.MkdirAll(outputDir, 0755) + + go func() { + defer func() { a.downloading = false }() + + for i, ep := range selected { + filename := fmt.Sprintf("%03d - %s.mp3", ep.Index, sanitizeFilename(ep.Title)) + filePath := filepath.Join(outputDir, filename) + + fyne.Do(func() { + a.statusLabel.SetText(fmt.Sprintf("Downloading %d/%d: %s", i+1, len(selected), ep.Title)) + a.progressBar.SetValue(0) + }) + + err := downloadFileWithProgress(filePath, ep.AudioURL, func(progress float64) { + fyne.Do(func() { + a.progressBar.SetValue(progress) + }) + }) + + if err != nil { + fyne.Do(func() { + a.statusLabel.SetText(fmt.Sprintf("Error: %v", err)) + }) + continue + } + + // Add ID3 tags + addID3Tags(filePath, ep, a.podcastInfo) + } + + fyne.Do(func() { + a.statusLabel.SetText(fmt.Sprintf("Downloaded %d episodes to %s", len(selected), outputDir)) + a.progressBar.SetValue(1) + + // Show completion dialog + dialog.ShowInformation("Download Complete", + fmt.Sprintf("Successfully downloaded %d episode(s) to:\n%s", len(selected), outputDir), + a.mainWindow) + + a.showEpisodeView() + }) + }() +} + +func (a *App) getSelectedEpisodes() []Episode { + var selected []Episode + for _, ep := range a.episodes { + if ep.Selected { + selected = append(selected, ep) + } + } + return selected +} + +func (a *App) showError(title string, err error) { + dialog.ShowError(fmt.Errorf("%s: %v", title, err), a.mainWindow) + a.statusLabel.SetText("Error: " + err.Error()) +} + +// Core functions (reused from TUI) + +func isNumeric(s string) bool { + for _, c := range s { + if c < '0' || c > '9' { + return false + } + } + return len(s) > 0 +} + +func hasPodcastIndexCredentials() bool { + apiKey := strings.TrimSpace(os.Getenv("PODCASTINDEX_API_KEY")) + apiSecret := strings.TrimSpace(os.Getenv("PODCASTINDEX_API_SECRET")) + return apiKey != "" && apiSecret != "" +} + +func sanitizeFilename(name string) string { + re := regexp.MustCompile(`[<>:"/\\|?*]`) + name = re.ReplaceAllString(name, "") + name = strings.TrimSpace(name) + if len(name) > 100 { + name = name[:100] + } + if name == "" { + return "episode" + } + return name +} + +func loadPodcastByID(podcastID string) (PodcastInfo, []Episode, error) { + podcastID = strings.TrimPrefix(strings.ToLower(podcastID), "id") + + url := fmt.Sprintf("https://itunes.apple.com/lookup?id=%s&entity=podcast", podcastID) + resp, err := http.Get(url) + if err != nil { + return PodcastInfo{}, nil, fmt.Errorf("failed to lookup podcast: %w", err) + } + defer resp.Body.Close() + + var result iTunesResponse + if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { + return PodcastInfo{}, nil, fmt.Errorf("failed to parse response: %w", err) + } + + if result.ResultCount == 0 { + return PodcastInfo{}, nil, fmt.Errorf("no podcast found with ID: %s", podcastID) + } + + r := result.Results[0] + info := PodcastInfo{ + Name: r.CollectionName, + Artist: r.ArtistName, + FeedURL: r.FeedURL, + ArtworkURL: r.ArtworkURL600, + ID: podcastID, + } + + if info.ArtworkURL == "" { + info.ArtworkURL = r.ArtworkURL100 + } + + if info.FeedURL == "" { + return PodcastInfo{}, nil, fmt.Errorf("no RSS feed URL found for this podcast") + } + + episodes, err := parseRSSFeed(info.FeedURL) + if err != nil { + return PodcastInfo{}, nil, err + } + + return info, episodes, nil +} + +func loadPodcastFromFeed(feedURL, name, artist, artworkURL string) (PodcastInfo, []Episode, error) { + info := PodcastInfo{ + Name: name, + Artist: artist, + FeedURL: feedURL, + ArtworkURL: artworkURL, + } + + fp := gofeed.NewParser() + feed, err := fp.ParseURL(feedURL) + if err != nil { + return PodcastInfo{}, nil, fmt.Errorf("failed to parse RSS feed: %w", err) + } + + if info.Name == "" && feed.Title != "" { + info.Name = feed.Title + } + if info.Artist == "" && feed.Author != nil { + info.Artist = feed.Author.Name + } + if info.ArtworkURL == "" && feed.Image != nil { + info.ArtworkURL = feed.Image.URL + } + + episodes, err := parseRSSFeedItems(feed.Items) + if err != nil { + return PodcastInfo{}, nil, err + } + + return info, episodes, nil +} + +func parseRSSFeed(feedURL string) ([]Episode, error) { + fp := gofeed.NewParser() + feed, err := fp.ParseURL(feedURL) + if err != nil { + return nil, fmt.Errorf("failed to parse RSS feed: %w", err) + } + return parseRSSFeedItems(feed.Items) +} + +func parseRSSFeedItems(items []*gofeed.Item) ([]Episode, error) { + var episodes []Episode + for i, item := range items { + audioURL := "" + for _, enc := range item.Enclosures { + if strings.Contains(enc.Type, "audio") || strings.HasSuffix(enc.URL, ".mp3") { + audioURL = enc.URL + break + } + } + if audioURL == "" { + continue + } + + var pubDate time.Time + if item.PublishedParsed != nil { + pubDate = *item.PublishedParsed + } + + duration := "" + if item.ITunesExt != nil { + duration = item.ITunesExt.Duration + } + + episodes = append(episodes, Episode{ + Index: i + 1, + Title: item.Title, + Description: item.Description, + AudioURL: audioURL, + PubDate: pubDate, + Duration: duration, + }) + } + + if len(episodes) == 0 { + return nil, fmt.Errorf("no downloadable episodes found") + } + + return episodes, nil +} + +func searchAppleResults(query string) ([]SearchResult, error) { + encodedQuery := strings.ReplaceAll(query, " ", "+") + apiURL := fmt.Sprintf("https://itunes.apple.com/search?term=%s&media=podcast&limit=25", encodedQuery) + + resp, err := http.Get(apiURL) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + var result iTunesResponse + if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { + return nil, err + } + + var results []SearchResult + for _, r := range result.Results { + if r.FeedURL == "" { + continue + } + results = append(results, SearchResult{ + ID: strconv.Itoa(r.CollectionID), + Name: r.CollectionName, + Artist: r.ArtistName, + FeedURL: r.FeedURL, + ArtworkURL: r.ArtworkURL600, + Source: ProviderApple, + }) + } + return results, nil +} + +func searchPodcastIndexResults(query string) ([]SearchResult, error) { + apiKey := strings.TrimSpace(os.Getenv("PODCASTINDEX_API_KEY")) + apiSecret := strings.TrimSpace(os.Getenv("PODCASTINDEX_API_SECRET")) + + apiHeaderTime := strconv.FormatInt(time.Now().Unix(), 10) + hashInput := apiKey + apiSecret + apiHeaderTime + h := sha1.New() + h.Write([]byte(hashInput)) + authHash := hex.EncodeToString(h.Sum(nil)) + + encodedQuery := url.QueryEscape(query) + apiURL := fmt.Sprintf("https://api.podcastindex.org/api/1.0/search/byterm?q=%s&max=25", encodedQuery) + + req, err := http.NewRequest("GET", apiURL, nil) + if err != nil { + return nil, err + } + + req.Header.Set("User-Agent", "PodcastDownload/1.0") + req.Header.Set("X-Auth-Key", apiKey) + req.Header.Set("X-Auth-Date", apiHeaderTime) + req.Header.Set("Authorization", authHash) + + client := &http.Client{Timeout: 30 * time.Second} + resp, err := client.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + body, _ := io.ReadAll(resp.Body) + return nil, fmt.Errorf("API error (%d): %s", resp.StatusCode, string(body)) + } + + var result podcastIndexResponse + if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { + return nil, err + } + + var results []SearchResult + for _, feed := range result.Feeds { + if feed.URL == "" { + continue + } + results = append(results, SearchResult{ + ID: strconv.Itoa(feed.ID), + Name: feed.Title, + Artist: feed.Author, + FeedURL: feed.URL, + ArtworkURL: feed.Image, + Source: ProviderPodcastIndex, + }) + } + return results, nil +} + +func searchBoth(query string) ([]SearchResult, error) { + var wg sync.WaitGroup + var appleResults, piResults []SearchResult + var appleErr, piErr error + + wg.Add(2) + + go func() { + defer wg.Done() + appleResults, appleErr = searchAppleResults(query) + }() + + go func() { + defer wg.Done() + piResults, piErr = searchPodcastIndexResults(query) + }() + + wg.Wait() + + if appleErr != nil && piErr != nil { + return nil, fmt.Errorf("search failed: Apple: %v, Podcast Index: %v", appleErr, piErr) + } + + var combined []SearchResult + seenFeedURLs := make(map[string]bool) + + if appleErr == nil { + for _, r := range appleResults { + normalizedURL := strings.ToLower(strings.TrimSuffix(r.FeedURL, "/")) + if !seenFeedURLs[normalizedURL] { + seenFeedURLs[normalizedURL] = true + combined = append(combined, r) + } + } + } + if piErr == nil { + for _, r := range piResults { + normalizedURL := strings.ToLower(strings.TrimSuffix(r.FeedURL, "/")) + if !seenFeedURLs[normalizedURL] { + seenFeedURLs[normalizedURL] = true + combined = append(combined, r) + } + } + } + + return combined, nil +} + +func downloadFileWithProgress(filepath string, fileURL string, progressCallback func(float64)) error { + // Check if already exists + if _, err := os.Stat(filepath); err == nil { + progressCallback(1.0) + return nil + } + + resp, err := http.Get(fileURL) + if err != nil { + return err + } + defer resp.Body.Close() + + out, err := os.Create(filepath) + if err != nil { + return err + } + defer out.Close() + + totalSize := resp.ContentLength + downloaded := int64(0) + lastPercent := float64(0) + + buf := make([]byte, 32*1024) + for { + n, err := resp.Body.Read(buf) + if n > 0 { + out.Write(buf[:n]) + downloaded += int64(n) + if totalSize > 0 { + percent := float64(downloaded) / float64(totalSize) + if percent-lastPercent >= 0.01 || percent >= 1.0 { + lastPercent = percent + progressCallback(percent) + } + } + } + if err == io.EOF { + break + } + if err != nil { + return err + } + } + + return nil +} + +func addID3Tags(filepath string, ep Episode, info PodcastInfo) error { + tag, err := id3v2.Open(filepath, id3v2.Options{Parse: true}) + if err != nil { + tag = id3v2.NewEmptyTag() + } + defer tag.Close() + + tag.SetTitle(ep.Title) + tag.SetArtist(info.Artist) + tag.SetAlbum(info.Name) + + trackFrame := id3v2.TextFrame{ + Encoding: id3v2.EncodingUTF8, + Text: strconv.Itoa(ep.Index), + } + tag.AddFrame(tag.CommonID("Track number/Position in set"), trackFrame) + + return tag.Save() +} diff --git a/go.mod b/go.mod @@ -3,6 +3,7 @@ module podcastdownload go 1.25.5 require ( + fyne.io/fyne/v2 v2.7.2 github.com/bogem/id3v2 v1.2.0 github.com/charmbracelet/bubbles v0.21.0 github.com/charmbracelet/bubbletea v1.3.10 @@ -11,6 +12,8 @@ require ( ) require ( + fyne.io/systray v1.12.0 // indirect + github.com/BurntSushi/toml v1.5.0 // indirect github.com/PuerkitoBio/goquery v1.8.0 // indirect github.com/andybalholm/cascadia v1.3.1 // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect @@ -19,8 +22,25 @@ require ( github.com/charmbracelet/x/ansi v0.10.1 // indirect github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd // indirect github.com/charmbracelet/x/term v0.2.1 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect + github.com/fredbi/uri v1.1.1 // indirect + github.com/fsnotify/fsnotify v1.9.0 // indirect + github.com/fyne-io/gl-js v0.2.0 // indirect + github.com/fyne-io/glfw-js v0.3.0 // indirect + github.com/fyne-io/image v0.1.1 // indirect + github.com/fyne-io/oksvg v0.2.0 // indirect + github.com/go-gl/gl v0.0.0-20231021071112-07e5d0ea2e71 // indirect + github.com/go-gl/glfw/v3.3/glfw v0.0.0-20240506104042-037f3cc74f2a // indirect + github.com/go-text/render v0.2.0 // indirect + github.com/go-text/typesetting v0.2.1 // indirect + github.com/godbus/dbus/v5 v5.1.0 // indirect + github.com/hack-pad/go-indexeddb v0.3.2 // indirect + github.com/hack-pad/safejs v0.1.0 // indirect + github.com/jeandeaual/go-locale v0.0.0-20250612000132-0ef82f21eade // indirect github.com/json-iterator/go v1.1.12 // indirect + github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25 // indirect + github.com/kr/text v0.2.0 // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-localereader v0.0.1 // indirect @@ -31,9 +51,19 @@ require ( github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect github.com/muesli/cancelreader v0.2.2 // indirect github.com/muesli/termenv v0.16.0 // indirect + github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 // indirect + github.com/nicksnyder/go-i18n/v2 v2.5.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect github.com/rivo/uniseg v0.4.7 // indirect + github.com/rymdport/portal v0.4.2 // indirect + github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c // indirect + github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef // indirect + github.com/stretchr/testify v1.11.1 // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect - golang.org/x/net v0.4.0 // indirect + github.com/yuin/goldmark v1.7.8 // indirect + golang.org/x/image v0.24.0 // indirect + golang.org/x/net v0.35.0 // indirect golang.org/x/sys v0.36.0 // indirect - golang.org/x/text v0.5.0 // indirect + golang.org/x/text v0.22.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum @@ -1,3 +1,9 @@ +fyne.io/fyne/v2 v2.7.2 h1:XiNpWkn0PzX43ZCjbb0QYGg1RCxVbugwfVgikWZBCMw= +fyne.io/fyne/v2 v2.7.2/go.mod h1:PXbqY3mQmJV3J1NRUR2VbVgUUx3vgvhuFJxyjRK/4Ug= +fyne.io/systray v1.12.0 h1:CA1Kk0e2zwFlxtc02L3QFSiIbxJ/P0n582YrZHT7aTM= +fyne.io/systray v1.12.0/go.mod h1:RVwqP9nYMo7h5zViCBHri2FgjXF7H2cub7MAq4NSoLs= +github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg= +github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/PuerkitoBio/goquery v1.8.0 h1:PJTF7AmFCFKk1N6V6jmKfrNH9tV5pNE6lZMkG0gta/U= github.com/PuerkitoBio/goquery v1.8.0/go.mod h1:ypIiRMtY7COPGk+I/YbZLbxsxn9g5ejnI2HSMtkjZvI= github.com/andybalholm/cascadia v1.3.1 h1:nhxRkql1kdYCc8Snf7D5/D3spOX+dBgjA6u8x004T2c= @@ -22,14 +28,53 @@ github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd h1:vy0G github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs= github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ= github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= +github.com/felixge/fgprof v0.9.3 h1:VvyZxILNuCiUCSXtPtYmmtGvb65nqXh2QFWc0Wpf2/g= +github.com/felixge/fgprof v0.9.3/go.mod h1:RdbpDgzqYVh/T9fPELJyV7EYJuHB55UTEULNun8eiPw= +github.com/fredbi/uri v1.1.1 h1:xZHJC08GZNIUhbP5ImTHnt5Ya0T8FI2VAwI/37kh2Ko= +github.com/fredbi/uri v1.1.1/go.mod h1:4+DZQ5zBjEwQCDmXW5JdIjz0PUA+yJbvtBv+u+adr5o= +github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= +github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= +github.com/fyne-io/gl-js v0.2.0 h1:+EXMLVEa18EfkXBVKhifYB6OGs3HwKO3lUElA0LlAjs= +github.com/fyne-io/gl-js v0.2.0/go.mod h1:ZcepK8vmOYLu96JoxbCKJy2ybr+g1pTnaBDdl7c3ajI= +github.com/fyne-io/glfw-js v0.3.0 h1:d8k2+Y7l+zy2pc7wlGRyPfTgZoqDf3AI4G+2zOWhWUk= +github.com/fyne-io/glfw-js v0.3.0/go.mod h1:Ri6te7rdZtBgBpxLW19uBpp3Dl6K9K/bRaYdJ22G8Jk= +github.com/fyne-io/image v0.1.1 h1:WH0z4H7qfvNUw5l4p3bC1q70sa5+YWVt6HCj7y4VNyA= +github.com/fyne-io/image v0.1.1/go.mod h1:xrfYBh6yspc+KjkgdZU/ifUC9sPA5Iv7WYUBzQKK7JM= +github.com/fyne-io/oksvg v0.2.0 h1:mxcGU2dx6nwjJsSA9PCYZDuoAcsZ/OuJlvg/Q9Njfo8= +github.com/fyne-io/oksvg v0.2.0/go.mod h1:dJ9oEkPiWhnTFNCmRgEze+YNprJF7YRbpjgpWS4kzoI= +github.com/go-gl/gl v0.0.0-20231021071112-07e5d0ea2e71 h1:5BVwOaUSBTlVZowGO6VZGw2H/zl9nrd3eCZfYV+NfQA= +github.com/go-gl/gl v0.0.0-20231021071112-07e5d0ea2e71/go.mod h1:9YTyiznxEY1fVinfM7RvRcjRHbw2xLBJ3AAGIT0I4Nw= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20240506104042-037f3cc74f2a h1:vxnBhFDDT+xzxf1jTJKMKZw3H0swfWk9RpWbBbDK5+0= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20240506104042-037f3cc74f2a/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-text/render v0.2.0 h1:LBYoTmp5jYiJ4NPqDc2pz17MLmA3wHw1dZSVGcOdeAc= +github.com/go-text/render v0.2.0/go.mod h1:CkiqfukRGKJA5vZZISkjSYrcdtgKQWRa2HIzvwNN5SU= +github.com/go-text/typesetting v0.2.1 h1:x0jMOGyO3d1qFAPI0j4GSsh7M0Q3Ypjzr4+CEVg82V8= +github.com/go-text/typesetting v0.2.1/go.mod h1:mTOxEwasOFpAMBjEQDhdWRckoLLeI/+qrQeBCTGEt6M= +github.com/go-text/typesetting-utils v0.0.0-20241103174707-87a29e9e6066 h1:qCuYC+94v2xrb1PoS4NIDe7DGYtLnU2wWiQe9a1B1c0= +github.com/go-text/typesetting-utils v0.0.0-20241103174707-87a29e9e6066/go.mod h1:DDxDdQEnB70R8owOx3LVpEFvpMK9eeH1o2r0yZhFI9o= +github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= +github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/pprof v0.0.0-20211214055906-6f57359322fd h1:1FjCyPC+syAzJ5/2S8fqdZK1R22vvA0J7JZKcuOIQ7Y= +github.com/google/pprof v0.0.0-20211214055906-6f57359322fd/go.mod h1:KgnwoLYCZ8IQu3XUZ8Nc/bM9CCZFOyjUNOSygVozoDg= +github.com/hack-pad/go-indexeddb v0.3.2 h1:DTqeJJYc1usa45Q5r52t01KhvlSN02+Oq+tQbSBI91A= +github.com/hack-pad/go-indexeddb v0.3.2/go.mod h1:QvfTevpDVlkfomY498LhstjwbPW6QC4VC/lxYb0Kom0= +github.com/hack-pad/safejs v0.1.0 h1:qPS6vjreAqh2amUqj4WNG1zIw7qlRQJ9K10eDKMCnE8= +github.com/hack-pad/safejs v0.1.0/go.mod h1:HdS+bKF1NrE72VoXZeWzxFOVQVUSqZJAG0xNCnb+Tio= +github.com/jeandeaual/go-locale v0.0.0-20250612000132-0ef82f21eade h1:FmusiCI1wHw+XQbvL9M+1r/C3SPqKrmBaIOYwVfQoDE= +github.com/jeandeaual/go-locale v0.0.0-20250612000132-0ef82f21eade/go.mod h1:ZDXo8KHryOWSIqnsb/CiDq7hQUYryCgdVnxbj8tDG7o= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25 h1:YLvr1eE6cdCqjOe972w/cYF+FjW34v27+9Vo5106B4M= +github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25/go.mod h1:kLgvv7o6UM+0QSf0QjAse3wReFDsb9qbZJdfexWlrQw= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= @@ -53,22 +98,40 @@ github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELU github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc= github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk= +github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ= +github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8= +github.com/nicksnyder/go-i18n/v2 v2.5.1 h1:IxtPxYsR9Gp60cGXjfuR/llTqV8aYMsC472zD0D1vHk= +github.com/nicksnyder/go-i18n/v2 v2.5.1/go.mod h1:DrhgsSDZxoAfvVrBVLXoxZn/pN5TXqaDbq7ju94viiQ= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= +github.com/pkg/profile v1.7.0 h1:hnbDkaNWPCLMO9wGLdBFTIZvzDrDfBM2072E1S9gJkA= +github.com/pkg/profile v1.7.0/go.mod h1:8Uer0jas47ZQMJ7VD+OHknK4YDY07LPUC6dEvqDjvNo= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/rymdport/portal v0.4.2 h1:7jKRSemwlTyVHHrTGgQg7gmNPJs88xkbKcIL3NlcmSU= +github.com/rymdport/portal v0.4.2/go.mod h1:kFF4jslnJ8pD5uCi17brj/ODlfIidOxlgUDTO5ncnC4= +github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c h1:km8GpoQut05eY3GiYWEedbTT0qnSxrCjsVbb7yKY1KE= +github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c/go.mod h1:cNQ3dwVJtS5Hmnjxy6AgTPd0Inb3pW05ftPSX7NZO7Q= +github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef h1:Ch6Q+AZUxDBCVqdkI8FSpFyZDtCVBc2VmejdNrm5rRQ= +github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef/go.mod h1:nXTWP6+gD5+LUJ8krVhhoeHjvHTutPxMYl5SvkcnJNE= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= -github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= +github.com/yuin/goldmark v1.7.8 h1:iERMLn0/QJeHFhxSt3p6PeN9mGnvIKSpG9YYorDMnic= +github.com/yuin/goldmark v1.7.8/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E= golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561 h1:MDc5xs78ZrZr3HMQugiXOAkSZtfTpbJLDr/lwfgO53E= golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE= +golang.org/x/image v0.24.0 h1:AN7zRgVsbvmTfNyqIbbOraYL8mSwcKncEj8ofjgzcMQ= +golang.org/x/image v0.24.0/go.mod h1:4b/ITuLfqYq1hqZcjofwctIhi7sZh2WaCjvsBNjjya8= golang.org/x/net v0.0.0-20210916014120-12bc252f5db8/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.4.0 h1:Q5QPcMlvfxFTAPV0+07Xz/MpK9NTXu2VDUuy0FeMfaU= -golang.org/x/net v0.4.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE= +golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8= +golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -78,8 +141,11 @@ golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.5.0 h1:OLmvp0KP+FVG99Ct/qFiL/Fhk4zp4QQnZ7b2U+5piUM= -golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= +golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/justfile b/justfile @@ -1,11 +1,22 @@ -# Build the podcast downloader +# Build the TUI podcast downloader build: go build -o podcastdownload main.go +# Build the GUI podcast downloader +build-gui: + go build -o podcast-gui ./cmd/gui/ + +# Build both versions +build-all: build build-gui + # Remove build artifacts clean: - rm -f podcastdownload + rm -f podcastdownload podcast-gui -# Build and run the application +# Build and run the TUI application run: build ./podcastdownload + +# Build and run the GUI application +run-gui: build-gui + ./podcast-gui