Skip to content

Commit

Permalink
Description Search/Replace implemented
Browse files Browse the repository at this point in the history
  • Loading branch information
vpoluyaktov committed Nov 2, 2023
1 parent da92be6 commit 227d402
Show file tree
Hide file tree
Showing 13 changed files with 302 additions and 160 deletions.
2 changes: 0 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,7 @@ Since the copyrights for the majority of old-time radio shows have expired and m


## TODO:
- Implement Search/Replace for Chapter name on Chapters page
- Implement Search/Replace for Description on Chapters page
- Implement bytes to human conversion in the config
- Finish Default Settings screen
- Create an audiobook Settings screen

13 changes: 10 additions & 3 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import (
"io/ioutil"
"time"

"github.com/vpoluyaktov/abb_ia/internal/logger"
"github.com/vpoluyaktov/abb_ia/internal/utils"
"gopkg.in/yaml.v3"
)

Expand Down Expand Up @@ -32,7 +34,7 @@ type Config struct {
ReEncodeFiles bool
BitRate string
SampleRate string
MaxFileSize int64
MaxFileSize string
CopyToAudiobookshelf bool
AudiobookshelfDir string
ShortenTitles bool
Expand All @@ -55,7 +57,7 @@ func Load() {
config.ReEncodeFiles = true
config.BitRate = "128k"
config.SampleRate = "44100"
config.MaxFileSize = 1024 * 1024 * 10
config.MaxFileSize = "100 Mb"
config.CopyToAudiobookshelf = true
config.AudiobookshelfDir = "/mnt/NAS/Audiobooks/Internet Archive"
config.ShortenTitles = true
Expand Down Expand Up @@ -203,7 +205,12 @@ func SampleRate() string {
}

func MaxFileSize() int64 {
return configInstance.MaxFileSize
maxFileSize, err := utils.HumanToBytes(configInstance.MaxFileSize)
if err != nil {
logger.Error("Config Loader Can't parse MaxFileSize: " + err.Error() + ". Using default 100 Mb")
maxFileSize, _ = utils.HumanToBytes("100 Mb")
}
return maxFileSize
}

func SetCopyToAudiobookshelf(b bool) {
Expand Down
12 changes: 12 additions & 0 deletions internal/controller/chaptersController.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ func (c *ChaptersController) dispatchMessage(m *mq.Message) {
switch dto := m.Dto.(type) {
case *dto.ChaptersCreate:
go c.createChapters(dto)
case *dto.SearchReplaceDescriptionCommand:
go c.searchReplaceDescription(dto)
case *dto.SearchReplaceChaptersCommand:
go c.searchReplaceChapters(dto)
case *dto.JoinChaptersCommand:
Expand Down Expand Up @@ -124,6 +126,16 @@ func (c *ChaptersController) createChapters(cmd *dto.ChaptersCreate) {
c.mq.SendMessage(mq.ChaptersController, mq.ChaptersPage, &dto.ChaptersReady{Audiobook: cmd.Audiobook}, true)
}

func (c *ChaptersController) searchReplaceDescription(cmd *dto.SearchReplaceDescriptionCommand) {
ab := cmd.Audiobook
searchStr := cmd.SearchStr
replaceStr := cmd.ReplaceStr
re := regexp.MustCompile(searchStr)
description := re.ReplaceAllString(ab.Description, replaceStr)
ab.Description = description
c.mq.SendMessage(mq.ChaptersController, mq.ChaptersPage, &dto.RefreshDescriptionCommand{Audiobook: cmd.Audiobook}, true)
}

func (c *ChaptersController) searchReplaceChapters(cmd *dto.SearchReplaceChaptersCommand) {
ab := cmd.Audiobook
searchStr := cmd.SearchStr
Expand Down
9 changes: 5 additions & 4 deletions internal/controller/searchController.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,9 +81,7 @@ func (c *SearchController) performSearch(cmd *dto.SearchCommand) {
if len(d.Metadata.Description) > 0 {
item.Description = tview.Escape(ia.Html2Text(d.Metadata.Description[0]))
}
// if len(d.Misc.Image) > 0 { // _thumb images are too small. Have to collect and sort my size all item images below
// item.Cover = d.Misc.Image
// }

for name, metadata := range d.Files {
format := metadata.Format
// collect mp3 files
Expand Down Expand Up @@ -121,7 +119,10 @@ func (c *SearchController) performSearch(cmd *dto.SearchCommand) {
item.TotalSize = totalSize
item.TotalLength = totalLength

// find biggest image by size
// if len(d.Misc.Image) > 0 { // _thumb images are too small. Have to collect and sort my size all item images below
// item.CoverUrl = d.Misc.Image
// }
// find biggest image by size (TODO: Need to find better solution. Maybe analyze if the image is colorful?)
if len(item.ImageFiles) > 0 {
biggestImage := item.ImageFiles[0]
for i := 1; i < len(item.ImageFiles); i++ {
Expand Down
20 changes: 19 additions & 1 deletion internal/dto/chapters.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,24 @@ func (c *ChaptersReady) String() string {
return fmt.Sprintf("ChaptersReady: %s", c.Audiobook.String())
}

type SearchReplaceDescriptionCommand struct {
Audiobook *Audiobook
SearchStr string
ReplaceStr string
}

func (c *SearchReplaceDescriptionCommand) String() string {
return fmt.Sprintf("SearchReplaceDescriptionCommand: %s/%s", c.SearchStr, c.SearchStr)
}

type RefreshDescriptionCommand struct {
Audiobook *Audiobook
}

func (c *RefreshDescriptionCommand) String() string {
return fmt.Sprintf("RefreshDescriptionCommand: %s", c.Audiobook.String())
}

type SearchReplaceChaptersCommand struct {
Audiobook *Audiobook
SearchStr string
Expand All @@ -58,4 +76,4 @@ type RefreshChaptersCommand struct {

func (c *RefreshChaptersCommand) String() string {
return fmt.Sprintf("RefreshChaptersCommand: %s", c.Audiobook.String())
}
}
29 changes: 10 additions & 19 deletions internal/mq/dispatcher.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,19 +10,17 @@ import (
"github.com/vpoluyaktov/abb_ia/internal/logger"
)



/**
* Dispatcher is a struct that provides a mechanism for dispatching messages
* to multiple recipients.
*
*
* @param mu - A mutex to ensure thread safety.
* @param recipients - A map of strings to message queues.
* @param listeners - A map of strings to callback functions.
*
*
* @returns Dispatcher - A struct that provides a mechanism for dispatching
* messages to multiple recipients.
*
*
* This code is useful for dispatching messages to multiple recipients in a
* thread-safe manner. The mutex ensures that only one thread can access the
* data at a time, while the maps provide a way to store and access the
Expand All @@ -43,13 +41,11 @@ const PullFrequency = 10 * time.Millisecond

type CallBackFunc func(*Message)



/**
* Creates a new Dispatcher instance.
*
*
* @returns A new Dispatcher instance.
*
*
* This function is useful for creating a new Dispatcher instance which is used to
* manage message queues and callbacks. The instance is initialized with empty
* maps for recipients and listeners.
Expand All @@ -61,14 +57,13 @@ func NewDispatcher() *Dispatcher {
return d
}


/**
* Sends a message to a recipient.
* @param from The sender of the message.
* @param to The recipient of the message.
* @param dto The data transfer object (DTO) to be sent.
* @param async Whether the message should be sent asynchronously.
*
*
* This function is useful for sending messages between different components of an application. If the message is sent asynchronously, it is pushed to a queue and the recipient will receive it when they are ready. If the message is sent synchronously, the recipient's method is called in blocking mode.
*/
func (d *Dispatcher) SendMessage(from string, to string, dto dto.Dto, async bool) {
Expand All @@ -84,8 +79,8 @@ func (d *Dispatcher) SendMessage(from string, to string, dto dto.Dto, async bool
if _, ok := d.recipients[m.To]; !ok {
d.recipients[m.To] = messageQueue{list.New()}
}
d.mu.Lock()
// 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())
Expand All @@ -99,8 +94,6 @@ func (d *Dispatcher) SendMessage(from string, to string, dto dto.Dto, async bool
}
}



/**
* Retrieves a message from the Dispatcher for the given recipient.
*
Expand All @@ -111,26 +104,24 @@ func (d *Dispatcher) SendMessage(from string, to string, dto dto.Dto, async bool
*/
func (d *Dispatcher) GetMessage(recipient string) *Message {
var m *Message
d.mu.Lock()
if _, ok := d.recipients[recipient]; ok {
d.mu.Lock()
e := d.recipients[recipient].messages.Front()
if e != nil {
d.recipients[recipient].messages.Remove(e)
m = e.Value.(*Message)
logger.Debug("MQ async --> " + m.String())
}
d.mu.Unlock()
}
d.mu.Unlock()
return m
}



/**
* Checks if a given message exists in the list of recipients.
* @param m - The message to check for.
* @returns A boolean indicating if the message exists.
*
*
* This function is useful for checking if a given message exists in the list of recipients. It iterates through the list of recipients and checks if the given message is equal to any of the messages in the list. If it is, it returns true, otherwise it returns false.
*/
func (d *Dispatcher) messageExists(m *Message) bool {
Expand Down
86 changes: 58 additions & 28 deletions internal/ui/chaptersPage.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,24 +16,25 @@ import (
)

type ChaptersPage struct {
mq *mq.Dispatcher
grid *tview.Grid
author *tview.InputField
title *tview.InputField
series *tview.InputField
seriesNo *tview.InputField
genre *tview.DropDown
narator *tview.InputField
cover *tview.InputField
descriptionEditor *tview.TextArea
chaptersSection *tview.Grid
chaptersTable *table
ab *dto.Audiobook
searchDescription string
replaceDescription string
searchChapters string
replaceChapters string
chaptersUndoStack *UndoStack
mq *mq.Dispatcher
grid *tview.Grid
author *tview.InputField
title *tview.InputField
series *tview.InputField
seriesNo *tview.InputField
genre *tview.DropDown
narator *tview.InputField
cover *tview.InputField
descriptionEditor *tview.TextArea
chaptersSection *tview.Grid
chaptersTable *table
ab *dto.Audiobook
searchDescription string
replaceDescription string
searchChapters string
replaceChapters string
chaptersUndoStack *UndoStack
descriptionUndoStack *UndoStack
}

func newChaptersPage(dispatcher *mq.Dispatcher) *ChaptersPage {
Expand Down Expand Up @@ -131,8 +132,8 @@ func newChaptersPage(dispatcher *mq.Dispatcher) *ChaptersPage {

f5.AddInputField("Search: ", "", 30, nil, func(s string) { p.searchDescription = s })
f5.AddInputField("Replace:", "", 30, nil, func(s string) { p.replaceDescription = s })
f5.AddButton("Replace", p.buildBook)
f5.AddButton(" Undo ", p.stopConfirmation)
f5.AddButton("Replace", p.searchReplaceDescription)
f5.AddButton(" Undo ", p.undoDescription)
f5.SetButtonsAlign(tview.AlignRight)
descriptionSection.AddItem(f5.f, 0, 1, 1, 1, 0, 0, true)
p.grid.AddItem(descriptionSection, 1, 0, 1, 1, 0, 0, true)
Expand Down Expand Up @@ -164,6 +165,7 @@ func newChaptersPage(dispatcher *mq.Dispatcher) *ChaptersPage {
p.grid.AddItem(p.chaptersSection, 2, 0, 1, 1, 0, 0, true)

p.chaptersUndoStack = NewUndoStack()
p.descriptionUndoStack = NewUndoStack()

return p
}
Expand All @@ -185,6 +187,8 @@ func (p *ChaptersPage) dispatchMessage(m *mq.Message) {
p.addPart(dto.Part)
case *dto.ChaptersReady:
p.displayParts(dto.Audiobook)
case *dto.RefreshDescriptionCommand:
p.refreshDescription(dto.Audiobook)
case *dto.RefreshChaptersCommand:
p.refreshChapters(dto.Audiobook)
default:
Expand All @@ -197,7 +201,7 @@ func (p *ChaptersPage) displayBookInfo(ab *dto.Audiobook) {
p.author.SetText(ab.Author)
p.title.SetText(ab.Title)
p.cover.SetText(ab.CoverURL)
p.descriptionEditor.SetText(ab.IAItem.Description, false)
p.descriptionEditor.SetText(ab.Description, false)

p.chaptersTable.clear()
p.chaptersTable.showHeader()
Expand Down Expand Up @@ -263,14 +267,40 @@ func (p *ChaptersPage) updateChapterEntry(row int, col int) {
d.Show()
}

func (p *ChaptersPage) searchReplaceDescription() {
if p.searchDescription != "" {
abCopy, err := p.ab.GetCopy()
if err != nil {
logger.Error("Can't create a copy of Audiobook struct: " + err.Error())
} else {
p.descriptionUndoStack.Push(abCopy)
p.mq.SendMessage(mq.ChaptersPage, mq.ChaptersController, &dto.SearchReplaceDescriptionCommand{Audiobook: p.ab, SearchStr: p.searchDescription, ReplaceStr: p.replaceDescription}, true)
}
}
}

func (p *ChaptersPage) undoDescription() {
ab, err := p.descriptionUndoStack.Pop()
if err == nil {
p.ab.Description = ab.Description
p.descriptionEditor.SetText(p.ab.Description, false)
}
p.mq.SendMessage(mq.ChaptersPage, mq.TUI, &dto.DrawCommand{Primitive: nil}, true)
}

func (p *ChaptersPage) refreshDescription(ab *dto.Audiobook) {
p.descriptionEditor.SetText(ab.Description, false)
p.mq.SendMessage(mq.ChaptersPage, mq.TUI, &dto.DrawCommand{Primitive: nil}, true)
}

func (p *ChaptersPage) searchReplaceChapters() {
if p.searchChapters != "" {
abCopy, err := p.ab.GetCopy()
if err != nil {
logger.Error("Can't create a copy of Audiobook struct: " + err.Error())
} else {
p.chaptersUndoStack.Push(abCopy)
p.mq.SendMessage(mq.ChaptersPage, mq.ChaptersController, &dto.SearchReplaceChaptersCommand{Audiobook: p.ab, SearchStr: p.searchChapters, ReplaceStr: p.replaceChapters}, false)
p.mq.SendMessage(mq.ChaptersPage, mq.ChaptersController, &dto.SearchReplaceChaptersCommand{Audiobook: p.ab, SearchStr: p.searchChapters, ReplaceStr: p.replaceChapters}, true)
}
}
}
Expand All @@ -281,7 +311,7 @@ func (p *ChaptersPage) joinChapters() {
logger.Error("Can't create a copy of Audiobook struct: " + err.Error())
} else {
p.chaptersUndoStack.Push(abCopy)
p.mq.SendMessage(mq.ChaptersPage, mq.ChaptersController, &dto.JoinChaptersCommand{Audiobook: p.ab}, false)
p.mq.SendMessage(mq.ChaptersPage, mq.ChaptersController, &dto.JoinChaptersCommand{Audiobook: p.ab}, true)
}
}

Expand All @@ -293,8 +323,8 @@ func (p *ChaptersPage) undoChapters() {
}
}

func (c *ChaptersPage) refreshChapters(ab *dto.Audiobook) {
go c.displayParts(ab)
func (p *ChaptersPage) refreshChapters(ab *dto.Audiobook) {
go p.displayParts(ab)
}

func (p *ChaptersPage) stopConfirmation() {
Expand All @@ -303,13 +333,13 @@ func (p *ChaptersPage) stopConfirmation() {

func (p *ChaptersPage) stopChapters() {
// Stop the chapters here
p.mq.SendMessage(mq.ChaptersPage, mq.ChaptersController, &dto.StopCommand{Process: "Chapters", Reason: "User request"}, false)
p.mq.SendMessage(mq.ChaptersPage, mq.Frame, &dto.SwitchToPageCommand{Name: "SearchPage"}, false)
p.mq.SendMessage(mq.ChaptersPage, mq.ChaptersController, &dto.StopCommand{Process: "Chapters", Reason: "User request"}, true)
p.mq.SendMessage(mq.ChaptersPage, mq.Frame, &dto.SwitchToPageCommand{Name: "SearchPage"}, true)
}

func (p *ChaptersPage) buildBook() {
p.mq.SendMessage(mq.ChaptersPage, mq.BuildController, &dto.BuildCommand{Audiobook: p.ab}, true)
p.mq.SendMessage(mq.ChaptersPage, mq.Frame, &dto.SwitchToPageCommand{Name: "BuildPage"}, false)
p.mq.SendMessage(mq.ChaptersPage, mq.Frame, &dto.SwitchToPageCommand{Name: "BuildPage"}, true)
}

// Simple Undo stack implementation
Expand Down
Loading

0 comments on commit 227d402

Please sign in to comment.