diff --git a/README.md b/README.md index 11ece2f..8d6ce3a 100644 --- a/README.md +++ b/README.md @@ -53,16 +53,16 @@ To install Audiobook Builder (`abb_ia`) on your system, follow these steps: 2. Move the downloaded binary file to a directory in your system's `PATH`. 3. Open a terminal and navigate to the directory where the binary file is located. 4. Run the binary file by executing the command `./abb_ia`. The TUI interface will appear. -5. Follow the instructions on the application interface to do a search, create an audiobook, and upload it to the [Audiobookshelf server](https://www.audiobookshelf.org) if necessary. You can try searching for: +5. Follow the instructions on the application interface to do a search, create an audiobook, and upload it to the [Audiobookshelf server](https://www.audiobookshelf.org) if necessary.
+ You can try searching for: - **Old Time Radio Researchers Group: Single Episodes** - - **BBC Radio 4: Radio Drama** (make sure to check if the show is copyrighted). +- **CBS Radio: Radio Mystery Theater** - **Boxcars711:*** - **Greg Lauer:*** - **Relic Radio:*** - **Radio Memories Network:*** -- **CBC: Radio Mystery Theater** 6. Enjoy listening to an audiobook on your favorite device. @@ -92,7 +92,7 @@ If you prefer to build the program from source, follow these instructions: - On some systems, you may not see a cursor in the input fields. This is because the color of the cursor in your terminal application settings is the same as the background color of the input field. To solve this issue, you can adjust the settings of your terminal program. You can either change the cursor color (so that it is different from the color of the input fields) or make the cursor blink. - On some Windows systems, the Audiobook builder has a problem launching ffmpeg (there is an issue with the input file path). If you encounter this problem, please enable DEBUG mode in the application settings, replicate the error, and file a GitHub Bug report by attaching the application log file. - Sometimes, when the application crashes, the terminal window may be filled with random characters and you won't see a cursor anymore. This happens because of a problem with the TUI framework that was used to create the application. To solve this issue, you can either reopen the terminal window or try running the `reset` command. -- Downloading audiobooks using `abb_id` is incredibly easy and fast. This means that over time, you might collect hundreds of audiobooks with thousands of hours of content. However, this can be a problem because it would be practically impossible to listen to all of them in your lifetime. :-) +- Downloading audiobooks using `abb_id` is incredibly easy and fast. This means that over time, you might collect hundreds of audiobooks with thousands of hours of content. However, this can be a problem because it would be practically impossible to listen to all of them in your lifetime. Congratulations!!! You are a data hoarder now! :-) ## Reporting Bugs through GitHub Issues @@ -145,6 +145,6 @@ Since the copyrights for the majority of old-time radio shows have expired and m ## Todo - The Text to Speech (**TTS**) version of Audiobook Builder is coming soon. It will allow you to create audiobooks from .epub, fb2, and other formats of electronic books. -## Join us on Discord +## Join me on Discord https://discord.gg/ntYyJ7xfzX diff --git a/internal/config/config.go b/internal/config/config.go index ebce678..e1461b5 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -28,6 +28,8 @@ var ( type Config struct { DefaultAuthor string `yaml:"DefaultAuthor"` DefaultTitle string `yaml:"DefaultTitle"` + SortBy string `yaml:"SortBy"` + SortOrder string `yaml:"SortOrder"` RowsPerPage int `yaml:"RowsPerPage"` LogFileName string `yaml:"LogFileName"` OutputDir string `yaml:"Outputdir"` @@ -80,6 +82,8 @@ func Load() { config.SaveMock = false config.DefaultAuthor = "Old Time Radio Researchers Group" config.DefaultTitle = "Single Episodes" + config.SortBy = "Date" + config.SortOrder = "Descending" config.ConcurrentDownloaders = 5 config.ConcurrentEncoders = 5 config.ReEncodeFiles = true @@ -226,6 +230,30 @@ func (c *Config) GetDefaultTitle() string { return c.DefaultTitle } +func (c *Config) SetSortBy(s string) { + c.SortBy = s +} + +func (c *Config) GetSortBy() string { + return c.SortBy +} + +func (c *Config) GetSortByOptions() []string { + return []string{"Creator", "Title", "Date", "Size"} +} + +func (c *Config) SetSortOrder(s string) { + c.SortOrder = s +} + +func (c *Config) GetSortOrder() string { + return c.SortOrder +} + +func (c *Config) GetSortOrderOptions() []string { + return []string{"Ascending", "Descending"} +} + func (c *Config) SetConcurrentDownloaders(n int) { c.ConcurrentDownloaders = n } diff --git a/internal/controller/config_controller.go b/internal/controller/config_controller.go index dc28e50..3518602 100644 --- a/internal/controller/config_controller.go +++ b/internal/controller/config_controller.go @@ -5,6 +5,7 @@ import ( "abb_ia/internal/dto" "abb_ia/internal/logger" "abb_ia/internal/mq" + "abb_ia/internal/utils" ) type ConfigController struct { @@ -41,6 +42,9 @@ func (c *ConfigController) updateConfig(cmd *dto.SaveConfigCommand) { config.SaveConfig(&cmd.Config) + logger.SetLogLevel(logger.LogLevelType(utils.GetIndex(logger.LogLeves(), cmd.Config.GetLogLevel()) + 1)) + c.mq.SendMessage(mq.ConfigController, mq.SearchPage, &dto.UpdateSearchConfigCommand{Config: cmd.Config}, true) + c.mq.SendMessage(mq.ConfigController, mq.Footer, &dto.SetBusyIndicator{Busy: false}, false) c.mq.SendMessage(mq.ConfigController, mq.Footer, &dto.UpdateStatus{Message: ""}, false) } diff --git a/internal/controller/search_controller.go b/internal/controller/search_controller.go index dfd5c1c..dbccd82 100644 --- a/internal/controller/search_controller.go +++ b/internal/controller/search_controller.go @@ -121,7 +121,7 @@ func (c *SearchController) fetchDetails(resp *ia_client.SearchResponse) (int, er if d != nil { item.Server = d.Server item.Dir = d.Dir - if len(doc.Creator) > 0 && d.Metadata.Creator[0] != "" { + if len(doc.Creator) > 0 && doc.Creator[0] != "" { item.Creator = doc.Creator[0] } else if len(d.Metadata.Creator) > 0 && d.Metadata.Creator[0] != "" { item.Creator = d.Metadata.Creator[0] diff --git a/internal/dto/search.go b/internal/dto/search.go index f4bd326..7aec74b 100644 --- a/internal/dto/search.go +++ b/internal/dto/search.go @@ -1,6 +1,9 @@ package dto -import "fmt" +import ( + "abb_ia/internal/config" + "fmt" +) type SearchCondition struct { Author string @@ -17,6 +20,14 @@ func (c *SearchCommand) String() string { return fmt.Sprintf("SearchCommand: Author: %s, Title: %s", c.Condition.Author, c.Condition.Title) } +type UpdateSearchConfigCommand struct { + Config config.Config +} + +func (c *UpdateSearchConfigCommand) String() string { + return fmt.Sprintf("UpdateSearchConfigCommand: Creator: %s, Title: %s", c.Config.DefaultAuthor, c.Config.DefaultTitle) +} + type GetNextPageCommand struct { Condition SearchCondition } diff --git a/internal/ia/client.go b/internal/ia/client.go index 12049a2..c3269ca 100644 --- a/internal/ia/client.go +++ b/internal/ia/client.go @@ -172,8 +172,9 @@ func (client *IAClient) DownloadFile(localDir string, localFile string, iaServer req, _ := http.NewRequest("GET", fileUrl, nil) resp, _ := http.DefaultClient.Do(req) - if resp.StatusCode != 200 { + if resp == nil || resp.StatusCode != 200 { logger.Fatal("Error while downloading: " + resp.Status) + return } defer resp.Body.Close() diff --git a/internal/ui/config_page.go b/internal/ui/config_page.go index 2bbdca8..a0db1dc 100644 --- a/internal/ui/config_page.go +++ b/internal/ui/config_page.go @@ -27,6 +27,8 @@ type ConfigPage struct { logLevelField *tview.DropDown defaultAuthor *tview.InputField defaultTitle *tview.InputField + sortByField *tview.DropDown + sortOrderField *tview.DropDown rowsPerPage *tview.InputField useMockField *tview.Checkbox saveMockField *tview.Checkbox @@ -66,18 +68,20 @@ func newConfigPage(dispatcher *mq.Dispatcher) *ConfigPage { // Audobookbuilder config section p.configSection = newGrid() - p.configSection.SetColumns(-2, -2, -1) + p.configSection.SetColumns(-2, -2, 15) p.configSection.SetBorder(true) p.configSection.SetTitle(" Audiobook Builder Configuration: ") p.configSection.SetTitleAlign(tview.AlignLeft) configFormLeft := newForm() configFormLeft.SetHorizontal(false) - p.defaultAuthor = configFormLeft.AddInputField("Creator:", "", 20, nil, func(t string) { p.configCopy.SetDefaultAuthor(t) }) - p.defaultTitle = configFormLeft.AddInputField("Title:", "", 20, nil, func(t string) { p.configCopy.SetDefaultTitle(t) }) + p.defaultAuthor = configFormLeft.AddInputField("Creator:", "", 25, nil, func(t string) { p.configCopy.SetDefaultAuthor(t) }) + p.defaultTitle = configFormLeft.AddInputField("Title:", "", 25, nil, func(t string) { p.configCopy.SetDefaultTitle(t) }) + p.sortByField = configFormLeft.AddDropdown("Sort By:", utils.AddSpaces(p.configCopy.GetSortByOptions()), 1, func(o string, i int) { p.configCopy.SetSortBy(strings.TrimSpace(o)) }) + p.sortOrderField = configFormLeft.AddDropdown("Sort Order:", utils.AddSpaces(p.configCopy.GetSortOrderOptions()), 1, func(o string, i int) { p.configCopy.SetSortOrder(strings.TrimSpace(o)) }) p.rowsPerPage = configFormLeft.AddInputField("Page size:", "", 4, acceptInt, func(t string) { p.configCopy.SetRowsPerPage(utils.ToInt(t)) }) - p.useMockField = configFormLeft.AddCheckbox("Use mock?", false, func(t bool) { p.configCopy.SetUseMock(t) }) - p.saveMockField = configFormLeft.AddCheckbox("Save mock?", false, func(t bool) { p.configCopy.SetSaveMock(t) }) + // p.useMockField = configFormLeft.AddCheckbox("Use mock?", false, func(t bool) { p.configCopy.SetUseMock(t) }) + // p.saveMockField = configFormLeft.AddCheckbox("Save mock?", false, func(t bool) { p.configCopy.SetSaveMock(t) }) p.configSection.AddItem(configFormLeft.Form, 0, 0, 1, 1, 0, 0, true) configFormRight := newForm() @@ -95,7 +99,7 @@ func newConfigPage(dispatcher *mq.Dispatcher) *ConfigPage { buttonsForm := newForm() buttonsForm.SetHorizontal(false) buttonsForm.SetButtonsAlign(tview.AlignRight) - p.saveConfigButton = buttonsForm.AddButton(" Save ", p.SaveConfig) + p.saveConfigButton = buttonsForm.AddButton(" Save ", p.SaveConfig) buttonsGrid.AddItem(buttonsForm, 0, 0, 1, 1, 0, 0, true) buttonsForm = newForm() buttonsForm.SetHorizontal(false) @@ -156,9 +160,9 @@ func newConfigPage(dispatcher *mq.Dispatcher) *ConfigPage { p.mainGrid.SetNavigationOrder( p.defaultAuthor, p.defaultTitle, + p.sortByField, + p.sortOrderField, p.rowsPerPage, - p.useMockField, - p.saveMockField, p.outputDir, p.copyToOutputDir, p.tmpDir, @@ -210,9 +214,9 @@ func (p *ConfigPage) displayConfig(c *dto.DisplayConfigCommand) { p.logLevelField.SetCurrentOption(utils.GetIndex(logger.LogLeves(), p.configCopy.GetLogLevel())) p.defaultAuthor.SetText(p.configCopy.GetDefaultAuthor()) p.defaultTitle.SetText(p.configCopy.GetDefaultTitle()) + p.sortByField.SetCurrentOption(utils.GetIndex(config.Instance().GetSortByOptions(), p.configCopy.GetSortBy())) + p.sortOrderField.SetCurrentOption(utils.GetIndex(config.Instance().GetSortOrderOptions(), p.configCopy.GetSortOrder())) p.rowsPerPage.SetText(utils.ToString(p.configCopy.GetRowsPerPage())) - p.useMockField.SetChecked(p.configCopy.IsUseMock()) - p.saveMockField.SetChecked(p.configCopy.IsSaveMock()) p.concurrentDownloaders.SetText(utils.ToString(p.configCopy.GetConcurrentDownloaders())) p.concurrentEncoders.SetText(utils.ToString(p.configCopy.GetConcurrentEncoders())) diff --git a/internal/ui/search_page.go b/internal/ui/search_page.go index d17afae..4463ef0 100644 --- a/internal/ui/search_page.go +++ b/internal/ui/search_page.go @@ -33,6 +33,8 @@ type SearchPage struct { resultSection *grid resultTable *table + urlSection *grid + urlField *tview.TextView detailsSection *grid descriptionView *tview.TextView @@ -48,7 +50,7 @@ func newSearchPage(dispatcher *mq.Dispatcher) *SearchPage { p.searchCondition.Title = config.Instance().GetDefaultTitle() p.mainGrid = newGrid() - p.mainGrid.SetRows(9, -1, -1) + p.mainGrid.SetRows(9, -1, -1, 3) p.mainGrid.SetColumns(0) // search section @@ -67,8 +69,8 @@ func newSearchPage(dispatcher *mq.Dispatcher) *SearchPage { f.SetButtonsAlign(tview.AlignRight) p.searchSection.AddItem(f, 0, 0, 1, 1, 0, 0, true) f = newForm() - p.SortBy = f.AddDropdown("Sort by:", utils.AddSpaces([]string{"Creator", "Title", "Date", "Size "}), 1, func(o string, i int) { p.searchCondition.SortBy = p.mapSortBy(o) }) - p.sortOrder = f.AddDropdown("Sort order:", utils.AddSpaces([]string{"Ascending", "Descending"}), 1, func(o string, i int) { p.searchCondition.SortOrder = p.mapSortOrder(o) }) + p.SortBy = f.AddDropdown("Sort by:", utils.AddSpaces(config.Instance().GetSortByOptions()), utils.GetIndex(config.Instance().GetSortByOptions(), config.Instance().GetSortBy()), func(o string, i int) { p.searchCondition.SortBy = p.mapSortBy(o) }) + p.sortOrder = f.AddDropdown("Sort order:", utils.AddSpaces(config.Instance().GetSortOrderOptions()), utils.GetIndex(config.Instance().GetSortOrderOptions(), config.Instance().GetSortOrder()), func(o string, i int) { p.searchCondition.SortOrder = p.mapSortOrder(o) }) p.searchSection.AddItem(f, 0, 1, 1, 1, 0, 0, true) g := newGrid() g.SetRows(-1, -1) @@ -129,11 +131,26 @@ func newSearchPage(dispatcher *mq.Dispatcher) *SearchPage { p.mainGrid.AddItem(p.detailsSection.Grid, 2, 0, 1, 1, 0, 0, true) + // URL section + p.urlSection = newGrid() + p.urlSection.SetRows(1) + p.urlSection.SetColumns(-1) + p.urlSection.SetBorder(true) + p.urlSection.SetTitle(" Internet Archive item url: ") + p.urlSection.SetTitleAlign(tview.AlignLeft) + p.urlField = tview.NewTextView() + p.urlSection.AddItem(p.urlField, 0, 0, 1, 1, 0, 0, false) + p.mainGrid.AddItem(p.urlSection.Grid, 3, 0, 1, 1, 0, 0, true) + p.mq.SendMessage(mq.SearchPage, mq.Frame, &dto.SwitchToPageCommand{Name: "SearchPage"}, false) ui.SetFocus(p.searchSection.Grid) p.mainGrid.Focus(func(pr tview.Primitive) { - ui.SetFocus(p.searchSection.Grid) + if p.resultTable.GetRowCount() == 0 { + ui.SetFocus(p.searchSection.Grid) + } else { + ui.SetFocus(p.resultSection.Grid) + } }) // screen navigation order @@ -163,6 +180,8 @@ func (p *SearchPage) checkMQ() { func (p *SearchPage) dispatchMessage(m *mq.Message) { switch dto := m.Dto.(type) { + case *dto.UpdateSearchConfigCommand: + p.updateSearchConfig(dto) case *dto.IAItem: p.updateResult(dto) case *dto.SearchProgress: @@ -205,6 +224,17 @@ func (p *SearchPage) clearEverything() { p.author.SetText("") p.title.SetText("") p.clearSearchResults() + p.urlField.SetText("") +} + +func (p *SearchPage) updateSearchConfig(c *dto.UpdateSearchConfigCommand) { + if p.author.GetText() == "" && p.title.GetText() == "" { + p.author.SetText(c.Config.GetDefaultAuthor()) + p.title.SetText(c.Config.GetDefaultTitle()) + p.SortBy.SetCurrentOption(utils.GetIndex(c.Config.GetSortByOptions(), c.Config.GetSortBy())) + p.sortOrder.SetCurrentOption(utils.GetIndex(c.Config.GetSortOrderOptions(), c.Config.GetSortOrder())) + ui.Draw() + } } func (p *SearchPage) lastRowEvent() { @@ -230,17 +260,20 @@ func (p *SearchPage) updateTitle(sp *dto.SearchProgress) { func (p *SearchPage) updateDetails(row int, col int) { if row > 0 && len(p.searchResult) > 0 && len(p.searchResult) >= row { - d := p.searchResult[row-1].Description + item := p.searchResult[row-1] + d := item.Description p.descriptionView.SetText(d) p.descriptionView.ScrollToBeginning() p.filesTable.Clear() p.filesTable.showHeader() - files := p.searchResult[row-1].AudioFiles + files := item.AudioFiles for _, f := range files { p.filesTable.appendRow(f.Name, f.Format, utils.SecondsToTime(f.Length), utils.BytesToHuman(f.Size)) } p.filesTable.ScrollToBeginning() + + p.urlField.SetText(" " + item.IaURL) } }