diff --git a/internal/config/config.go b/internal/config/config.go index c89e34d..b2e1a80 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -26,7 +26,8 @@ var ( // Fields of this stuct should to be private but I have to make them public because yaml.Marshal/Unmarshal can't work with private fields type Config struct { - SearchCondition string `yaml:"SearchCondition"` + DefaultAuthor string `yaml:"DefaultAuthor"` + DefaultTitle string `yaml:"DefaultTitle"` RowsPerPage int `yaml:"RowsPerPage"` LogFileName string `yaml:"LogFileName"` OutputDir string `yaml:"Outputdir"` @@ -72,12 +73,13 @@ func Load() { config.LogFileName = "abb_ia.log" config.TmpDir = "tmp" config.CopyToOutputDir = true - config.OutputDir = "/mnt/NAS/Audiobooks/Internet Archive" + config.OutputDir = "output" config.LogLevel = "INFO" config.RowsPerPage = 25 config.UseMock = false config.SaveMock = false - config.SearchCondition = "" + config.DefaultAuthor = "Old Time Radio Researchers Group" + config.DefaultTitle = "Single Episodes" config.ConcurrentDownloaders = 5 config.ConcurrentEncoders = 5 config.ReEncodeFiles = true @@ -85,8 +87,8 @@ func Load() { config.BitRateKbs = 128 config.SampleRateHz = 44100 config.MaxFileSizeMb = 250 - config.UploadToAudiobookshef = true - config.ScanAudiobookshef = true + config.UploadToAudiobookshef = false + config.ScanAudiobookshef = false config.AudiobookshelfUser = "admin" config.AudiobookshelfPassword = "" config.AudiobookshelfLibrary = "Internet Archive" @@ -102,6 +104,7 @@ func Load() { "History", "Podcast", "Nonfiction", + "News", "Speech", } @@ -206,12 +209,20 @@ func (c *Config) IsSaveMock() bool { return c.SaveMock } -func (c *Config) SetSearchCondition(s string) { - c.SearchCondition = s +func (c *Config) SetDefaultAuthor(s string) { + c.DefaultAuthor = s } -func (c *Config) GetSearchCondition() string { - return c.SearchCondition +func (c *Config) GetDefaultAuthor() string { + return c.DefaultAuthor +} + +func (c *Config) SetDefaultTitle(s string) { + c.DefaultTitle = s +} + +func (c *Config) GetDefaultTitle() string { + return c.DefaultTitle } func (c *Config) SetConcurrentDownloaders(n int) { diff --git a/internal/controller/search_controller.go b/internal/controller/search_controller.go index 2833f64..1bced27 100644 --- a/internal/controller/search_controller.go +++ b/internal/controller/search_controller.go @@ -9,7 +9,7 @@ import ( "abb_ia/internal/config" "abb_ia/internal/dto" - ia_client "abb_ia/internal/ia" + "abb_ia/internal/ia" "abb_ia/internal/logger" "abb_ia/internal/mq" "abb_ia/internal/utils" @@ -61,9 +61,9 @@ func (c *SearchController) search(cmd *dto.SearchCommand) { c.mq.SendMessage(mq.SearchController, mq.Footer, &dto.SetBusyIndicator{Busy: true}, false) c.totalItemsFetched = 0 c.ia = ia_client.New(config.Instance().GetRowsPerPage(), config.Instance().IsUseMock(), config.Instance().IsSaveMock()) - resp := c.ia.Search(cmd.SearchCondition, "audio") + resp := c.ia.Search(cmd.Condition.Author, cmd.Condition.Title, "audio") if resp == nil { - logger.Error(mq.SearchController + ": Failed to perform IA search with condition: " + cmd.SearchCondition) + logger.Error(mq.SearchController + ": Failed to perform IA search with condition: " + cmd.Condition.Author + " - " + cmd.Condition.Title) } itemsFetched, err := c.fetchDetails(resp) if err != nil { @@ -71,9 +71,9 @@ func (c *SearchController) search(cmd *dto.SearchCommand) { } c.mq.SendMessage(mq.SearchController, mq.Footer, &dto.SetBusyIndicator{Busy: false}, false) c.mq.SendMessage(mq.SearchController, mq.Footer, &dto.UpdateStatus{Message: ""}, false) - c.mq.SendMessage(mq.SearchController, mq.SearchPage, &dto.SearchComplete{SearchCondition: cmd.SearchCondition}, false) + c.mq.SendMessage(mq.SearchController, mq.SearchPage, &dto.SearchComplete{Condition: cmd.Condition}, false) if itemsFetched == 0 { - c.mq.SendMessage(mq.SearchController, mq.SearchPage, &dto.NothingFoundError{SearchCondition: cmd.SearchCondition}, false) + c.mq.SendMessage(mq.SearchController, mq.SearchPage, &dto.NothingFoundError{Condition: cmd.Condition}, false) } } @@ -81,9 +81,9 @@ func (c *SearchController) getGetNextPage(cmd *dto.GetNextPageCommand) { logger.Info(mq.SearchController + " received " + cmd.String()) c.mq.SendMessage(mq.SearchController, mq.Footer, &dto.UpdateStatus{Message: "Fetching Internet Archive items..."}, false) c.mq.SendMessage(mq.SearchController, mq.Footer, &dto.SetBusyIndicator{Busy: true}, false) - resp := c.ia.GetNextPage(cmd.SearchCondition, "audio") + resp := c.ia.GetNextPage(cmd.Condition.Author, cmd.Condition.Title, "audio") if resp == nil { - logger.Error(mq.SearchController + ": Failed to perform IA search with condition: " + cmd.SearchCondition) + logger.Error(mq.SearchController + ": Failed to perform IA search with condition: " + cmd.Condition.Author + " - " + cmd.Condition.Title) } itemsFetched, err := c.fetchDetails(resp) if err != nil { @@ -91,9 +91,9 @@ func (c *SearchController) getGetNextPage(cmd *dto.GetNextPageCommand) { } c.mq.SendMessage(mq.SearchController, mq.Footer, &dto.SetBusyIndicator{Busy: false}, false) c.mq.SendMessage(mq.SearchController, mq.Footer, &dto.UpdateStatus{Message: ""}, false) - c.mq.SendMessage(mq.SearchController, mq.SearchPage, &dto.SearchComplete{SearchCondition: cmd.SearchCondition}, false) + c.mq.SendMessage(mq.SearchController, mq.SearchPage, &dto.SearchComplete{Condition: cmd.Condition}, false) if itemsFetched == 0 { - c.mq.SendMessage(mq.SearchController, mq.SearchPage, &dto.LastPageMessage{SearchCondition: cmd.SearchCondition}, false) + c.mq.SendMessage(mq.SearchController, mq.SearchPage, &dto.LastPageMessage{Condition: cmd.Condition}, false) } } diff --git a/internal/controller/upload_controller.go b/internal/controller/upload_controller.go index dda22a1..7a803b3 100644 --- a/internal/controller/upload_controller.go +++ b/internal/controller/upload_controller.go @@ -65,12 +65,12 @@ func (c *UploadController) absScan(cmd *dto.AbsScanCommand) { absClient := audiobookshelf.NewClient(url) err := absClient.Login(username, password) if err != nil { - logger.Error("Can't login to audiobookshlf server: " + err.Error()) + logger.Error("Can't login to audiobookshelf server: " + err.Error()) return } libraries, err := absClient.GetLibraries() if err != nil { - logger.Error("Can't get a list of libraries from audiobookshlf server: " + err.Error()) + logger.Error("Can't get a list of libraries from audiobookshelf server: " + err.Error()) return } libraryID, err := absClient.GetLibraryId(libraries, libraryName) @@ -80,14 +80,14 @@ func (c *UploadController) absScan(cmd *dto.AbsScanCommand) { } err = absClient.ScanLibrary(libraryID) if err != nil { - logger.Error("Can't launch library scan on audiobookshlf server: " + err.Error()) + logger.Error("Can't launch library scan on audiobookshelf server: " + err.Error()) return } if err != nil { - logger.Error("Can't launch library scan on audiobookshlf server: " + err.Error()) + logger.Error("Can't launch library scan on audiobookshelf server: " + err.Error()) return } - logger.Info("A scan launched for library " + libraryName + " on audiobookshlf server") + logger.Info("A scan launched for library " + libraryName + " on audiobookshelf server") } c.mq.SendMessage(mq.UploadController, mq.BuildPage, &dto.ScanComplete{Audiobook: cmd.Audiobook}, true) } diff --git a/internal/dto/search.go b/internal/dto/search.go index 4088e19..bae190f 100644 --- a/internal/dto/search.go +++ b/internal/dto/search.go @@ -2,44 +2,49 @@ package dto import "fmt" +type SearchCondition struct { + Author string + Title string +} + type SearchCommand struct { - SearchCondition string + Condition SearchCondition } func (c *SearchCommand) String() string { - return fmt.Sprintf("SearchCommand: %s", c.SearchCondition) + return fmt.Sprintf("SearchCommand: Author: %s, Title: %s", c.Condition.Author, c.Condition.Title) } type GetNextPageCommand struct { - SearchCondition string + Condition SearchCondition } func (c *GetNextPageCommand) String() string { - return fmt.Sprintf("GetNextPageCommand: %s", c.SearchCondition) + return fmt.Sprintf("GetNextPageCommand: Author: %s, Title: %s", c.Condition.Author, c.Condition.Title) } type SearchComplete struct { - SearchCondition string + Condition SearchCondition } func (c *SearchComplete) String() string { - return fmt.Sprintf("SearchComplete: %s", c.SearchCondition) + return fmt.Sprintf("SearchComplete: Author: %s, Title: %s", c.Condition.Author, c.Condition.Title) } type NothingFoundError struct { - SearchCondition string + Condition SearchCondition } func (c *NothingFoundError) String() string { - return fmt.Sprintf("NothingFoundError: %s", c.SearchCondition) + return fmt.Sprintf("NothingFoundError: Author: %s, Title: %s", c.Condition.Author, c.Condition.Title) } type LastPageMessage struct { - SearchCondition string + Condition SearchCondition } func (c *LastPageMessage) String() string { - return fmt.Sprintf("LastPageMessage: %s", c.SearchCondition) + return fmt.Sprintf("LastPageMessage: Author: %s, Title: %s", c.Condition.Author, c.Condition.Title) } type SearchProgress struct { diff --git a/internal/ia/client.go b/internal/ia/client.go index 2184a4d..26fd8f5 100644 --- a/internal/ia/client.go +++ b/internal/ia/client.go @@ -44,36 +44,44 @@ func New(maxSearchRows int, useMock bool, saveMock bool) *IAClient { return client } -func (client *IAClient) Search(searchCondition string, mediaType string) *SearchResponse { - if strings.Contains(searchCondition, IA_BASE_URL+"/details/") { - item_id := strings.Split(searchCondition, "/")[4] +func (client *IAClient) Search(author string, title string, mediaType string) *SearchResponse { + if strings.Contains(title, IA_BASE_URL+"/details/") { + item_id := strings.Split(title, "/")[4] return client.searchByID(item_id, mediaType) } else { client.page = 1 - return client.searchByTitle(searchCondition, mediaType) + return client.searchByTitle(author, title, mediaType) } } -func (client *IAClient) GetNextPage(searchCondition string, mediaType string) *SearchResponse { - if strings.Contains(searchCondition, IA_BASE_URL+"/details/") { +func (client *IAClient) GetNextPage(author string, title string, mediaType string) *SearchResponse { + if strings.Contains(title, IA_BASE_URL+"/details/") { return &SearchResponse{} } else { client.page += 1 - resp := client.searchByTitle(searchCondition, mediaType) + resp := client.searchByTitle(author, title, mediaType) return resp } } -func (client *IAClient) searchByTitle(title string, mediaType string) *SearchResponse { - mockFile := MOCK_DIR + "/SearchByTitle.json" +func (client *IAClient) searchByTitle(author string, title string, mediaType string) *SearchResponse { + mockFile := MOCK_DIR + "/SearchByAuthorAndTitle.json" result := &SearchResponse{} if client.loadMockResult { if err := utils.LoadJson(mockFile, result); err != nil { - logger.Error("IA Client SearchByTitle() mock load error: " + err.Error()) + logger.Error("IA Client SearchByAuthorAndTitle() mock load error: " + err.Error()) } } else { - var searchURL = fmt.Sprintf(IA_BASE_URL+"/advancedsearch.php?q=title:(%s)+AND+mediatype:(%s)&output=json&rows=%d&page=%d", - url.QueryEscape(title), mediaType, client.maxSearchRows, client.page) + searchCondition := "" + if author != "" && title != "" { + searchCondition = fmt.Sprintf("creator:(%s)+AND+title:(%s)", url.QueryEscape(author), url.QueryEscape(title)) + } else if author != "" { + searchCondition = fmt.Sprintf("creator:(%s)", url.QueryEscape(author)) + } else if title != "" { + searchCondition = fmt.Sprintf("title:(%s)", url.QueryEscape(title)) + } + var searchURL = fmt.Sprintf(IA_BASE_URL+"/advancedsearch.php?q=%s+AND+mediatype:(%s)&output=json&rows=%d&page=%d", + searchCondition, mediaType, client.maxSearchRows, client.page) logger.Debug("IA request: " + searchURL) _, err := client.restyClient.R().SetResult(result).Get(searchURL) if err != nil { diff --git a/internal/ia/client_test.go b/internal/ia/client_test.go index 6f059fa..b64d22a 100644 --- a/internal/ia/client_test.go +++ b/internal/ia/client_test.go @@ -26,11 +26,19 @@ func TestMain(m *testing.M) { func TestSearch(t *testing.T) { ia := ia_client.New(5, false, false) - res := ia.Search("Single Episodes", "audio") // search by title + res := ia.Search("Old Time Radio Researchers", "Single Episodes", "audio") // search by author and title assert.NotNil(t, res) assert.Equal(t, 5, len(res.Response.Docs)) - res = ia.Search("https://archive.org/details/OTRR_Frank_Race_Singles", "audio") // search by item ID + res = ia.Search("Old Time Radio Researchers", "", "audio") // search by author only + assert.NotNil(t, res) + assert.Equal(t, 5, len(res.Response.Docs)) + + res = ia.Search("", "Single Episodes", "audio") // search by title only + assert.NotNil(t, res) + assert.Equal(t, 5, len(res.Response.Docs)) + + res = ia.Search("", "https://archive.org/details/OTRR_Frank_Race_Singles", "audio") // search by item ID assert.NotNil(t, res) assert.Equal(t, 1, len(res.Response.Docs)) } diff --git a/internal/ia/model.go b/internal/ia/model.go index f6627cb..b242a71 100644 --- a/internal/ia/model.go +++ b/internal/ia/model.go @@ -21,6 +21,8 @@ type SearchResponse struct { NumFound int `json:"numFound"` Start int `json:"start"` Docs []struct { + AvgRating float64 `json:"avg_rating"` + Btih strArray `json:"btih"` Collection []string `json:"collection"` Creator strArray `json:"creator,omitempty"` Date time.Time `json:"date,omitempty"` @@ -34,6 +36,7 @@ type SearchResponse struct { Month int `json:"month"` OaiUpdatedate []time.Time `json:"oai_updatedate"` Publicdate time.Time `json:"publicdate"` + Reviewdate time.Time `json:"reviewdate"` Subject strArray `json:"subject,omitempty"` Title string `json:"title"` Week int `json:"week"` @@ -52,28 +55,44 @@ type ItemDetails struct { Server string `json:"server"` Dir string `json:"dir"` Metadata struct { - Identifier []string `json:"identifier"` - Creator []string `json:"creator"` - Artist []string `json:"artist"` - Date []string `json:"date"` - Description []string `json:"description"` - GUID []string `json:"guid"` - Mediatype []string `json:"mediatype"` - Rssfeed []string `json:"rssfeed"` - Scanner []string `json:"scanner"` - Sessionid []string `json:"sessionid"` - Subject []string `json:"subject"` - Title []string `json:"title"` - Uploadsoftware []string `json:"uploadsoftware"` - Collection []string `json:"collection"` - Publicdate []string `json:"publicdate"` - Addeddate []string `json:"addeddate"` - Curation []string `json:"curation"` - AccessRestrictedItem []string `json:"access-restricted-item"` + Identifier []string `json:"identifier"` + Creator []string `json:"creator"` + Artist []string `json:"artist"` + Date []string `json:"date"` + Description []string `json:"description"` + GUID []string `json:"guid"` + Mediatype []string `json:"mediatype"` + Rssfeed []string `json:"rssfeed"` + Scanner []string `json:"scanner"` + Sessionid []string `json:"sessionid"` + Subject []string `json:"subject"` + Title []string `json:"title"` + Uploadsoftware []string `json:"uploadsoftware"` + Collection []string `json:"collection"` + Licenseurl []string `json:"licenseurl"` + Notes []string `json:"notes"` + Publicdate []string `json:"publicdate"` + Addeddate []string `json:"addeddate"` + Curation []string `json:"curation"` + BackupLocation []string `json:"backup_location"` + AccessRestrictedItem []string `json:"access-restricted-item"` + ExternalMetadataUpdate []string `json:"external_metadata_update"` + Reviews struct { + Info map[string]string `json:"info"` + Reviews []struct { + ReviewBody string `json:"reviewbody"` + ReviewTitle string `json:"reviewtitle"` + Reviewer string `json:"reviewer"` + ReviewerItemname string `json:"reviewer_itemname"` + ReviewDate string `json:"reviewdate"` + Stars string `json:"stars"` + } `json:"reviews"` + } `json:"reviews"` } `json:"metadata"` Files map[string]struct { Source string `json:"source"` Format string `json:"format"` + Original string `json:"original"` Length string `json:"length"` Mtime string `json:"mtime"` Size string `json:"size"` diff --git a/internal/mq/dispatcher.go b/internal/mq/dispatcher.go index 3bcf2db..4ab7f23 100644 --- a/internal/mq/dispatcher.go +++ b/internal/mq/dispatcher.go @@ -75,12 +75,12 @@ func (d *Dispatcher) SendMessage(from string, to string, dto dto.Dto, async bool m.Async = async if async { + d.mu.Lock() // push message to queue if _, ok := d.recipients[m.To]; !ok { d.recipients[m.To] = messageQueue{list.New()} } // check if such message is already in queue - d.mu.Lock() if !d.messageExists(m) { d.recipients[m.To].messages.PushBack(m) logger.Debug("MQ <-- async " + m.String()) diff --git a/internal/ui/build_page.go b/internal/ui/build_page.go index 9a89b54..5bdfc23 100644 --- a/internal/ui/build_page.go +++ b/internal/ui/build_page.go @@ -56,11 +56,11 @@ func newBuildPage(dispatcher *mq.Dispatcher) *BuildPage { // audiobook build section p.buildSection = newGrid() p.buildSection.SetColumns(-1) - p.buildSection.SetTitle(" Building audiobook... ") + p.buildSection.SetTitle(" Building audiobook: ") p.buildSection.SetTitleAlign(tview.AlignLeft) p.buildSection.SetBorder(true) p.buildTable = newTable() - p.buildTable.setHeaders(" # ", "File name", "Format", "Duration", "Total Size", "Build progress") + p.buildTable.setHeaders(" # ", "File name", "Format", "Duration", "Size", "Build progress") p.buildTable.setWeights(1, 2, 1, 1, 1, 5) p.buildTable.setAlign(tview.AlignRight, tview.AlignLeft, tview.AlignLeft, tview.AlignRight, tview.AlignRight, tview.AlignLeft) p.buildSection.AddItem(p.buildTable.Table, 0, 0, 1, 1, 0, 0, true) @@ -73,7 +73,7 @@ func newBuildPage(dispatcher *mq.Dispatcher) *BuildPage { p.copySection.SetTitleAlign(tview.AlignLeft) p.copySection.SetBorder(true) p.copyTable = newTable() - p.copyTable.setHeaders(" # ", "File name", "Format", "Duration", "Total Size", "Copy Progress") + p.copyTable.setHeaders(" # ", "File name", "Format", "Duration", "Size", "Copy Progress") p.copyTable.setWeights(1, 2, 1, 1, 1, 5) p.copyTable.setAlign(tview.AlignRight, tview.AlignLeft, tview.AlignLeft, tview.AlignRight, tview.AlignRight, tview.AlignLeft) p.copySection.AddItem(p.copyTable.Table, 0, 0, 1, 1, 0, 0, true) @@ -86,7 +86,7 @@ func newBuildPage(dispatcher *mq.Dispatcher) *BuildPage { p.uploadSection.SetTitleAlign(tview.AlignLeft) p.uploadSection.SetBorder(true) p.uploadTable = newTable() - p.uploadTable.setHeaders(" # ", "File name", "Format", "Duration", "Total Size", "Upload Progress") + p.uploadTable.setHeaders(" # ", "File name", "Format", "Duration", "Size", "Upload Progress") p.uploadTable.setWeights(1, 2, 1, 1, 1, 5) p.uploadTable.setAlign(tview.AlignRight, tview.AlignLeft, tview.AlignLeft, tview.AlignRight, tview.AlignRight, tview.AlignLeft) p.uploadSection.AddItem(p.uploadTable.Table, 0, 0, 1, 1, 0, 0, true) diff --git a/internal/ui/chapters_page.go b/internal/ui/chapters_page.go index 4a33433..612eeff 100644 --- a/internal/ui/chapters_page.go +++ b/internal/ui/chapters_page.go @@ -62,19 +62,19 @@ func newChaptersPage(dispatcher *mq.Dispatcher) *ChaptersPage { // book info section infoSection := newGrid() - infoSection.SetColumns(50, 50, -1, 30) + infoSection.SetColumns(40, 30, -1, 30) infoSection.SetRows(5, 2) infoSection.SetBorder(true) infoSection.SetTitle(" Audiobook information: ") infoSection.SetTitleAlign(tview.AlignLeft) f0 := newForm() f0.SetBorderPadding(1, 0, 1, 2) - p.inputAuthor = f0.AddInputField("Author:", "", 40, nil, func(s string) { + p.inputAuthor = f0.AddInputField("Author:", "", 30, nil, func(s string) { if p.ab != nil { p.ab.Author = s } }) - p.inputTitle = f0.AddInputField("Title:", "", 40, nil, func(s string) { + p.inputTitle = f0.AddInputField("Title:", "", 30, nil, func(s string) { if p.ab != nil { p.ab.Title = s } @@ -82,7 +82,7 @@ func newChaptersPage(dispatcher *mq.Dispatcher) *ChaptersPage { infoSection.AddItem(f0.Form, 0, 0, 1, 1, 0, 0, true) f1 := newForm() f1.SetBorderPadding(1, 0, 2, 2) - p.inputSeries = f1.AddInputField("Series:", "", 40, nil, func(s string) { + p.inputSeries = f1.AddInputField("Series:", "", 20, nil, func(s string) { if p.ab != nil { p.ab.Series = s } @@ -100,7 +100,7 @@ func newChaptersPage(dispatcher *mq.Dispatcher) *ChaptersPage { p.ab.Genre = strings.TrimSpace(s) } }) - p.inputNarator = f2.AddInputField("Narator:", "", 40, nil, func(s string) { + p.inputNarator = f2.AddInputField("Narator:", "", 20, nil, func(s string) { if p.ab != nil { p.ab.Narator = s } @@ -115,7 +115,7 @@ func newChaptersPage(dispatcher *mq.Dispatcher) *ChaptersPage { }) infoSection.AddItem(f3.Form, 1, 0, 1, 4, 0, 0, true) f4 := newForm() - f4.SetHorizontal(false) + f4.SetHorizontal(true) f4.SetButtonsAlign(tview.AlignRight) p.buttonCreateBook = f4.AddButton("Create Book", p.buildBook) p.buttonCancel = f4.AddButton("Cancel", p.stopConfirmation) @@ -124,7 +124,7 @@ func newChaptersPage(dispatcher *mq.Dispatcher) *ChaptersPage { // description section descriptionSection := newGrid() - descriptionSection.SetColumns(-1, 45) + descriptionSection.SetColumns(-1, 33) descriptionSection.SetBorder(false) p.textAreaDescription = newTextArea("") p.textAreaDescription.SetChangedFunc(p.updateDescription) @@ -136,8 +136,8 @@ func newChaptersPage(dispatcher *mq.Dispatcher) *ChaptersPage { f5.SetBorder(true) f5.SetHorizontal(true) - p.inputSearchDescription = f5.AddInputField("Search: ", "", 30, nil, func(s string) { p.searchDescription = s }) - p.inputReplaceDescription = f5.AddInputField("Replace:", "", 30, nil, func(s string) { p.replaceDescription = s }) + p.inputSearchDescription = f5.AddInputField("Search: ", "", 20, nil, func(s string) { p.searchDescription = s }) + p.inputReplaceDescription = f5.AddInputField("Replace:", "", 20, nil, func(s string) { p.replaceDescription = s }) p.buttonDescriptionReplace = f5.AddButton("Replace", p.searchReplaceDescription) p.buttonDescriptionUndo = f5.AddButton(" Undo ", p.undoDescription) f5.SetButtonsAlign(tview.AlignRight) @@ -146,13 +146,13 @@ func newChaptersPage(dispatcher *mq.Dispatcher) *ChaptersPage { // chapters section p.chaptersSection = newGrid() - p.chaptersSection.SetColumns(-1, 45) + p.chaptersSection.SetColumns(-1, 33) p.chaptersTable = newTable() p.chaptersTable.SetBorder(true) p.chaptersTable.SetTitle(" Book chapters: ") p.chaptersTable.SetTitleAlign(tview.AlignLeft) p.chaptersTable.setHeaders(" # ", "Start", "End", "Duration", "Chapter name") - p.chaptersTable.setWeights(1, 1, 1, 1, 20) + p.chaptersTable.setWeights(1, 1, 1, 2, 10) p.chaptersTable.setAlign(tview.AlignRight, tview.AlignRight, tview.AlignRight, tview.AlignRight, tview.AlignLeft) p.chaptersTable.SetSelectedFunc(p.updateChapterEntry) p.chaptersTable.SetMouseDblClickFunc(p.updateChapterEntry) @@ -166,8 +166,8 @@ func newChaptersPage(dispatcher *mq.Dispatcher) *ChaptersPage { f6 := newForm() f6.SetBorder(false) f6.SetHorizontal(true) - p.inputSearchChapters = f6.AddInputField("Search: ", "", 30, nil, func(s string) { p.searchChapters = s }) - p.inputReplaceChapters = f6.AddInputField("Replace:", "", 30, nil, func(s string) { p.replaceChapters = s }) + p.inputSearchChapters = f6.AddInputField("Search: ", "", 20, nil, func(s string) { p.searchChapters = s }) + p.inputReplaceChapters = f6.AddInputField("Replace:", "", 20, nil, func(s string) { p.replaceChapters = s }) p.buttonChaptersReplace = f6.AddButton("Replace", p.searchReplaceChapters) p.buttonChaptersUndo = f6.AddButton(" Undo ", p.undoChapters) p.buttonChaptersJoin = f6.AddButton(" Join Similar Chapters ", p.joinChapters) diff --git a/internal/ui/colors.go b/internal/ui/colors.go index d2b3b77..69a9b10 100644 --- a/internal/ui/colors.go +++ b/internal/ui/colors.go @@ -16,6 +16,9 @@ var ( cyan = tcell.NewRGBColor(80, 176, 189) red = tcell.ColorRed + labelsColor = yellow + valuesColor = white + footerFgColor = black footerBgColor = cyan diff --git a/internal/ui/config_page.go b/internal/ui/config_page.go index 0053908..2bbdca8 100644 --- a/internal/ui/config_page.go +++ b/internal/ui/config_page.go @@ -25,7 +25,8 @@ type ConfigPage struct { // Audobookbuilder config section logFileNameField *tview.InputField logLevelField *tview.DropDown - searchCondition *tview.InputField + defaultAuthor *tview.InputField + defaultTitle *tview.InputField rowsPerPage *tview.InputField useMockField *tview.Checkbox saveMockField *tview.Checkbox @@ -60,7 +61,7 @@ func newConfigPage(dispatcher *mq.Dispatcher) *ConfigPage { p.mq.RegisterListener(mq.ConfigPage, p.dispatchMessage) p.mainGrid = newGrid() - p.mainGrid.SetRows(-1, -1, -1) + p.mainGrid.SetRows(13, 13, -1) p.mainGrid.SetColumns(0) // Audobookbuilder config section @@ -72,28 +73,36 @@ func newConfigPage(dispatcher *mq.Dispatcher) *ConfigPage { configFormLeft := newForm() configFormLeft.SetHorizontal(false) - p.searchCondition = configFormLeft.AddInputField("Default Search condition:", "", 40, nil, func(t string) { p.configCopy.SetSearchCondition(t) }) - p.rowsPerPage = configFormLeft.AddInputField("Rows per a page in the search result:", "", 4, acceptInt, func(t string) { p.configCopy.SetRowsPerPage(utils.ToInt(t)) }) + 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.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.configSection.AddItem(configFormLeft.Form, 0, 0, 1, 1, 0, 0, true) configFormRight := newForm() configFormRight.SetHorizontal(false) - p.outputDir = configFormRight.AddInputField("Output directory:", "", 40, nil, func(t string) { p.configCopy.SetOutputdDir(t) }) - p.copyToOutputDir = configFormRight.AddCheckbox("Copy audiobook to the output directory?", false, func(t bool) { p.configCopy.SetCopyToOutputDir(t) }) - p.tmpDir = configFormRight.AddInputField("Temporary (working) directory:", "", 40, nil, func(t string) { p.configCopy.SetTmpDir(t) }) - p.logFileNameField = configFormRight.AddInputField("Log file name:", "", 40, nil, func(t string) { p.configCopy.SetLogfileName(t) }) + p.outputDir = configFormRight.AddInputField("Output directory:", "", 25, nil, func(t string) { p.configCopy.SetOutputdDir(t) }) + p.copyToOutputDir = configFormRight.AddCheckbox("Copy to output dir?", false, func(t bool) { p.configCopy.SetCopyToOutputDir(t) }) + p.tmpDir = configFormRight.AddInputField("Work directory:", "", 25, nil, func(t string) { p.configCopy.SetTmpDir(t) }) + p.logFileNameField = configFormRight.AddInputField("Log name:", "", 25, nil, func(t string) { p.configCopy.SetLogfileName(t) }) p.logLevelField = configFormRight.AddDropdown("Log level:", utils.AddSpaces(logger.LogLeves()), 1, func(o string, i int) { p.configCopy.SetLogLevel(strings.TrimSpace(o)) }) p.configSection.AddItem(configFormRight.Form, 0, 1, 1, 1, 0, 0, true) + buttonsGrid := newGrid() + buttonsGrid.SetRows(3, 3, -1) + buttonsGrid.SetColumns(0) buttonsForm := newForm() buttonsForm.SetHorizontal(false) buttonsForm.SetButtonsAlign(tview.AlignRight) - p.saveConfigButton = buttonsForm.AddButton("Save Settings", p.SaveConfig) + p.saveConfigButton = buttonsForm.AddButton(" Save ", p.SaveConfig) + buttonsGrid.AddItem(buttonsForm, 0, 0, 1, 1, 0, 0, true) + buttonsForm = newForm() + buttonsForm.SetHorizontal(false) + buttonsForm.SetButtonsAlign(tview.AlignRight) p.cancelButton = buttonsForm.AddButton("Cancel", p.Cancel) - p.configSection.AddItem(buttonsForm.Form, 0, 2, 1, 1, 0, 0, false) - + buttonsGrid.AddItem(buttonsForm, 1, 0, 1, 1, 0, 0, true) + p.configSection.AddItem(buttonsGrid, 0, 2, 1, 1, 0, 0, true) p.mainGrid.AddItem(p.configSection.Grid, 0, 0, 1, 1, 0, 0, true) // audiobook build configuration section @@ -107,7 +116,7 @@ func newConfigPage(dispatcher *mq.Dispatcher) *ConfigPage { buildFormLeft.SetHorizontal(false) p.concurrentDownloaders = buildFormLeft.AddInputField("Concurrent Downloaders:", "", 4, acceptInt, func(t string) { p.configCopy.SetConcurrentDownloaders(utils.ToInt(t)) }) p.concurrentEncoders = buildFormLeft.AddInputField("Concurrent Encoders:", "", 4, acceptInt, func(t string) { p.configCopy.SetConcurrentEncoders(utils.ToInt(t)) }) - p.reEncodeFiles = buildFormLeft.AddCheckbox("Re-encode .mp3 files to the same Bit Rate?", false, func(t bool) { p.configCopy.SetReEncodeFiles(t) }) + p.reEncodeFiles = buildFormLeft.AddCheckbox("Re-encode .mp3 files?", false, func(t bool) { p.configCopy.SetReEncodeFiles(t) }) p.bitRate = buildFormLeft.AddInputField("Bit Rate (Kbps):", "", 4, acceptInt, func(t string) { p.configCopy.SetBitRate(utils.ToInt(t)) }) p.sampleRate = buildFormLeft.AddInputField("Sample Rate (Hz):", "", 6, acceptInt, func(t string) { p.configCopy.SetSampleRate(utils.ToInt(t)) }) p.buildSection.AddItem(buildFormLeft.Form, 0, 0, 1, 1, 0, 0, true) @@ -115,7 +124,7 @@ func newConfigPage(dispatcher *mq.Dispatcher) *ConfigPage { buildFormRight := newForm() buildFormRight.SetHorizontal(false) p.maxFileSize = buildFormRight.AddInputField("Audiobook part max file size (Mb):", "", 6, acceptInt, func(t string) { p.configCopy.SetMaxFileSizeMb(utils.ToInt(t)) }) - p.shortenTitles = buildFormRight.AddCheckbox("Shorten titles (for ex. Old Time Radio -> OTRR)?", false, func(t bool) { p.configCopy.SetShortenTitles(t) }) + p.shortenTitles = buildFormRight.AddCheckbox("Shorten titles (-> OTRR for ex.)?", false, func(t bool) { p.configCopy.SetShortenTitles(t) }) p.buildSection.AddItem(buildFormRight.Form, 0, 1, 1, 1, 0, 0, true) p.mainGrid.AddItem(p.buildSection.Grid, 1, 0, 1, 1, 0, 0, true) @@ -130,10 +139,10 @@ func newConfigPage(dispatcher *mq.Dispatcher) *ConfigPage { absFormLeft := newForm() absFormLeft.SetHorizontal(false) p.uploadToAudiobookshelf = absFormLeft.AddCheckbox("Upload the audiobook to Audiobookshelf server?", false, func(t bool) { p.configCopy.SetUploadToAudiobookshelf(t) }) - p.audiobookshelfUrl = absFormLeft.AddInputField("Audiobookshelf Server URL:", "", 40, nil, func(t string) { p.configCopy.SetAudiobookshelfUrl(t) }) - p.audiobookshelfUser = absFormLeft.AddInputField("Audiobookshelf Server User:", "", 40, nil, func(t string) { p.configCopy.SetAudiobookshelfUser(t) }) - p.audiobookshelfPassword = absFormLeft.AddPasswordField("Audiobookshelf Server Password:", "", 40, 0, func(t string) { p.configCopy.SetAudiobookshelfPassword(t) }) - p.audiobookshelfLibrary = absFormLeft.AddInputField("Audiobookshelf destination Library:", "", 40, nil, func(t string) { p.configCopy.SetAudiobookshelfLibrary(t) }) + p.audiobookshelfUrl = absFormLeft.AddInputField("Audiobookshelf Server URL:", "", 30, nil, func(t string) { p.configCopy.SetAudiobookshelfUrl(t) }) + p.audiobookshelfUser = absFormLeft.AddInputField("Audiobookshelf Server User:", "", 30, nil, func(t string) { p.configCopy.SetAudiobookshelfUser(t) }) + p.audiobookshelfPassword = absFormLeft.AddPasswordField("Audiobookshelf Server Password:", "", 30, 0, func(t string) { p.configCopy.SetAudiobookshelfPassword(t) }) + p.audiobookshelfLibrary = absFormLeft.AddInputField("Audiobookshelf destination Library:", "", 30, nil, func(t string) { p.configCopy.SetAudiobookshelfLibrary(t) }) p.scanAudiobookshelf = absFormLeft.AddCheckbox("Scan the Audiobookshelf library after copy/upload?", false, func(t bool) { p.configCopy.SetScanAudiobookshelf(t) }) p.absSection.AddItem(absFormLeft.Form, 0, 0, 1, 1, 0, 0, true) @@ -145,7 +154,8 @@ func newConfigPage(dispatcher *mq.Dispatcher) *ConfigPage { // screen navigation order p.mainGrid.SetNavigationOrder( - p.searchCondition, + p.defaultAuthor, + p.defaultTitle, p.rowsPerPage, p.useMockField, p.saveMockField, @@ -198,7 +208,8 @@ func (p *ConfigPage) displayConfig(c *dto.DisplayConfigCommand) { p.logFileNameField.SetText(p.configCopy.GetLogFileName()) p.logLevelField.SetCurrentOption(utils.GetIndex(logger.LogLeves(), p.configCopy.GetLogLevel())) - p.searchCondition.SetText(p.configCopy.GetSearchCondition()) + p.defaultAuthor.SetText(p.configCopy.GetDefaultAuthor()) + p.defaultTitle.SetText(p.configCopy.GetDefaultTitle()) p.rowsPerPage.SetText(utils.ToString(p.configCopy.GetRowsPerPage())) p.useMockField.SetChecked(p.configCopy.IsUseMock()) p.saveMockField.SetChecked(p.configCopy.IsSaveMock()) diff --git a/internal/ui/download_page.go b/internal/ui/download_page.go index 92bb42d..2df07a3 100644 --- a/internal/ui/download_page.go +++ b/internal/ui/download_page.go @@ -56,7 +56,7 @@ func newDownloadPage(dispatcher *mq.Dispatcher) *DownloadPage { p.filesSection.SetBorder(true) p.filesTable = newTable() - p.filesTable.setHeaders(" # ", "File name", "Format", "Duration", "Total Size", "Download progress") + p.filesTable.setHeaders(" # ", "File name", "Format", "Duration", "Size", "Download progress") p.filesTable.setWeights(1, 2, 1, 1, 1, 5) p.filesTable.setAlign(tview.AlignRight, tview.AlignLeft, tview.AlignLeft, tview.AlignRight, tview.AlignRight, tview.AlignLeft) p.filesSection.AddItem(p.filesTable.Table, 0, 0, 1, 1, 0, 0, true) @@ -166,7 +166,7 @@ func (p *DownloadPage) updateTotalProgress(dp *dto.TotalDownloadProgress) { } infoCell := p.progressTable.GetCell(0, 0) progressCell := p.progressTable.GetCell(1, 0) - infoCell.Text = fmt.Sprintf(" [yellow]Time elapsed: [white]%10s | [yellow]Downloaded: [white]%10s | [yellow]Files: [white]%10s | [yellow]Speed: [white]%12s | [yellow]ETA: [white]%10s", dp.Elapsed, dp.Bytes, dp.Files, dp.Speed, dp.ETA) + infoCell.Text = fmt.Sprintf(" [yellow]Elapsed: [white]%7s | [yellow]Done: [white]%8s | [yellow]Files: [white]%6s | [yellow]Speed: [white]%6s | [yellow]ETA: [white]%7s", dp.Elapsed, dp.Bytes, dp.Files, dp.Speed, dp.ETA) col := 0 w := p.progressTable.GetColumnWidth(col) - 5 diff --git a/internal/ui/encoding_page.go b/internal/ui/encoding_page.go index 075688d..9edf3bd 100644 --- a/internal/ui/encoding_page.go +++ b/internal/ui/encoding_page.go @@ -56,7 +56,7 @@ func newEncodingPage(dispatcher *mq.Dispatcher) *EncodingPage { p.filesSection.SetBorder(true) p.filesTable = newTable() - p.filesTable.setHeaders(" # ", "File name", "Format", "Duration", "Total Size", "Encoding progress") + p.filesTable.setHeaders(" # ", "File name", "Format", "Duration", "Size", "Encoding progress") p.filesTable.setWeights(1, 2, 1, 1, 1, 5) p.filesTable.setAlign(tview.AlignRight, tview.AlignLeft, tview.AlignLeft, tview.AlignRight, tview.AlignRight, tview.AlignLeft) p.filesSection.AddItem(p.filesTable.Table, 0, 0, 1, 1, 0, 0, true) diff --git a/internal/ui/search_page.go b/internal/ui/search_page.go index 7cc2607..f4dfe19 100644 --- a/internal/ui/search_page.go +++ b/internal/ui/search_page.go @@ -16,12 +16,13 @@ import ( type SearchPage struct { mq *mq.Dispatcher mainGrid *grid - searchCriteria string + searchCondition dto.SearchCondition isSearchRunning bool searchResult []*dto.IAItem searchSection *grid - inputField *tview.InputField + author *tview.InputField + title *tview.InputField searchButton *tview.Button clearButton *tview.Button createAudioBookButton *tview.Button @@ -40,30 +41,41 @@ func newSearchPage(dispatcher *mq.Dispatcher) *SearchPage { p.mq = dispatcher p.mq.RegisterListener(mq.SearchPage, p.dispatchMessage) - p.searchCriteria = config.Instance().GetSearchCondition() + p.searchCondition.Author = config.Instance().GetDefaultAuthor() + p.searchCondition.Title = config.Instance().GetDefaultTitle() p.mainGrid = newGrid() - p.mainGrid.SetRows(5, -1, -1) + p.mainGrid.SetRows(9, -1, -1) p.mainGrid.SetColumns(0) // search section p.searchSection = newGrid() - p.searchSection.SetColumns(-2, -1) + p.searchSection.SetColumns(50, -1) p.searchSection.SetBorder(true) p.searchSection.SetTitle(" Internet Archive Search ") p.searchSection.SetTitleAlign(tview.AlignLeft) f := newForm() - f.SetHorizontal(true) - p.inputField = f.AddInputField("Search criteria", config.Instance().GetSearchCondition(), 40, nil, func(t string) { p.searchCriteria = t }) + f.SetHorizontal(false) + p.author = f.AddInputField("Creator", config.Instance().GetDefaultAuthor(), 40, nil, func(t string) { p.searchCondition.Author = t }) + p.title = f.AddInputField("Title", config.Instance().GetDefaultTitle(), 40, nil, func(t string) { p.searchCondition.Title = t }) p.searchButton = f.AddButton("Search", p.runSearch) p.clearButton = f.AddButton("Clear", p.clearEverything) - p.searchSection.AddItem(f.Form, 0, 0, 1, 1, 0, 0, true) + f.SetButtonsAlign(tview.AlignRight) + p.searchSection.AddItem(f, 0, 0, 1, 1, 0, 0, true) + g := newGrid() + g.SetRows(-1, -1) + g.SetColumns(0) f = newForm() f.SetHorizontal(false) f.SetButtonsAlign(tview.AlignRight) p.createAudioBookButton = f.AddButton("Create Audiobook", p.createBook) + g.AddItem(f, 0, 0, 1, 1, 1, 1, true) + f = newForm() + f.SetHorizontal(false) + f.SetButtonsAlign(tview.AlignRight) + g.AddItem(f, 1, 0, 1, 1, 1, 1, true) p.SettingsButton = f.AddButton("Settings", p.updateConfig) - p.searchSection.AddItem(f.Form, 0, 1, 1, 1, 0, 0, true) + p.searchSection.AddItem(g, 0, 1, 1, 1, 0, 0, true) p.mainGrid.AddItem(p.searchSection.Grid, 0, 0, 1, 1, 0, 0, true) @@ -75,8 +87,8 @@ func newSearchPage(dispatcher *mq.Dispatcher) *SearchPage { p.resultSection.SetBorder(true) p.resultTable = newTable() - p.resultTable.setHeaders(" # ", "Author", "Title", "Files", "Duration (hh:mm:ss)", "Total Size") - p.resultTable.setWeights(1, 3, 6, 2, 1, 1) + p.resultTable.setHeaders(" # ", "Author", "Title", "Files", "Duration", "Size") + p.resultTable.setWeights(1, 3, 6, 1, 2, 2) p.resultTable.setAlign(tview.AlignRight, tview.AlignLeft, tview.AlignLeft, tview.AlignRight, tview.AlignRight, tview.AlignRight) p.resultTable.SetSelectionChangedFunc(p.updateDetails) p.resultTable.setLastRowEvent(p.lastRowEvent) @@ -101,7 +113,7 @@ func newSearchPage(dispatcher *mq.Dispatcher) *SearchPage { p.filesTable.SetTitle(" Files: ") p.filesTable.SetTitleAlign(tview.AlignLeft) p.filesTable.setHeaders("File name", "Format", "Duration", "Size") - p.filesTable.setWeights(3, 1, 1, 1) + p.filesTable.setWeights(3, 2, 2, 2) p.filesTable.setAlign(tview.AlignLeft, tview.AlignRight, tview.AlignRight, tview.AlignRight) p.detailsSection.AddItem(p.filesTable.Table, 0, 2, 1, 1, 0, 0, true) @@ -116,7 +128,8 @@ func newSearchPage(dispatcher *mq.Dispatcher) *SearchPage { // screen navigation order p.mainGrid.SetNavigationOrder( - p.inputField, + p.author, + p.title, p.searchButton, p.clearButton, p.createAudioBookButton, @@ -147,7 +160,7 @@ func (p *SearchPage) dispatchMessage(m *mq.Message) { case *dto.NothingFoundError: p.showNothingFoundError(dto) case *dto.LastPageMessage: - p.showLastPageMessage(dto) + p.showLastPageMessage(dto) case *dto.NewAppVersionFound: p.showNewVersionMessage(dto) case *dto.FFMPEGNotFoundError: @@ -164,7 +177,7 @@ func (p *SearchPage) runSearch() { p.isSearchRunning = true p.clearSearchResults() p.resultTable.showHeader() - p.mq.SendMessage(mq.SearchPage, mq.SearchController, &dto.SearchCommand{SearchCondition: p.searchCriteria}, false) + p.mq.SendMessage(mq.SearchPage, mq.SearchController, &dto.SearchCommand{Condition: p.searchCondition}, false) p.mq.SendMessage(mq.SearchPage, mq.TUI, &dto.SetFocusCommand{Primitive: p.resultTable.Table}, true) } @@ -177,7 +190,8 @@ func (p *SearchPage) clearSearchResults() { } func (p *SearchPage) clearEverything() { - p.inputField.SetText("") + p.author.SetText("") + p.title.SetText("") p.clearSearchResults() } @@ -186,7 +200,7 @@ func (p *SearchPage) lastRowEvent() { return } p.isSearchRunning = true - p.mq.SendMessage(mq.SearchPage, mq.SearchController, &dto.GetNextPageCommand{SearchCondition: p.searchCriteria}, false) + p.mq.SendMessage(mq.SearchPage, mq.SearchController, &dto.GetNextPageCommand{Condition: p.searchCondition}, false) } func (p *SearchPage) updateResult(i *dto.IAItem) { @@ -265,7 +279,7 @@ func (p *SearchPage) updateConfig() { func (p *SearchPage) showNothingFoundError(dto *dto.NothingFoundError) { newMessageDialog(p.mq, "Error", - "No results were found for your search term: [darkblue]'"+dto.SearchCondition+"'[black].\n"+ + "No results were found for your search term: [darkblue]'"+dto.Condition.Author+" - "+dto.Condition.Title+"'[black].\n"+ "Please revise your search criteria.", p.searchSection.Grid, func() {}) } @@ -273,8 +287,8 @@ func (p *SearchPage) showNothingFoundError(dto *dto.NothingFoundError) { func (p *SearchPage) showLastPageMessage(dto *dto.LastPageMessage) { newMessageDialog(p.mq, "Notification", "No more items were found for \n"+ - "your search term: [darkblue]'"+dto.SearchCondition+"'[black].\n"+ - "This is the last page.\n", + "your search term: [darkblue]'"+dto.Condition.Author+" - "+dto.Condition.Title+"'[black]\n"+ + "This is the last page.\n", p.resultSection.Grid, func() {}) } diff --git a/internal/ui/wrappers.go b/internal/ui/wrappers.go index ccf69c0..44de85f 100644 --- a/internal/ui/wrappers.go +++ b/internal/ui/wrappers.go @@ -292,7 +292,7 @@ func (p *infoPanel) appendRow(label string, value string) { labelCell.SetTextColor(yellow) p.SetCell(row, 0, labelCell) // value - valueCell := tview.NewTableCell(" " + value) + valueCell := tview.NewTableCell(" [white]" + value) p.SetCell(row, 1, valueCell) } diff --git a/main.go b/main.go index f038fd4..bedf9d2 100644 --- a/main.go +++ b/main.go @@ -3,6 +3,7 @@ package main import ( "flag" "os" + "strings" "abb_ia/cmd" "abb_ia/internal/config" @@ -32,7 +33,11 @@ func main() { // save runtime configuration if searchCondition != "" { - config.Instance().SetSearchCondition(searchCondition) + condition := strings.Split(searchCondition, " - ") + if len(condition) == 2 { + config.Instance().SetDefaultAuthor(condition[0]) + config.Instance().SetDefaultTitle(condition[1]) + } } if utils.IsFlagPassed("log-level") { config.Instance().SetLogLevel(*logLevel)