From 27388f23e60e86a4c49a29570ce55c1fdd972941 Mon Sep 17 00:00:00 2001 From: vpoluyaktov Date: Wed, 15 Nov 2023 12:01:42 -0800 Subject: [PATCH 1/8] Small UI improvements --- internal/config/config.go | 56 +++++++++++++---------- internal/controller/chaptersController.go | 8 ++-- internal/dto/audiobook.go | 1 + internal/ffmpeg/ffmpeg.go | 4 ++ internal/ui/buildPage.go | 39 +++++++--------- 5 files changed, 60 insertions(+), 48 deletions(-) diff --git a/internal/config/config.go b/internal/config/config.go index 5980818..00d7bae 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -25,28 +25,34 @@ 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 { - LogFileName string `yaml:"LogFileName"` - OutputDir string `yaml:"OutputDir"` - LogLevel string `yaml:"LogLevel"` - SearchRowsMax int `yaml:"SearchRowsMax"` - UseMock bool `yaml:"UseMock"` - SaveMock bool `yaml:"SaveMock"` - SearchCondition string `yaml:"SearchCondition"` - ConcurrentDownloaders int `yaml:"ConcurrentDownloaders"` - ConcurrentEncoders int `yaml:"ConcurrentEncoders"` - ReEncodeFiles bool `yaml:"ReEncodeFiles"` - BasePortNumber int `yaml:"BasePortNumber"` - BitRateKbs int `yaml:"BitRateKbs"` - SampleRateHz int `yaml:"SampleRateHz"` - MaxFileSizeMb int `yaml:"MaxFileSizeMb"` - CopyToAudiobookshelf bool `yaml:"CopyToAudiobookshelf"` - AudiobookshelfUrl string `yaml:"AudiobookshelfUrl"` - AudiobookshelfUser string `yaml:"AudiobookshelfUser"` - AudiobookshelfPassword string `yaml:"AudiobookshelfPassword"` - AudiobookshelfLibrary string `yaml:"AudiobookshelfLibrary"` - AudiobookshelfDir string `yaml:"AudiobookshelfDir"` - ShortenTitles bool `yaml:"ShortenTitles"` - Genres []string `yaml:"Genres"` + LogFileName string `yaml:"LogFileName"` + OutputDir string `yaml:"OutputDir"` + LogLevel string `yaml:"LogLevel"` + SearchRowsMax int `yaml:"SearchRowsMax"` + UseMock bool `yaml:"UseMock"` + SaveMock bool `yaml:"SaveMock"` + SearchCondition string `yaml:"SearchCondition"` + ConcurrentDownloaders int `yaml:"ConcurrentDownloaders"` + ConcurrentEncoders int `yaml:"ConcurrentEncoders"` + ReEncodeFiles bool `yaml:"ReEncodeFiles"` + BasePortNumber int `yaml:"BasePortNumber"` + BitRateKbs int `yaml:"BitRateKbs"` + SampleRateHz int `yaml:"SampleRateHz"` + MaxFileSizeMb int `yaml:"MaxFileSizeMb"` + CopyToAudiobookshelf bool `yaml:"CopyToAudiobookshelf"` + AudiobookshelfUrl string `yaml:"AudiobookshelfUrl"` + AudiobookshelfUser string `yaml:"AudiobookshelfUser"` + AudiobookshelfPassword string `yaml:"AudiobookshelfPassword"` + AudiobookshelfLibrary string `yaml:"AudiobookshelfLibrary"` + AudiobookshelfDir string `yaml:"AudiobookshelfDir"` + ShortenTitles bool `yaml:"ShortenTitles"` + ShortenPairs []ShortenPair `yaml:"ShortenPairs"` + Genres []string `yaml:"Genres"` +} + +type ShortenPair struct { + Search string `yaml:"Search"` + Replace string `yaml:"Replace"` } func Instance() *Config { @@ -71,7 +77,7 @@ func Load() { config.ConcurrentEncoders = 5 config.ReEncodeFiles = true config.BasePortNumber = 31000 - config.BitRateKbs = 96 + config.BitRateKbs = 128 config.SampleRateHz = 44100 config.MaxFileSizeMb = 250 config.CopyToAudiobookshelf = true @@ -80,6 +86,10 @@ func Load() { config.AudiobookshelfLibrary = "Internet Archive" config.AudiobookshelfDir = "/mnt/NAS/Audiobooks/Internet Archive" config.ShortenTitles = true + config.ShortenPairs = []ShortenPair{ + {"Old Time Radio Researchers Group", "OTRR"}, + {" - Single Episodes", ""}, + } config.Genres = []string{ "Audiobook", "Fiction", diff --git a/internal/controller/chaptersController.go b/internal/controller/chaptersController.go index 987f8ce..381d842 100644 --- a/internal/controller/chaptersController.go +++ b/internal/controller/chaptersController.go @@ -76,8 +76,10 @@ func (c *ChaptersController) createChapters(cmd *dto.ChaptersCreate) { c.ab = cmd.Audiobook if c.ab.Config.IsShortenTitle() { - c.ab.Title = strings.ReplaceAll(c.ab.Title, " - Single Episodes", "") - c.ab.Author = strings.ReplaceAll(c.ab.Author, "Old Time Radio Researchers Group", "OTRR") + for _, pair := range c.ab.Config.ShortenPairs { + c.ab.Title = strings.ReplaceAll(c.ab.Title, pair.Search, pair.Replace) + c.ab.Author = strings.ReplaceAll(c.ab.Author, pair.Search, pair.Replace) + } } c.mq.SendMessage(mq.ChaptersController, mq.ChaptersPage, &dto.DisplayBookInfoCommand{Audiobook: c.ab}, true) @@ -109,7 +111,7 @@ func (c *ChaptersController) createChapters(cmd *dto.ChaptersCreate) { chapterNo++ chapterFiles = []dto.Mp3File{} if partSize >= int64(c.ab.Config.GetMaxFileSizeMb())*1024*1024 || i == len(c.ab.Mp3Files)-1 { - part := dto.Part{Number: partNo, Size: partSize, Duration: partDuration, Chapters: partChapters} + part := dto.Part{Number: partNo, Format: "VBR MP3", Size: partSize, Duration: partDuration, Chapters: partChapters} c.ab.Parts = append(c.ab.Parts, part) partNo++ fileNo = 1 diff --git a/internal/dto/audiobook.go b/internal/dto/audiobook.go index 2adae42..7a5257f 100644 --- a/internal/dto/audiobook.go +++ b/internal/dto/audiobook.go @@ -34,6 +34,7 @@ type Part struct { M4BFile string FListFile string MetadataFile string + Format string Size int64 Duration float64 Chapters []Chapter diff --git a/internal/ffmpeg/ffmpeg.go b/internal/ffmpeg/ffmpeg.go index b3a3130..09c6111 100644 --- a/internal/ffmpeg/ffmpeg.go +++ b/internal/ffmpeg/ffmpeg.go @@ -1,6 +1,7 @@ package ffmpeg import ( + "os" "os/exec" "github.com/vpoluyaktov/abb_ia/internal/logger" @@ -10,6 +11,7 @@ type FFmpeg struct { input input output output params params + process *os.Process } type input struct { @@ -31,6 +33,7 @@ func NewFFmpeg() *FFmpeg { input: input{}, output: output{}, params: params{}, + process: nil, } return ffmpeg } @@ -75,6 +78,7 @@ func (f *FFmpeg) Run() (string, *exitErr) { args = args.AppendArgs(f.output.args).AppendFileName(f.output.fileName) command := exec.Command(cmd, args.String()...) logger.Debug("FFMPEG cmd: " + command.String()) + f.process = command.Process out, err := command.Output() if err != nil { return string(out), ExitErr(err) diff --git a/internal/ui/buildPage.go b/internal/ui/buildPage.go index abe1753..c06efae 100644 --- a/internal/ui/buildPage.go +++ b/internal/ui/buildPage.go @@ -66,9 +66,9 @@ func newBuildPage(dispatcher *mq.Dispatcher) *BuildPage { p.buildSection.SetBorder(true) p.buildTable = newTable() - p.buildTable.setHeaders(" Part # ", "File name", "Duration", "Total Size", "Build progress") - p.buildTable.setWeights(1, 2, 1, 1, 5) - p.buildTable.setAlign(tview.AlignRight, tview.AlignLeft, tview.AlignRight, tview.AlignRight, tview.AlignLeft) + p.buildTable.setHeaders(" # ", "File name", "Format", "Duration", "Total 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.t, 0, 0, 1, 1, 0, 0, true) p.grid.AddItem(p.buildSection, 1, 0, 1, 1, 0, 0, true) @@ -80,9 +80,9 @@ func newBuildPage(dispatcher *mq.Dispatcher) *BuildPage { p.copySection.SetBorder(true) p.copyTable = newTable() - p.copyTable.setHeaders(" Part # ", "File name", "Duration", "Total Size", "Copy progress") - p.copyTable.setWeights(1, 2, 1, 1, 5) - p.copyTable.setAlign(tview.AlignRight, tview.AlignLeft, tview.AlignRight, tview.AlignRight, tview.AlignLeft) + p.copyTable.setHeaders(" # ", "File name", "Format", "Duration", "Total 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.t, 0, 0, 1, 1, 0, 0, true) p.grid.AddItem(p.copySection, 2, 0, 1, 1, 0, 0, true) @@ -143,9 +143,7 @@ func (p *BuildPage) displayBookInfo(ab *dto.Audiobook) { p.buildTable.clear() p.buildTable.showHeader() for i, part := range ab.Parts { - durationH := utils.SecondsToTime(part.Duration) - sizeH := utils.BytesToHuman(part.Size) - p.buildTable.appendRow(" "+strconv.Itoa(i+1)+" ", filepath.Base(part.M4BFile), durationH, sizeH, "") + p.buildTable.appendRow(" "+strconv.Itoa(i+1)+" ", filepath.Base(part.M4BFile), part.Format, utils.SecondsToTime(part.Duration), utils.BytesToHuman(part.Size), "") } p.buildTable.ScrollToBeginning() @@ -153,13 +151,10 @@ func (p *BuildPage) displayBookInfo(ab *dto.Audiobook) { p.copyTable.clear() p.copyTable.showHeader() for i, part := range ab.Parts { - durationH := utils.SecondsToTime(part.Duration) - sizeH := utils.BytesToHuman(part.Size) - p.copyTable.appendRow(" "+strconv.Itoa(i+1)+" ", filepath.Base(part.M4BFile), durationH, sizeH, "") + p.copyTable.appendRow(" "+strconv.Itoa(i+1)+" ", filepath.Base(part.M4BFile), part.Format, utils.SecondsToTime(part.Duration), utils.BytesToHuman(part.Size), "") } p.copyTable.ScrollToBeginning() } - p.mq.SendMessage(mq.BuildPage, mq.TUI, &dto.SetFocusCommand{Primitive: p.buildTable.t}, true) p.mq.SendMessage(mq.BuildPage, mq.TUI, &dto.DrawCommand{Primitive: nil}, true) } @@ -176,14 +171,14 @@ func (p *BuildPage) stopBuild() { func (p *BuildPage) updateFileBuildProgress(dp *dto.BuildFileProgress) { // update file progress - col := 4 - w := p.buildTable.getColumnWidth(col) - 5 + col := 5 + w := p.buildTable.getColumnWidth(col) - 4 progressText := fmt.Sprintf(" %3d%% ", dp.Percent) barWidth := int((float32((w - len(progressText))) * float32(dp.Percent) / 100)) progressBar := strings.Repeat("━", barWidth) + strings.Repeat(" ", w-len(progressText)-barWidth) cell := p.buildTable.t.GetCell(dp.FileId+1, col) - cell.SetExpansion(0) - cell.SetMaxWidth(45) + // cell.SetExpansion(0) + // cell.SetMaxWidth(50) cell.Text = fmt.Sprintf("%s |%s|", progressText, progressBar) // p.buildTable.t.Select(dp.FileId+1, col) p.mq.SendMessage(mq.BuildPage, mq.TUI, &dto.DrawCommand{Primitive: nil}, false) @@ -200,7 +195,7 @@ func (p *BuildPage) updateTotalBuildProgress(dp *dto.BuildProgress) { infoCell.Text = fmt.Sprintf(" [yellow]Time elapsed: [white]%10s | [yellow]Files: [white]%10s | [yellow]Speed: [white]%10s | [yellow]ETA: [white]%10s", dp.Elapsed, dp.Files, dp.Speed, dp.ETA) col := 0 - w := p.progressTable.getColumnWidth(col) - 5 + w := p.progressTable.getColumnWidth(col) - 4 progressText := fmt.Sprintf(" %3d%% ", dp.Percent) barWidth := int((float32((w - len(progressText))) * float32(dp.Percent) / 100)) progressBar := strings.Repeat("▒", barWidth) + strings.Repeat(" ", w-len(progressText)-barWidth) @@ -212,14 +207,14 @@ func (p *BuildPage) updateTotalBuildProgress(dp *dto.BuildProgress) { func (p *BuildPage) updateFileCopyProgress(dp *dto.CopyFileProgress) { // update file progress - col := 4 - w := p.copyTable.getColumnWidth(col) - 5 + col := 5 + w := p.copyTable.getColumnWidth(col) - 3 progressText := fmt.Sprintf(" %3d%% ", dp.Percent) barWidth := int((float32((w - len(progressText))) * float32(dp.Percent) / 100)) progressBar := strings.Repeat("━", barWidth) + strings.Repeat(" ", w-len(progressText)-barWidth) cell := p.copyTable.t.GetCell(dp.FileId+1, col) - cell.SetExpansion(0) - cell.SetMaxWidth(45) + // cell.SetExpansion(0) + // cell.SetMaxWidth(50) cell.Text = fmt.Sprintf("%s |%s|", progressText, progressBar) // p.copyTable.t.Select(dp.FileId+1, col) p.mq.SendMessage(mq.BuildPage, mq.TUI, &dto.DrawCommand{Primitive: nil}, false) From 46f91229a8b753429cab452dcf163b47a766fc0b Mon Sep 17 00:00:00 2001 From: vpoluyaktov Date: Fri, 17 Nov 2023 12:47:51 -0800 Subject: [PATCH 2/8] Encoding force stop feature implemented --- internal/controller/buildController.go | 23 +++++++++++++++++------ internal/controller/chaptersController.go | 5 ++++- internal/controller/copyController.go | 6 ++++-- internal/controller/downloadController.go | 9 ++++----- internal/controller/encodingController.go | 23 +++++++++++++++++------ internal/ffmpeg/ffmpeg.go | 20 +++++++++++++------- internal/ui/chaptersPage.go | 1 - internal/ui/downloadPage.go | 1 - internal/ui/encodingPage.go | 5 ++--- 9 files changed, 61 insertions(+), 32 deletions(-) diff --git a/internal/controller/buildController.go b/internal/controller/buildController.go index 0df8926..3525bfa 100644 --- a/internal/controller/buildController.go +++ b/internal/controller/buildController.go @@ -107,10 +107,12 @@ func (c *BuildController) startBuild(cmd *dto.BuildCommand) { go c.updateTotalProgress() jd.Start() - c.stopFlag = true c.mq.SendMessage(mq.BuildController, mq.Footer, &dto.SetBusyIndicator{Busy: false}, false) c.mq.SendMessage(mq.BuildController, mq.Footer, &dto.UpdateStatus{Message: ""}, false) - c.mq.SendMessage(mq.BuildController, mq.BuildPage, &dto.BuildComplete{Audiobook: cmd.Audiobook}, true) + if !c.stopFlag { + c.mq.SendMessage(mq.BuildController, mq.BuildPage, &dto.BuildComplete{Audiobook: cmd.Audiobook}, true) + } + c.stopFlag = true } func (c *BuildController) createFilesLists(ab *dto.Audiobook) { @@ -218,21 +220,30 @@ func (c *BuildController) buildAudiobookPart(ab *dto.Audiobook, partId int) { logger.Error("FFMPEG Error: " + string(err.Error())) } else { // add Metadata, cover image and convert to .m4b - _, err := ffmpeg.NewFFmpeg(). + ffmpeg := ffmpeg.NewFFmpeg(). Input(part.AACFile, ""). Input(part.MetadataFile, ""). Input(ab.CoverURL, ""). Output(part.M4BFile, "-map_metadata 1 -y -acodec copy -y -vf pad='width=ceil(iw/2)*2:height=ceil(ih/2)*2'"). Overwrite(true). Params("-hide_banner -nostdin -nostats"). - SendProgressTo("http://127.0.0.1:" + strconv.Itoa(port)). - Run() - if err != nil { + SendProgressTo("http://127.0.0.1:" + strconv.Itoa(port)) + + go c.killSwitch(ffmpeg) + _, err := ffmpeg.Run() + if err != nil && ! c.stopFlag { logger.Error("FFMPEG Error: " + string(err.Error())) } } } +func (c *BuildController) killSwitch(ffmpeg *ffmpeg.FFmpeg) { + for !c.stopFlag { + time.Sleep(mq.PullFrequency) + } + ffmpeg.Kill() +} + func (c *BuildController) startProgressListener(fileId int) (net.Listener, int) { basePortNumber := 31000 diff --git a/internal/controller/chaptersController.go b/internal/controller/chaptersController.go index 381d842..f6d7ee3 100644 --- a/internal/controller/chaptersController.go +++ b/internal/controller/chaptersController.go @@ -124,7 +124,10 @@ func (c *ChaptersController) createChapters(cmd *dto.ChaptersCreate) { c.mq.SendMessage(mq.ChaptersController, mq.Footer, &dto.SetBusyIndicator{Busy: false}, false) c.mq.SendMessage(mq.ChaptersController, mq.Footer, &dto.UpdateStatus{Message: ""}, false) - c.mq.SendMessage(mq.ChaptersController, mq.ChaptersPage, &dto.ChaptersReady{Audiobook: cmd.Audiobook}, true) + if !c.stopFlag { + c.mq.SendMessage(mq.ChaptersController, mq.ChaptersPage, &dto.ChaptersReady{Audiobook: cmd.Audiobook}, true) + } + c.stopFlag = true } func (c *ChaptersController) searchReplaceDescription(cmd *dto.SearchReplaceDescriptionCommand) { diff --git a/internal/controller/copyController.go b/internal/controller/copyController.go index 849aa06..5ffde39 100644 --- a/internal/controller/copyController.go +++ b/internal/controller/copyController.go @@ -115,10 +115,12 @@ func (c *CopyController) startCopy(cmd *dto.CopyCommand) { go c.updateTotalCopyProgress() jd.Start() - c.stopFlag = true c.mq.SendMessage(mq.CopyController, mq.Footer, &dto.SetBusyIndicator{Busy: false}, false) c.mq.SendMessage(mq.CopyController, mq.Footer, &dto.UpdateStatus{Message: ""}, false) - c.mq.SendMessage(mq.CopyController, mq.BuildPage, &dto.CopyComplete{Audiobook: cmd.Audiobook}, true) + if !c.stopFlag { + c.mq.SendMessage(mq.CopyController, mq.BuildPage, &dto.CopyComplete{Audiobook: cmd.Audiobook}, true) + } + c.stopFlag = true } func (c *CopyController) stopCopy(cmd *dto.StopCommand) { diff --git a/internal/controller/downloadController.go b/internal/controller/downloadController.go index 66c6bcf..23abe87 100644 --- a/internal/controller/downloadController.go +++ b/internal/controller/downloadController.go @@ -87,16 +87,15 @@ func (c *DownloadController) startDownload(cmd *dto.DownloadCommand) { jd.AddJob(i, ia.DownloadFile, c.ab.OutputDir, localFileName, item.Server, item.Dir, iaFile.Name, i, iaFile.Size, c.updateFileProgress) } go c.updateTotalProgress() - // if c.stopFlag { - // break - // } jd.Start() - c.stopFlag = true c.mq.SendMessage(mq.DownloadController, mq.Footer, &dto.SetBusyIndicator{Busy: false}, false) c.mq.SendMessage(mq.DownloadController, mq.Footer, &dto.UpdateStatus{Message: ""}, false) - c.mq.SendMessage(mq.DownloadController, mq.DownloadPage, &dto.DownloadComplete{Audiobook: cmd.Audiobook}, true) + if ! c.stopFlag { + c.mq.SendMessage(mq.DownloadController, mq.DownloadPage, &dto.DownloadComplete{Audiobook: cmd.Audiobook}, true) + } + c.stopFlag = true } func (c *DownloadController) updateFileProgress(fileId int, fileName string, size int64, pos int64, percent int) { diff --git a/internal/controller/encodingController.go b/internal/controller/encodingController.go index aa41484..c1197cb 100644 --- a/internal/controller/encodingController.go +++ b/internal/controller/encodingController.go @@ -93,10 +93,12 @@ func (c *EncodingController) startEncoding(cmd *dto.EncodeCommand) { go c.updateTotalProgress() jd.Start() - c.stopFlag = true c.mq.SendMessage(mq.EncodingController, mq.Footer, &dto.SetBusyIndicator{Busy: false}, false) c.mq.SendMessage(mq.EncodingController, mq.Footer, &dto.UpdateStatus{Message: ""}, false) - c.mq.SendMessage(mq.EncodingController, mq.EncodingPage, &dto.EncodingComplete{Audiobook: cmd.Audiobook}, true) + if !c.stopFlag { + c.mq.SendMessage(mq.EncodingController, mq.EncodingPage, &dto.EncodingComplete{Audiobook: cmd.Audiobook}, true) + } + c.stopFlag = true } func (c *EncodingController) encodeFile(fileId int, outputDir string) { @@ -113,14 +115,16 @@ func (c *EncodingController) encodeFile(fileId int, outputDir string) { go c.updateFileProgress(fileId, l) // launch ffmpeg process - _, err := ffmpeg.NewFFmpeg(). + ffmpeg := ffmpeg.NewFFmpeg(). Input(filePath, "-f mp3"). Output(tmpFile, fmt.Sprintf("-f mp3 -ab %dk -ar %d -vn", c.ab.Config.GetBitRate(), c.ab.Config.GetSampleRate())). Overwrite(true). Params("-hide_banner -nostdin -nostats -loglevel fatal"). - SendProgressTo("http://127.0.0.1:" + strconv.Itoa(port)). - Run() - if err != nil { + SendProgressTo("http://127.0.0.1:" + strconv.Itoa(port)) + + go c.killSwitch(ffmpeg) + _, err := ffmpeg.Run() + if err != nil && !c.stopFlag { logger.Error("FFMPEG Error: " + string(err.Error())) } else { err := os.Remove(filePath) @@ -132,6 +136,13 @@ func (c *EncodingController) encodeFile(fileId int, outputDir string) { } } +func (c *EncodingController) killSwitch(ffmpeg *ffmpeg.FFmpeg) { + for !c.stopFlag { + time.Sleep(mq.PullFrequency) + } + ffmpeg.Kill() +} + func (c *EncodingController) startProgressListener(fileId int) (net.Listener, int) { portNumber := config.Instance().GetBasePortNumber() + fileId l, err := net.Listen("tcp", "127.0.0.1:"+strconv.Itoa(portNumber)) diff --git a/internal/ffmpeg/ffmpeg.go b/internal/ffmpeg/ffmpeg.go index 09c6111..d61fd43 100644 --- a/internal/ffmpeg/ffmpeg.go +++ b/internal/ffmpeg/ffmpeg.go @@ -1,7 +1,6 @@ package ffmpeg import ( - "os" "os/exec" "github.com/vpoluyaktov/abb_ia/internal/logger" @@ -11,7 +10,7 @@ type FFmpeg struct { input input output output params params - process *os.Process + cmd *exec.Cmd } type input struct { @@ -33,7 +32,7 @@ func NewFFmpeg() *FFmpeg { input: input{}, output: output{}, params: params{}, - process: nil, + cmd: nil, } return ffmpeg } @@ -76,13 +75,20 @@ func (f *FFmpeg) Run() (string, *exitErr) { args = args.AppendArgs("-i").AppendFileName(fileName) } args = args.AppendArgs(f.output.args).AppendFileName(f.output.fileName) - command := exec.Command(cmd, args.String()...) - logger.Debug("FFMPEG cmd: " + command.String()) - f.process = command.Process - out, err := command.Output() + f.cmd = exec.Command(cmd, args.String()...) + logger.Debug("FFMPEG cmd: " + f.cmd.String()) + out, err := f.cmd.Output() if err != nil { return string(out), ExitErr(err) } else { return string(out), nil } } + +func (f *FFmpeg) Kill() error { + if f.cmd != nil && f.cmd.Process != nil { + return f.cmd.Process.Kill() + } else { + return nil + } +} diff --git a/internal/ui/chaptersPage.go b/internal/ui/chaptersPage.go index 51629b0..0f83ea6 100644 --- a/internal/ui/chaptersPage.go +++ b/internal/ui/chaptersPage.go @@ -332,7 +332,6 @@ 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"}, true) p.mq.SendMessage(mq.ChaptersPage, mq.Frame, &dto.SwitchToPageCommand{Name: "SearchPage"}, true) } diff --git a/internal/ui/downloadPage.go b/internal/ui/downloadPage.go index 2213ec1..eb44a5e 100644 --- a/internal/ui/downloadPage.go +++ b/internal/ui/downloadPage.go @@ -130,7 +130,6 @@ func (p *DownloadPage) stopConfirmation() { } func (p *DownloadPage) stopDownload() { - // Stop the download here p.mq.SendMessage(mq.DownloadPage, mq.DownloadController, &dto.StopCommand{Process: "Download", Reason: "User request"}, false) p.mq.SendMessage(mq.DownloadPage, mq.Frame, &dto.SwitchToPageCommand{Name: "SearchPage"}, false) } diff --git a/internal/ui/encodingPage.go b/internal/ui/encodingPage.go index bc2e09d..8a0cbba 100644 --- a/internal/ui/encodingPage.go +++ b/internal/ui/encodingPage.go @@ -130,9 +130,8 @@ func (p *EncodingPage) stopConfirmation() { } func (p *EncodingPage) stopEncoding() { - // Stop the encoding here - p.mq.SendMessage(mq.EncodingPage, mq.EncodingController, &dto.StopCommand{Process: "Encoding", Reason: "User request"}, false) - p.mq.SendMessage(mq.EncodingPage, mq.Frame, &dto.SwitchToPageCommand{Name: "SearchPage"}, false) + p.mq.SendMessage(mq.EncodingPage, mq.EncodingController, &dto.StopCommand{Process: "Encoding", Reason: "User request"}, true) + p.mq.SendMessage(mq.EncodingPage, mq.Frame, &dto.SwitchToPageCommand{Name: "SearchPage"}, true) } func (p *EncodingPage) updateFileProgress(dp *dto.EncodingFileProgress) { From d8c1d1d39b1260a0d18283b718ba39922788c642 Mon Sep 17 00:00:00 2001 From: vpoluyaktov Date: Fri, 17 Nov 2023 15:37:19 -0800 Subject: [PATCH 3/8] Few UI improvements --- internal/controller/chaptersController.go | 4 ++-- internal/dto/search.go | 2 +- internal/ffmpeg/ffprobe.go | 14 ++++++++++++++ internal/ui/buildPage.go | 4 ++-- internal/ui/encodingPage.go | 2 +- internal/ui/header.go | 1 + internal/ui/tui.go | 1 + internal/ui/wrappers.go | 2 +- 8 files changed, 23 insertions(+), 7 deletions(-) diff --git a/internal/controller/chaptersController.go b/internal/controller/chaptersController.go index f6d7ee3..4ae9473 100644 --- a/internal/controller/chaptersController.go +++ b/internal/controller/chaptersController.go @@ -111,7 +111,7 @@ func (c *ChaptersController) createChapters(cmd *dto.ChaptersCreate) { chapterNo++ chapterFiles = []dto.Mp3File{} if partSize >= int64(c.ab.Config.GetMaxFileSizeMb())*1024*1024 || i == len(c.ab.Mp3Files)-1 { - part := dto.Part{Number: partNo, Format: "VBR MP3", Size: partSize, Duration: partDuration, Chapters: partChapters} + part := dto.Part{Number: partNo, Format: mp3.Format(), Size: partSize, Duration: partDuration, Chapters: partChapters} c.ab.Parts = append(c.ab.Parts, part) partNo++ fileNo = 1 @@ -154,7 +154,7 @@ func (c *ChaptersController) searchReplaceChapters(cmd *dto.SearchReplaceChapter } for partNo, p := range ab.Parts { - for chapterNo, _ := range p.Chapters { + for chapterNo := range p.Chapters { chapter := &ab.Parts[partNo].Chapters[chapterNo] title := chapter.Name title = re.ReplaceAllString(title, replaceStr) diff --git a/internal/dto/search.go b/internal/dto/search.go index 83ea30a..f215637 100644 --- a/internal/dto/search.go +++ b/internal/dto/search.go @@ -6,7 +6,7 @@ import "fmt" var Mp3Formats = []string{"16Kbps MP3", "24Kbps MP3", "32Kbps MP3", "40Kbps MP3", "48Kbps MP3", "56Kbps MP3", "64Kbps MP3", "80Kbps MP3", "96Kbps MP3", "112Kbps MP3", "128Kbps MP3", "144Kbps MP3", "160Kbps MP3", "224Kbps MP3", "256Kbps MP3", "320Kbps MP3", "VBR MP3"} // -var CoverFormats = []string{"JPEG", "PNG", "JPEG Thumb"} +var CoverFormats = []string{"JPEG"} type SearchCommand struct { SearchCondition string diff --git a/internal/ffmpeg/ffprobe.go b/internal/ffmpeg/ffprobe.go index 68ac142..5f5509b 100644 --- a/internal/ffmpeg/ffprobe.go +++ b/internal/ffmpeg/ffprobe.go @@ -2,9 +2,11 @@ package ffmpeg import ( "encoding/json" + "fmt" "os/exec" "path/filepath" "strconv" + "strings" ) type FFProbe struct { @@ -78,3 +80,15 @@ func (p *FFProbe) Size() int64 { return 0 } } + +func (p *FFProbe) Format() string { + bitRate, err := strconv.Atoi(p.BitRate()) + if err != nil { + return p.metadata.Format.FormatName + } + return fmt.Sprintf("%s %d kb/s", strings.ToUpper(p.metadata.Format.FormatName), int(bitRate/1000)) +} + +func (p *FFProbe) BitRate() string { + return p.metadata.Format.BitRate +} diff --git a/internal/ui/buildPage.go b/internal/ui/buildPage.go index c06efae..49d1a26 100644 --- a/internal/ui/buildPage.go +++ b/internal/ui/buildPage.go @@ -68,7 +68,7 @@ func newBuildPage(dispatcher *mq.Dispatcher) *BuildPage { p.buildTable = newTable() p.buildTable.setHeaders(" # ", "File name", "Format", "Duration", "Total 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.buildTable.setAlign(tview.AlignRight, tview.AlignLeft, tview.AlignLeft, tview.AlignRight, tview.AlignRight, tview.AlignLeft) p.buildSection.AddItem(p.buildTable.t, 0, 0, 1, 1, 0, 0, true) p.grid.AddItem(p.buildSection, 1, 0, 1, 1, 0, 0, true) @@ -244,7 +244,7 @@ func (p *BuildPage) updateTotalCopyProgress(dp *dto.CopyProgress) { func (p *BuildPage) buildComplete(c *dto.BuildComplete) { // copy book to Audiobookshelf if needed - ab := c.Audiobook + ab := c.Audiobook if ab.Config.IsCopyToAudiobookshelf() { p.mq.SendMessage(mq.BuildPage, mq.CopyController, &dto.CopyCommand{Audiobook: c.Audiobook}, true) } else { diff --git a/internal/ui/encodingPage.go b/internal/ui/encodingPage.go index 8a0cbba..625c62e 100644 --- a/internal/ui/encodingPage.go +++ b/internal/ui/encodingPage.go @@ -118,7 +118,7 @@ func (p *EncodingPage) displayBookInfo(ab *dto.Audiobook) { p.filesTable.clear() p.filesTable.showHeader() for i, f := range ab.IAItem.AudioFiles { - p.filesTable.appendRow(" "+strconv.Itoa(i+1)+" ", f.Name, f.Format, utils.SecondsToTime(f.Length), utils.BytesToHuman(f.Size), "") + p.filesTable.appendRow(" "+strconv.Itoa(i+1)+" ", f.Name, fmt.Sprintf("MP3 %d kb/s", ab.Config.GetBitRate()), utils.SecondsToTime(f.Length), utils.BytesToHuman(f.Size), "") } p.filesTable.t.ScrollToBeginning() p.mq.SendMessage(mq.EncodingPage, mq.TUI, &dto.SetFocusCommand{Primitive: p.filesTable.t}, true) diff --git a/internal/ui/header.go b/internal/ui/header.go index a27d3b7..12549f3 100644 --- a/internal/ui/header.go +++ b/internal/ui/header.go @@ -50,6 +50,7 @@ func (h *header) dispatchMessage(m *mq.Message) { switch dto := m.Dto.(type) { case *dto.DrawCommand: if dto.Primitive == nil { + return } default: m.UnsupportedTypeError(mq.Header) diff --git a/internal/ui/tui.go b/internal/ui/tui.go index 570c76b..4cb5293 100644 --- a/internal/ui/tui.go +++ b/internal/ui/tui.go @@ -107,6 +107,7 @@ func (ui *TUI) dispatchMessage(m *mq.Message) { if cmd.Primitive == nil { ui.app.Draw() } else { + ui.app.Draw() // ui.app.Draw(cmd.Primitive) // not supported by rivo/tview } case *dto.SetFocusCommand: diff --git a/internal/ui/wrappers.go b/internal/ui/wrappers.go index 947a8cf..632b8c1 100644 --- a/internal/ui/wrappers.go +++ b/internal/ui/wrappers.go @@ -144,7 +144,7 @@ func (t *table) recalculateColumnWidths() { m := (float64(tw-len(t.colWeight)-1) / float64(allWeights)) // multiplier t.colWidth = make([]int, len(t.colWeight)) - for c, _ := range t.colWidth { + for c := range t.colWidth { t.colWidth[c] = int(math.Round(m * float64(t.colWeight[c]))) } } From acf37e8d781b40402e36b7d4d5449ea9d97d3072 Mon Sep 17 00:00:00 2001 From: vpoluyaktov Date: Mon, 20 Nov 2023 17:51:44 -0800 Subject: [PATCH 4/8] Audiobookshelf upload controller created --- .gitignore | 2 +- .../audiobookshelf/audiobookshelf_test.go | 30 ++- internal/audiobookshelf/client.go | 172 +++++++++++++++--- internal/config/config.go | 54 +++--- .../controller/audiobookShelfController.go | 61 ++++++- internal/controller/buildController.go | 12 +- internal/controller/chaptersController.go | 10 +- internal/controller/cleanupController.go | 2 +- internal/controller/copyController.go | 15 +- internal/controller/downloadController.go | 6 +- internal/controller/searchController.go | 2 + internal/dto/audiobook.go | 2 + internal/dto/audiobookshelf.go | 16 ++ internal/dto/ia.go | 2 + internal/ia_client/ia_client.go | 17 +- internal/ia_client/is_client_test.go | 2 + internal/mq/recepients.go | 1 + internal/ui/buildPage.go | 25 ++- internal/ui/chaptersPage.go | 11 +- internal/ui/configPage.go | 23 ++- internal/utils/filepath.go | 3 + 21 files changed, 351 insertions(+), 117 deletions(-) diff --git a/.gitignore b/.gitignore index 825f4b6..8484ab0 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,6 @@ bin/ mock/ output/ dist/ -__debug_bin +__debug_bin* abb_ia.log config.yaml diff --git a/internal/audiobookshelf/audiobookshelf_test.go b/internal/audiobookshelf/audiobookshelf_test.go index 802181d..945c7b8 100644 --- a/internal/audiobookshelf/audiobookshelf_test.go +++ b/internal/audiobookshelf/audiobookshelf_test.go @@ -15,11 +15,9 @@ func TestLogin(t *testing.T) { password := config.Instance().GetAudiobookshelfPassword() if url != "" && username != "" && password != "" { - loginResp, err := audiobookshelf.Login(url+"/login", username, password) - + absClient := audiobookshelf.NewClient(url) + err := absClient.Login(username, password) assert.NoError(t, err) - assert.NotNil(t, loginResp.User.ID) - assert.NotNil(t, loginResp.User.Token) } } @@ -30,13 +28,14 @@ func TestLibraries(t *testing.T) { password := config.Instance().GetAudiobookshelfPassword() if url != "" && username != "" && password != "" { - loginResp, err := audiobookshelf.Login(url+"/login", username, password) + absClient := audiobookshelf.NewClient(url) + err := absClient.Login(username, password) assert.NoError(t, err) if err == nil { - libraryResponse, err := audiobookshelf.Libraries(url, loginResp.User.Token) + libraries, err := absClient.GetLibraries() assert.NoError(t, err) - assert.NotNil(t, libraryResponse) - assert.NotEmpty(t, libraryResponse.Libraries) + assert.NotNil(t, libraries) + assert.NotEmpty(t, libraries) } } } @@ -49,19 +48,16 @@ func TestScan(t *testing.T) { libraryName := config.Instance().GetAudiobookshelfLibrary() if url != "" && username != "" && password != "" && libraryName != "" { - loginResp, err := audiobookshelf.Login(url+"/login", username, password) + absClient := audiobookshelf.NewClient(url) + err := absClient.Login(username, password) + assert.NoError(t, err) if err == nil { + libraries, err := absClient.GetLibraries() assert.NoError(t, err) - libraryResponse, err := audiobookshelf.Libraries(url, loginResp.User.Token) + libraryID, err := absClient.GetLibraryId(libraries, libraryName) if err == nil { + err = absClient.ScanLibrary(libraryID) assert.NoError(t, err) - libraryID, err := audiobookshelf.GetLibraryByName(libraryResponse.Libraries, libraryName) - if err == nil { - err = audiobookshelf.ScanLibrary(url, loginResp.User.Token, libraryID) - assert.NoError(t, err) - assert.NotNil(t, libraryResponse) - assert.NotEmpty(t, libraryResponse.Libraries) - } } } } diff --git a/internal/audiobookshelf/client.go b/internal/audiobookshelf/client.go index e0919ed..db591e5 100644 --- a/internal/audiobookshelf/client.go +++ b/internal/audiobookshelf/client.go @@ -4,75 +4,99 @@ import ( "bytes" "encoding/json" "fmt" + "io" "io/ioutil" + "mime/multipart" "net/http" + "os" + "path/filepath" + "strconv" + + "github.com/vpoluyaktov/abb_ia/internal/dto" ) +type AudiobookshelfClient struct { + url string + userName string + password string + loginResponse *LoginResponse +} + +func NewClient(url string) *AudiobookshelfClient { + c := &AudiobookshelfClient{ + url: url, + } + return c +} + // Call the Audiobookshelf API login method. -func Login(absUrl string, username, password string) (LoginResponse, error) { +func (c *AudiobookshelfClient) Login(userName string, password string) error { + + c.userName = userName + c.password = password + requestBody := LoginRequest{ - Username: username, - Password: password, + Username: c.userName, + Password: c.password, } requestBodyBytes, err := json.Marshal(requestBody) if err != nil { - return LoginResponse{}, fmt.Errorf("failed to marshal login response body: %v", err) + return fmt.Errorf("failed to marshal login response body: %v", err) } - resp, err := http.Post(absUrl, "application/json", bytes.NewBuffer(requestBodyBytes)) + resp, err := http.Post(c.url+"/login", "application/json", bytes.NewBuffer(requestBodyBytes)) if err != nil { - return LoginResponse{}, fmt.Errorf("failed to make login API call: %v", err) + return fmt.Errorf("failed to make login API call: %v", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { - return LoginResponse{}, fmt.Errorf("login API call returned status code: %d", resp.StatusCode) + return fmt.Errorf("login API call returned status code: %d", resp.StatusCode) } var loginResp LoginResponse err = json.NewDecoder(resp.Body).Decode(&loginResp) if err != nil { - return LoginResponse{}, fmt.Errorf("failed to decode login response: %v", err) + return fmt.Errorf("failed to decode login response: %v", err) } + c.loginResponse = &loginResp - return loginResp, nil + return nil } // Call the Audiobookshelf API libraries method -func Libraries(url string, token string) (LibrariesResponse, error) { +func (c *AudiobookshelfClient) GetLibraries() ([]Library, error) { client := &http.Client{} - url = url + "/api/libraries" - req, err := http.NewRequest("GET", url, nil) + req, err := http.NewRequest("GET", c.url+"/api/libraries", nil) if err != nil { - return LibrariesResponse{}, fmt.Errorf("error creating request: %v", err) + return nil, fmt.Errorf("error creating request: %v", err) } - req.Header.Add("Authorization", "Bearer "+token) + req.Header.Add("Authorization", "Bearer "+c.loginResponse.User.Token) resp, err := client.Do(req) if err != nil { - return LibrariesResponse{}, fmt.Errorf("error sending request: %v", err) + return nil, fmt.Errorf("error sending request: %v", err) } defer resp.Body.Close() body, err := ioutil.ReadAll(resp.Body) if err != nil { - return LibrariesResponse{}, fmt.Errorf("error reading response body: %v", err) + return nil, fmt.Errorf("error reading response body: %v", err) } var response LibrariesResponse err = json.Unmarshal(body, &response) if err != nil { - return LibrariesResponse{}, fmt.Errorf("error parsing response body: %v", err) + return nil, fmt.Errorf("error parsing response body: %v", err) } - return response, nil + return response.Libraries, nil } -func GetLibraryByName(libraries []Library, libraryName string) (libraryID string, err error) { - +func (c *AudiobookshelfClient) GetLibraryId(libraries []Library, libraryName string) (libraryID string, err error) { for _, library := range libraries { if library.Name == libraryName { return library.ID, nil @@ -81,14 +105,24 @@ func GetLibraryByName(libraries []Library, libraryName string) (libraryID string return "", fmt.Errorf("no library with name '%s' found", libraryName) } -// Call the Audiobookshelf API -func ScanLibrary(url string, authToken string, libraryID string) error { - url = url + "/api/libraries/" + libraryID + "/scan" +func (c *AudiobookshelfClient) GetFolders(libraries []Library, libraryName string) (folders []Folder, err error) { + for _, library := range libraries { + if library.Name == libraryName { + return library.Folders, nil + } + } + return nil, fmt.Errorf("no library with name '%s' found", libraryName) +} + + +// Call the Audiobookshelf API for a library Scan +func (c *AudiobookshelfClient) ScanLibrary(libraryID string) error { + url := c.url + "/api/libraries/" + libraryID + "/scan" req, err := http.NewRequest("POST", url, nil) if err != nil { return fmt.Errorf("error creating request: %v", err) } - req.Header.Set("Authorization", "Bearer "+authToken) + req.Header.Set("Authorization", "Bearer "+c.loginResponse.User.Token) client := &http.Client{} resp, err := client.Do(req) @@ -104,4 +138,92 @@ func ScanLibrary(url string, authToken string, libraryID string) error { } else { return nil } -} \ No newline at end of file +} + +// Upload audiobook to The Audibookshelf server +func (c *AudiobookshelfClient) UploadBook(ab *dto.Audiobook, libraryID string, folderID string, callback func(int, int) ) error { + // Open each file for upload + var filesList []*os.File + for _, part := range ab.Parts { + f, err := os.Open(part.M4BFile) + if err != nil { + return err + } + defer f.Close() + filesList = append(filesList, f) + } + + body := &bytes.Buffer{} + writer := multipart.NewWriter(body) + + // Add metadata fields + _ = writer.WriteField("title", ab.Title) + _ = writer.WriteField("author", ab.Author) + _ = writer.WriteField("series", ab.Series) + _ = writer.WriteField("library", libraryID) + _ = writer.WriteField("folder", folderID) + + // Add files to the request with progress reporting + for i, file := range filesList { + part, err := writer.CreateFormFile(strconv.Itoa(i), filepath.Base(file.Name())) + if err != nil { + return err + } + + // Create a progress reader to track callback + fileStat, err := file.Stat() + if err != nil { + return err + } + pr := &progressReader{ + Reader: file, + Size: fileStat.Size(), + Callback: callback, + } + + _, err = io.Copy(part, pr) + if err != nil { + return err + } + } + + err := writer.Close() + if err != nil { + return err + } + + req, err := http.NewRequest("POST", c.url+"/api/upload", body) + if err != nil { + return err + } + req.Header.Set("Content-Type", writer.FormDataContentType()) + req.Header.Set("Authorization", "Bearer "+c.loginResponse.User.Token) + + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + + // Check response status code + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("failed to upload audiobook: %s", resp.Status) + } + + return nil +} + +type progressReader struct { + Reader io.Reader + Size int64 + Callback func(int, int) +} + +func (pr *progressReader) Read(p []byte) (int, error) { + n, err := pr.Reader.Read(p) + if err == nil { + pr.Callback(n, int(pr.Size)) + } + return n, err +} diff --git a/internal/config/config.go b/internal/config/config.go index 00d7bae..966b2b1 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -25,13 +25,15 @@ 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"` + SearchRowsMax int `yaml:"SearchRowsMax"` LogFileName string `yaml:"LogFileName"` - OutputDir string `yaml:"OutputDir"` + OutputDir string `yaml:"Outputdir"` + CopyToOutputDir bool `yaml:"CopyToOutputDir"` + TmpDir string `yaml:"TmpDir"` LogLevel string `yaml:"LogLevel"` - SearchRowsMax int `yaml:"SearchRowsMax"` UseMock bool `yaml:"UseMock"` SaveMock bool `yaml:"SaveMock"` - SearchCondition string `yaml:"SearchCondition"` ConcurrentDownloaders int `yaml:"ConcurrentDownloaders"` ConcurrentEncoders int `yaml:"ConcurrentEncoders"` ReEncodeFiles bool `yaml:"ReEncodeFiles"` @@ -39,12 +41,11 @@ type Config struct { BitRateKbs int `yaml:"BitRateKbs"` SampleRateHz int `yaml:"SampleRateHz"` MaxFileSizeMb int `yaml:"MaxFileSizeMb"` - CopyToAudiobookshelf bool `yaml:"CopyToAudiobookshelf"` + UploadToAudiobookshef bool `yaml:"UploadToAudiobookshelf"` AudiobookshelfUrl string `yaml:"AudiobookshelfUrl"` AudiobookshelfUser string `yaml:"AudiobookshelfUser"` AudiobookshelfPassword string `yaml:"AudiobookshelfPassword"` AudiobookshelfLibrary string `yaml:"AudiobookshelfLibrary"` - AudiobookshelfDir string `yaml:"AudiobookshelfDir"` ShortenTitles bool `yaml:"ShortenTitles"` ShortenPairs []ShortenPair `yaml:"ShortenPairs"` Genres []string `yaml:"Genres"` @@ -67,7 +68,9 @@ func Load() { // default settings config.LogFileName = "abb_ia.log" - config.OutputDir = "output" + config.TmpDir = "tmp" + config.CopyToOutputDir = true + config.OutputDir = "/mnt/NAS/Audiobooks/Internet Archive" config.LogLevel = "INFO" config.SearchRowsMax = 25 config.UseMock = false @@ -80,11 +83,10 @@ func Load() { config.BitRateKbs = 128 config.SampleRateHz = 44100 config.MaxFileSizeMb = 250 - config.CopyToAudiobookshelf = true + config.UploadToAudiobookshef = true config.AudiobookshelfUser = "admin" config.AudiobookshelfPassword = "" config.AudiobookshelfLibrary = "Internet Archive" - config.AudiobookshelfDir = "/mnt/NAS/Audiobooks/Internet Archive" config.ShortenTitles = true config.ShortenPairs = []ShortenPair{ {"Old Time Radio Researchers Group", "OTRR"}, @@ -145,14 +147,30 @@ func (c *Config) GetLogFileName() string { return c.LogFileName } -func (c *Config) SetOutputDir(outputDir string) { - c.OutputDir = outputDir +func (c *Config) SetTmpDir(tmpDir string) { + c.TmpDir = tmpDir +} + +func (c *Config) GetTmpDir() string { + return c.TmpDir } func (c *Config) GetOutputDir() string { return c.OutputDir } +func (c *Config) SetOutputdDir(d string) { + c.OutputDir = d +} + +func (c *Config) SetCopyToOutputDir(b bool) { + c.CopyToOutputDir = b +} + +func (c *Config) IsCopyToOutputDir() bool { + return c.CopyToOutputDir +} + func (c *Config) SetLogLevel(logLevel string) { c.LogLevel = logLevel } @@ -249,20 +267,12 @@ func (c *Config) GetMaxFileSizeMb() int { return c.MaxFileSizeMb } -func (c *Config) SetCopyToAudiobookshelf(b bool) { - c.CopyToAudiobookshelf = b -} - -func (c *Config) IsCopyToAudiobookshelf() bool { - return c.CopyToAudiobookshelf -} - -func (c *Config) GetAudiobookshelfDir() string { - return c.AudiobookshelfDir +func (c *Config) SetUploadToAudiobookshelf(b bool) { + c.UploadToAudiobookshef = b } -func (c *Config) SetAudiobookshelfDir(d string) { - c.AudiobookshelfDir = d +func (c *Config) IsUploadToAudiobookshef() bool { + return c.UploadToAudiobookshef } func (c *Config) GetAudiobookshelfUrl() string { diff --git a/internal/controller/audiobookShelfController.go b/internal/controller/audiobookShelfController.go index c7a89bf..8369ab3 100644 --- a/internal/controller/audiobookShelfController.go +++ b/internal/controller/audiobookShelfController.go @@ -1,6 +1,8 @@ package controller import ( + "strconv" + "github.com/vpoluyaktov/abb_ia/internal/audiobookshelf" "github.com/vpoluyaktov/abb_ia/internal/dto" "github.com/vpoluyaktov/abb_ia/internal/logger" @@ -29,6 +31,8 @@ func (c *AudiobookshelfController) dispatchMessage(m *mq.Message) { switch dto := m.Dto.(type) { case *dto.AudiobookshelfScanCommand: go c.audiobookshelfScan(dto) + case *dto.AudiobookshelfUploadCommand: + go c.uploadAudiobook(dto) default: m.UnsupportedTypeError(mq.AudiobookshelfController) } @@ -43,22 +47,23 @@ func (c *AudiobookshelfController) audiobookshelfScan(cmd *dto.AudiobookshelfSca libraryName := ab.Config.GetAudiobookshelfLibrary() if url != "" && username != "" && password != "" && libraryName != "" { - loginResp, err := audiobookshelf.Login(url+"/login", username, password) + absClient := audiobookshelf.NewClient(url) + err := absClient.Login(username, password) if err != nil { logger.Error("Can't login to audiobookshlf server: " + err.Error()) return } - libraryResponse, err := audiobookshelf.Libraries(url, loginResp.User.Token) + libraries, err := absClient.GetLibraries() if err != nil { logger.Error("Can't get a list of libraries from audiobookshlf server: " + err.Error()) return } - libraryID, err := audiobookshelf.GetLibraryByName(libraryResponse.Libraries, libraryName) + libraryID, err := absClient.GetLibraryId(libraries, libraryName) if err != nil { logger.Error("Can't find audiobookshlf library by name: " + err.Error()) return } - err = audiobookshelf.ScanLibrary(url, loginResp.User.Token, libraryID) + err = absClient.ScanLibrary(libraryID) if err != nil { logger.Error("Can't launch library scan on audiobookshlf server: " + err.Error()) return @@ -70,3 +75,51 @@ func (c *AudiobookshelfController) audiobookshelfScan(cmd *dto.AudiobookshelfSca logger.Info("A scan launched for library " + libraryName + " on audiobookshlf server") } } + +func (c *AudiobookshelfController) uploadAudiobook(cmd *dto.AudiobookshelfUploadCommand) { + logger.Info(mq.AudiobookshelfController + " received " + cmd.String()) + ab := cmd.Audiobook + url := ab.Config.GetAudiobookshelfUrl() + username := ab.Config.GetAudiobookshelfUser() + password := ab.Config.GetAudiobookshelfPassword() + libraryName := ab.Config.GetAudiobookshelfLibrary() + + if url != "" && username != "" && password != "" && libraryName != "" { + absClient := audiobookshelf.NewClient(url) + err := absClient.Login(username, password) + if err != nil { + 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 audiobookshelf server: " + err.Error()) + return + } + libraryID, err := absClient.GetLibraryId(libraries, libraryName) + if err != nil { + logger.Error("Can't find audiobookshelf library by name: " + err.Error()) + return + } + folders, err := absClient.GetFolders(libraries, libraryName) + if err != nil || len(folders) == 0 { + logger.Error("Can't get a folder for library: " + err.Error()) + return + } + + // TODO: Check if a folder selector is needed here. Let's use first folder in a library for upload + err = absClient.UploadBook(ab, libraryID, folders[0].ID, c.updateFileUplodProgress) + if err != nil { + logger.Error("Can't upload the audiobook to audiobookshelf server: " + err.Error()) + return + } + } +} + +func (c *AudiobookshelfController) updateFileUplodProgress(bytesUploaded int, totalBytes int) { + + if totalBytes != 0 { + percent := bytesUploaded / totalBytes * 100 + logger.Debug("Upload percent: " + strconv.Itoa(percent)) + } +} diff --git a/internal/controller/buildController.go b/internal/controller/buildController.go index 3525bfa..ad0a20e 100644 --- a/internal/controller/buildController.go +++ b/internal/controller/buildController.go @@ -79,7 +79,7 @@ func (c *BuildController) startBuild(cmd *dto.BuildCommand) { // calculate output file names for i := range c.ab.Parts { part := &c.ab.Parts[i] - filePath := filepath.Join(c.ab.Config.GetOutputDir(), c.ab.Author+" - "+c.ab.Title) + filePath := filepath.Join(c.ab.Config.GetTmpDir(), c.ab.Author+" - "+c.ab.Title) if len(c.ab.Parts) > 1 { filePath = filePath + fmt.Sprintf(", Part %02d", i+1) } @@ -149,9 +149,9 @@ func (c *BuildController) createMetadata(ab *dto.Audiobook) { f.WriteString("album=" + ab.Title + "\n") f.WriteString("genre=" + ab.Genre + "\n") f.WriteString("description=" + strings.ReplaceAll(ab.Description, "\n", "\\\n") + "\n") - f.WriteString("copyright=" + ab.Copyright + "\n") - f.WriteString("comment=Downloaded from Internet Archive: " + ab.IaURL + "\n") - f.WriteString("encoder=This audiobook was created by 'Audiobook Builder Internet Archive version' https://github.com/vpoluyaktov/abb_ia\n") + f.WriteString("copyright=" + ab.LicenseUrl + "\n") + f.WriteString("comment=This audiobook was created using the 'Audiobook Builder' tool: https://github.com/vpoluyaktov/abb_ia\\\n" + + "The audio files used for this book were obtained from the Internet Archive site: " + ab.IaURL + "\n") for _, chapter := range part.Chapters { f.WriteString("[CHAPTER]\n") @@ -165,7 +165,7 @@ func (c *BuildController) createMetadata(ab *dto.Audiobook) { } func (c *BuildController) downloadCoverImage(ab *dto.Audiobook) error { - filePath := filepath.Join(ab.Config.GetOutputDir(), ab.Author+" - "+ab.Title) + filePath := filepath.Join(ab.Config.GetTmpDir(), ab.Author+" - "+ab.Title) if strings.HasSuffix(ab.CoverURL, ".jpg") { ab.CoverFile = filePath + ".jpg" } else if strings.HasSuffix(ab.CoverURL, ".png") { @@ -231,7 +231,7 @@ func (c *BuildController) buildAudiobookPart(ab *dto.Audiobook, partId int) { go c.killSwitch(ffmpeg) _, err := ffmpeg.Run() - if err != nil && ! c.stopFlag { + if err != nil && !c.stopFlag { logger.Error("FFMPEG Error: " + string(err.Error())) } } diff --git a/internal/controller/chaptersController.go b/internal/controller/chaptersController.go index 4ae9473..1f8ef35 100644 --- a/internal/controller/chaptersController.go +++ b/internal/controller/chaptersController.go @@ -196,11 +196,11 @@ func (c *ChaptersController) joinChapters(cmd *dto.JoinChaptersCommand) { previousChapterName = chapter.Name } - // add last chapter in a part - if chNo == len(part.Chapters)-1 { - chapters = append(chapters, *chapter) - chapterNo++ - } + } + // add last chapter in a part + if chNo == len(part.Chapters)-1 { + chapters = append(chapters, *chapter) + chapterNo++ } } part.Chapters = chapters diff --git a/internal/controller/cleanupController.go b/internal/controller/cleanupController.go index af9cdbd..7dc6902 100644 --- a/internal/controller/cleanupController.go +++ b/internal/controller/cleanupController.go @@ -47,7 +47,7 @@ func (c *CleanupController) cleanUp(cmd *dto.CleanupCommand) { os.Remove(part.AACFile) os.Remove(part.FListFile) os.Remove(part.MetadataFile) - if c.ab.Config.IsCopyToAudiobookshelf() { + if c.ab.Config.IsCopyToOutputDir() { os.Remove(part.M4BFile) } } diff --git a/internal/controller/copyController.go b/internal/controller/copyController.go index 5ffde39..a6250ba 100644 --- a/internal/controller/copyController.go +++ b/internal/controller/copyController.go @@ -84,6 +84,7 @@ func (c *CopyController) dispatchMessage(m *mq.Message) { } func (c *CopyController) startCopy(cmd *dto.CopyCommand) { + c.startTime = time.Now() logger.Info(mq.CopyController + " received " + cmd.String()) c.ab = cmd.Audiobook @@ -141,7 +142,7 @@ func (c *CopyController) copyAudiobookPart(ab *dto.Audiobook, partId int) { defer file.Close() // Calculate Audiobookshelf directory structure (see: https://www.audiobookshelf.org/docs#book-directory-structure) - destPath := filepath.Join(ab.Config.GetAudiobookshelfDir(), ab.Author) + destPath := filepath.Join(ab.Config.GetOutputDir(), ab.Author) if ab.Series != "" { destPath = filepath.Join(destPath, ab.Author+" - "+ab.Series) } @@ -205,10 +206,10 @@ func (c *CopyController) updateTotalCopyProgress() { for !c.stopFlag && percent <= 100 { var totalSize = c.ab.TotalSize - var totalBytesDownloaded int64 = 0 + var totalBytesCopied int64 = 0 filesCopied := 0 for _, f := range c.filesCopy { - totalBytesDownloaded += f.bytesCopied + totalBytesCopied += f.bytesCopied if f.progress == 100 { filesCopied++ } @@ -216,13 +217,13 @@ func (c *CopyController) updateTotalCopyProgress() { var p int = 0 if totalSize > 0 { - p = int(float64(totalBytesDownloaded) / float64(totalSize) * 100) + p = int(float64(totalBytesCopied) / float64(totalSize) * 100) } // fix wrong incorrect calculation if filesCopied == len(c.filesCopy) { p = 100 - totalBytesDownloaded = c.ab.TotalSize + totalBytesCopied = c.ab.TotalSize } if percent != p { @@ -237,14 +238,14 @@ func (c *CopyController) updateTotalCopyProgress() { } elapsed := time.Since(c.startTime).Seconds() - speed := int64(float64(totalBytesDownloaded) / elapsed) + speed := int64(float64(totalBytesCopied) / elapsed) eta := (100 / (float64(percent) / elapsed)) - elapsed if eta < 0 || eta > (60*60*24*365) { eta = 0 } elapsedH := utils.SecondsToTime(elapsed) - bytesH := utils.BytesToHuman(totalBytesDownloaded) + bytesH := utils.BytesToHuman(totalBytesCopied) filesH := fmt.Sprintf("%d/%d", filesCopied, len(c.ab.Parts)) speedH := utils.SpeedToHuman(speed) etaH := utils.SecondsToTime(eta) diff --git a/internal/controller/downloadController.go b/internal/controller/downloadController.go index 23abe87..1c4f8f8 100644 --- a/internal/controller/downloadController.go +++ b/internal/controller/downloadController.go @@ -69,7 +69,9 @@ func (c *DownloadController) startDownload(cmd *dto.DownloadCommand) { c.ab.Title = item.Title c.ab.Description = item.Description c.ab.CoverURL = item.CoverUrl - c.ab.OutputDir = utils.SanitizeFilePath(filepath.Join(c.ab.Config.GetOutputDir(), item.ID)) + c.ab.IaURL = item.IaURL + c.ab.LicenseUrl = item.LicenseUrl + c.ab.OutputDir = utils.SanitizeFilePath(filepath.Join(c.ab.Config.GetTmpDir(), item.ID)) c.ab.TotalSize = item.TotalSize c.ab.TotalDuration = item.TotalLength @@ -92,7 +94,7 @@ func (c *DownloadController) startDownload(cmd *dto.DownloadCommand) { c.mq.SendMessage(mq.DownloadController, mq.Footer, &dto.SetBusyIndicator{Busy: false}, false) c.mq.SendMessage(mq.DownloadController, mq.Footer, &dto.UpdateStatus{Message: ""}, false) - if ! c.stopFlag { + if !c.stopFlag { c.mq.SendMessage(mq.DownloadController, mq.DownloadPage, &dto.DownloadComplete{Audiobook: cmd.Audiobook}, true) } c.stopFlag = true diff --git a/internal/controller/searchController.go b/internal/controller/searchController.go index a866c36..cd409ee 100644 --- a/internal/controller/searchController.go +++ b/internal/controller/searchController.go @@ -60,6 +60,8 @@ func (c *SearchController) performSearch(cmd *dto.SearchCommand) { item := &dto.IAItem{} item.ID = doc.Identifier item.Title = tview.Escape(doc.Title) + item.IaURL = ia_client.IA_BASE_URL + "/details/" + doc.Identifier + item.LicenseUrl = doc.Licenseurl item.AudioFiles = make([]dto.AudioFile, 0) var totalSize int64 = 0 diff --git a/internal/dto/audiobook.go b/internal/dto/audiobook.go index 7a5257f..3fdb0b8 100644 --- a/internal/dto/audiobook.go +++ b/internal/dto/audiobook.go @@ -15,10 +15,12 @@ type Audiobook struct { Series string SeriesNo string Narator string + Year string CoverURL string CoverFile string IaURL string Copyright string + LicenseUrl string OutputDir string Mp3Files []Mp3File TotalDuration float64 diff --git a/internal/dto/audiobookshelf.go b/internal/dto/audiobookshelf.go index eb14a41..1dd99c3 100644 --- a/internal/dto/audiobookshelf.go +++ b/internal/dto/audiobookshelf.go @@ -8,4 +8,20 @@ type AudiobookshelfScanCommand struct { func (c *AudiobookshelfScanCommand) String() string { return fmt.Sprintf("AudiobookshelfScanCommand: %s", c.Audiobook.String()) +} + +type AudiobookshelfUploadCommand struct { + Audiobook *Audiobook +} + +func (c *AudiobookshelfUploadCommand) String() string { + return fmt.Sprintf("AudiobookshelfUploadCommand: %s", c.Audiobook.String()) +} + +type UploadComplete struct { + Audiobook *Audiobook +} + +func (c *UploadComplete) String() string { + return fmt.Sprintf("UploadComplete: %s", c.Audiobook.String()) } \ No newline at end of file diff --git a/internal/dto/ia.go b/internal/dto/ia.go index aa7193c..bdba209 100644 --- a/internal/dto/ia.go +++ b/internal/dto/ia.go @@ -8,6 +8,8 @@ type IAItem struct { Creator string Description string CoverUrl string + IaURL string + LicenseUrl string Server string Dir string TotalLength float64 diff --git a/internal/ia_client/ia_client.go b/internal/ia_client/ia_client.go index 97c3ed8..d55e533 100644 --- a/internal/ia_client/ia_client.go +++ b/internal/ia_client/ia_client.go @@ -17,13 +17,13 @@ import ( ) const ( - IA_BASE_URL = "https://archive.org" - MOCK_DIR = "mock" + IA_BASE_URL = "https://archive.org" + MOCK_DIR = "mock" ) type IAClient struct { restyClient *resty.Client - maxSearchRows int + maxSearchRows int loadMockResult bool saveMockResult bool } @@ -112,8 +112,8 @@ func (client *IAClient) GetItemDetails(itemId string) *ItemDetails { logger.Error("IAClient GetItemDetails() mock load error: " + err.Error()) } } else { - var getURL = IA_BASE_URL + "/details/%s/?output=json" - _, err := client.restyClient.R().SetResult(result).Get(fmt.Sprintf(getURL, itemId)) + var getURL = fmt.Sprintf(IA_BASE_URL + "/details/%s/?output=json", itemId) + _, err := client.restyClient.R().SetResult(result).Get(getURL) if err != nil { logger.Error("IAClient GetItemDetails() error: " + err.Error()) } @@ -141,7 +141,12 @@ func (client *IAClient) DownloadFile(localDir string, localFile string, iaServer iaDir = strings.TrimPrefix(iaDir, "/") iaFile = strings.TrimPrefix(iaFile, "/") - fileUrl := fmt.Sprintf("https://%s/%s/%s", iaServer, iaDir, iaFile) + URL := &url.URL{ + Scheme: "https", + Host: iaServer, + Path: iaDir + "/" + iaFile, + } + fileUrl := URL.String() localPath := filepath.Join(localDir, localFile) tempPath := localPath + ".tmp" diff --git a/internal/ia_client/is_client_test.go b/internal/ia_client/is_client_test.go index 50f408e..1a8ef86 100644 --- a/internal/ia_client/is_client_test.go +++ b/internal/ia_client/is_client_test.go @@ -39,6 +39,8 @@ func TestGetItemById(t *testing.T) { ia := ia_client.New(5, false, false) item := ia.GetItemDetails("OTRR_Frank_Race_Singles") assert.NotNil(t, item) + assert.GreaterOrEqual(t, 1, len(item.Metadata.Title)) + assert.GreaterOrEqual(t, 1, len(item.Metadata.Creator)) if logLevel == logger.DEBUG { if item != nil { fmt.Printf("Title: %s\n", item.Metadata.Title[0]) diff --git a/internal/mq/recepients.go b/internal/mq/recepients.go index b5659a9..90c7a8c 100644 --- a/internal/mq/recepients.go +++ b/internal/mq/recepients.go @@ -21,6 +21,7 @@ const ( ChaptersController = "ChaptersController" BuildController = "BuildController" CopyController = "CopyController" + UploadController = "UploadController" CleanupController = "CleanupController" AudiobookshelfController = "AudiobookshelfController" ) diff --git a/internal/ui/buildPage.go b/internal/ui/buildPage.go index 49d1a26..6e58321 100644 --- a/internal/ui/buildPage.go +++ b/internal/ui/buildPage.go @@ -75,7 +75,7 @@ func newBuildPage(dispatcher *mq.Dispatcher) *BuildPage { // copy section p.copySection = tview.NewGrid() p.copySection.SetColumns(-1) - p.copySection.SetTitle(" Audiobookshelf copy progress: ") + p.copySection.SetTitle(" Output directory copy progress: ") p.copySection.SetTitleAlign(tview.AlignLeft) p.copySection.SetBorder(true) @@ -126,6 +126,8 @@ func (p *BuildPage) dispatchMessage(m *mq.Message) { p.updateTotalCopyProgress(dto) case *dto.CopyComplete: p.copyComplete(dto) + case *dto.UploadComplete: + p.uploadComplete(dto) default: m.UnsupportedTypeError(mq.BuildPage) } @@ -147,7 +149,7 @@ func (p *BuildPage) displayBookInfo(ab *dto.Audiobook) { } p.buildTable.ScrollToBeginning() - if ab.Config.IsCopyToAudiobookshelf() { + if ab.Config.IsCopyToOutputDir() { p.copyTable.clear() p.copyTable.showHeader() for i, part := range ab.Parts { @@ -243,17 +245,15 @@ func (p *BuildPage) updateTotalCopyProgress(dp *dto.CopyProgress) { } func (p *BuildPage) buildComplete(c *dto.BuildComplete) { - // copy book to Audiobookshelf if needed + // copy the book to Output directory if needed ab := c.Audiobook - if ab.Config.IsCopyToAudiobookshelf() { + if ab.Config.IsCopyToOutputDir() { p.mq.SendMessage(mq.BuildPage, mq.CopyController, &dto.CopyCommand{Audiobook: c.Audiobook}, true) - } else { - p.mq.SendMessage(mq.BuildPage, mq.CleanupController, &dto.CleanupCommand{Audiobook: c.Audiobook}, true) - p.mq.SendMessage(mq.BuildPage, mq.AudiobookshelfController, &dto.AudiobookshelfScanCommand{Audiobook: c.Audiobook}, true) - newMessageDialog(p.mq, "Build Complete", "Audiobook has been created", p.buildSection) - //p.mq.SendMessage(mq.BuildPage, mq.Frame, &dto.SwitchToPageCommand{Name: "SearchPage"}, false) } + if ab.Config.IsUploadToAudiobookshef() { + p.mq.SendMessage(mq.BuildPage, mq.AudiobookshelfController, &dto.AudiobookshelfUploadCommand{Audiobook: c.Audiobook}, true) + } } func (p *BuildPage) copyComplete(c *dto.CopyComplete) { @@ -262,3 +262,10 @@ func (p *BuildPage) copyComplete(c *dto.CopyComplete) { newMessageDialog(p.mq, "Build Complete", "Audiobook has been created", p.buildSection) //p.mq.SendMessage(mq.BuildPage, mq.Frame, &dto.SwitchToPageCommand{Name: "SearchPage"}, false) } + +func (p *BuildPage) uploadComplete(c *dto.UploadComplete) { + p.mq.SendMessage(mq.BuildPage, mq.CleanupController, &dto.CleanupCommand{Audiobook: c.Audiobook}, true) + p.mq.SendMessage(mq.BuildPage, mq.AudiobookshelfController, &dto.AudiobookshelfScanCommand{Audiobook: c.Audiobook}, true) + newMessageDialog(p.mq, "Build Complete", "Audiobook has been created", p.buildSection) + //p.mq.SendMessage(mq.BuildPage, mq.Frame, &dto.SwitchToPageCommand{Name: "SearchPage"}, false) +} diff --git a/internal/ui/chaptersPage.go b/internal/ui/chaptersPage.go index 0f83ea6..1b938ea 100644 --- a/internal/ui/chaptersPage.go +++ b/internal/ui/chaptersPage.go @@ -122,6 +122,7 @@ func newChaptersPage(dispatcher *mq.Dispatcher) *ChaptersPage { descriptionSection.SetColumns(-1, 40) descriptionSection.SetBorder(false) p.descriptionEditor = newTextArea("") + p.descriptionEditor.SetChangedFunc(p.updateDescription) p.descriptionEditor.SetBorder(true) p.descriptionEditor.SetTitle(" Book description: ") p.descriptionEditor.SetTitleAlign(tview.AlignLeft) @@ -158,7 +159,7 @@ func newChaptersPage(dispatcher *mq.Dispatcher) *ChaptersPage { f6.AddInputField("Replace:", "", 30, nil, func(s string) { p.replaceChapters = s }) f6.AddButton("Replace", p.searchReplaceChapters) f6.AddButton(" Undo ", p.undoChapters) - f6.AddButton(" Join Chapters ", p.joinChapters) + f6.AddButton(" Join Similar Chapters ", p.joinChapters) f6.SetButtonsAlign(tview.AlignRight) f6.SetMouseDblClickFunc(func() {}) p.chaptersSection.AddItem(f6.f, 0, 1, 1, 1, 0, 0, false) @@ -221,7 +222,7 @@ func (p *ChaptersPage) displayParts(ab *dto.Audiobook) { func (p *ChaptersPage) addPart(part *dto.Part) { if len(p.ab.Parts) > 1 { number := strconv.Itoa(part.Number) - p.chaptersTable.appendSeparator("", "", "", "", "Part Number "+number) + p.chaptersTable.appendSeparator("", "", "", "", "Part # "+number+". Size: "+utils.BytesToHuman(part.Size)) } for _, chapter := range part.Chapters { p.addChapter(&chapter) @@ -238,6 +239,12 @@ func (p *ChaptersPage) addChapter(chapter *dto.Chapter) { p.chaptersTable.ScrollToBeginning() } +func (p *ChaptersPage) updateDescription() { + if p.ab != nil { + p.ab.Description = p.descriptionEditor.GetText() + } +} + func (p *ChaptersPage) updateChapterEntry(row int, col int) { chapterNo, err := strconv.Atoi(p.chaptersTable.t.GetCell(row, 0).Text) if err != nil { diff --git a/internal/ui/configPage.go b/internal/ui/configPage.go index 099f066..e884708 100644 --- a/internal/ui/configPage.go +++ b/internal/ui/configPage.go @@ -29,6 +29,8 @@ type ConfigPage struct { useMockField *tview.Checkbox saveMockField *tview.Checkbox outputDir *tview.InputField + copyToOutputDir *tview.Checkbox + tmpDir *tview.InputField // audiobook build config section concurrentDownloaders *tview.InputField @@ -40,9 +42,8 @@ type ConfigPage struct { shortenTitles *tview.Checkbox // audiobookshelf config section - copyToAudiobookshelf *tview.Checkbox + uploadToAudiobookshelf *tview.Checkbox audiobookshelfUrl *tview.InputField - audiobookshelfDir *tview.InputField audiobookshelfUser *tview.InputField audiobookshelfPassword *tview.InputField audiobookshelfLibrary *tview.InputField @@ -67,20 +68,21 @@ func newConfigPage(dispatcher *mq.Dispatcher) *ConfigPage { p.configSection.SetTitle(" Audiobook Builder Configuration: ") p.configSection.SetTitleAlign(tview.AlignLeft) - configFormLeft := newForm() configFormLeft.SetHorizontal(false) p.searchCondition = configFormLeft.AddInputField("Default Search condition:", "", 40, nil, func(t string) { p.configCopy.SetSearchCondition(t) }) p.maxSearchRows = configFormLeft.AddInputField("Maximum rows in the search result:", "", 4, acceptInt, func(t string) { p.configCopy.SetSearchRowsMax(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.f, 0, 0, 1, 1, 0, 0, true) configFormRight := newForm() configFormRight.SetHorizontal(false) - p.outputDir = configFormRight.AddInputField("Output (working) directory:", "", 40, nil, func(t string) { p.configCopy.SetOutputDir(t) }) + 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.logLevelField = configFormRight.AddDropdown("Log level:", utils.AddSpaces(logger.LogLeves()), 1, func(o string, i int) { p.configCopy.SetLogLevel(strings.TrimSpace(o)) }) - p.useMockField = configFormRight.AddCheckbox("Use mock:", false, func(t bool) { p.configCopy.SetUseMock(t) }) - p.saveMockField = configFormRight.AddCheckbox("Save mock:", false, func(t bool) { p.configCopy.SetSaveMock(t) }) p.configSection.AddItem(configFormRight.f, 0, 1, 1, 1, 0, 0, true) buttonsForm := newForm() @@ -125,9 +127,8 @@ func newConfigPage(dispatcher *mq.Dispatcher) *ConfigPage { absFormLeft := newForm() absFormLeft.SetHorizontal(false) - p.copyToAudiobookshelf = absFormLeft.AddCheckbox("Copy the audiobook to Audiobookshelf directory?", false, func(t bool) { p.configCopy.SetCopyToAudiobookshelf(t) }) + 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.audiobookshelfDir = absFormLeft.AddInputField("Audiobookshelf Server Directory:", "", 60, nil, func(t string) { p.configCopy.SetAudiobookshelfDir(t) }) p.audiobookshelfLibrary = absFormLeft.AddInputField("Audiobookshelf destination Library:", "", 40, nil, func(t string) { p.configCopy.SetAudiobookshelfLibrary(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) }) @@ -161,6 +162,9 @@ func (p *ConfigPage) dispatchMessage(m *mq.Message) { func (p *ConfigPage) displayConfig(c *dto.DisplayConfigCommand) { p.configCopy = c.Config p.outputDir.SetText(p.configCopy.GetOutputDir()) + p.copyToOutputDir.SetChecked(p.configCopy.IsCopyToOutputDir()) + p.tmpDir.SetText(p.configCopy.GetTmpDir()) + p.logFileNameField.SetText(p.configCopy.GetLogFileName()) p.logLevelField.SetCurrentOption(utils.GetIndex(logger.LogLeves(), p.configCopy.GetLogLevel())) p.searchCondition.SetText(p.configCopy.GetSearchCondition()) @@ -176,9 +180,8 @@ func (p *ConfigPage) displayConfig(c *dto.DisplayConfigCommand) { p.maxFileSize.SetText(utils.ToString(p.configCopy.GetMaxFileSizeMb())) p.shortenTitles.SetChecked(p.configCopy.IsShortenTitle()) - p.copyToAudiobookshelf.SetChecked(p.configCopy.IsCopyToAudiobookshelf()) + p.uploadToAudiobookshelf.SetChecked(p.configCopy.IsCopyToOutputDir()) p.audiobookshelfUrl.SetText(p.configCopy.GetAudiobookshelfUrl()) - p.audiobookshelfDir.SetText(p.configCopy.GetAudiobookshelfDir()) p.audiobookshelfLibrary.SetText(p.configCopy.GetAudiobookshelfLibrary()) p.audiobookshelfUser.SetText(p.configCopy.GetAudiobookshelfUser()) p.audiobookshelfPassword.SetText(p.configCopy.GetAudiobookshelfPassword()) diff --git a/internal/utils/filepath.go b/internal/utils/filepath.go index bbd2781..b28e73b 100644 --- a/internal/utils/filepath.go +++ b/internal/utils/filepath.go @@ -11,6 +11,9 @@ func SanitizeFilePath(path string) string { {"!", "."}, {"?", "."}, {"…", ""}, + {"#","N"}, + {"[", ""}, + {"]", ""}, } for { From 3e0dd0a53934f76445950b23e5b86e6acae068d1 Mon Sep 17 00:00:00 2001 From: vpoluyaktov Date: Tue, 21 Nov 2023 18:17:58 -0800 Subject: [PATCH 5/8] Audiobookshelf upload progress calculation implemented --- .gitignore | 2 +- internal/audiobookshelf/client.go | 22 +++- internal/config/config.go | 10 ++ .../controller/audiobookShelfController.go | 116 +++++++++++++++--- internal/controller/buildController.go | 1 + internal/controller/cleanupController.go | 2 + internal/controller/copyController.go | 2 +- internal/dto/audiobookshelf.go | 33 ++++- internal/dto/cleanup.go | 10 +- internal/dto/copy.go | 2 +- internal/mq/recepients.go | 1 - internal/ui/buildPage.go | 47 +++++-- internal/ui/configPage.go | 5 +- internal/ui/encodingPage.go | 2 +- 14 files changed, 216 insertions(+), 39 deletions(-) diff --git a/.gitignore b/.gitignore index 8484ab0..66f6cb5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,6 @@ bin/ mock/ -output/ +tmp/ dist/ __debug_bin* abb_ia.log diff --git a/internal/audiobookshelf/client.go b/internal/audiobookshelf/client.go index db591e5..514d2aa 100644 --- a/internal/audiobookshelf/client.go +++ b/internal/audiobookshelf/client.go @@ -141,7 +141,7 @@ func (c *AudiobookshelfClient) ScanLibrary(libraryID string) error { } // Upload audiobook to The Audibookshelf server -func (c *AudiobookshelfClient) UploadBook(ab *dto.Audiobook, libraryID string, folderID string, callback func(int, int) ) error { +func (c *AudiobookshelfClient) UploadBook(ab *dto.Audiobook, libraryID string, folderID string, callback Fn ) error { // Open each file for upload var filesList []*os.File for _, part := range ab.Parts { @@ -175,7 +175,9 @@ func (c *AudiobookshelfClient) UploadBook(ab *dto.Audiobook, libraryID string, f if err != nil { return err } - pr := &progressReader{ + pr := &ProgressReader{ + FileId: i, + FileName: filepath.Base(file.Name()), Reader: file, Size: fileStat.Size(), Callback: callback, @@ -214,16 +216,24 @@ func (c *AudiobookshelfClient) UploadBook(ab *dto.Audiobook, libraryID string, f return nil } -type progressReader struct { +// Progress Reader for file upload progress +type Fn func(fileId int, fileName string, size int64, pos int64, percent int) +type ProgressReader struct { + FileId int + FileName string Reader io.Reader Size int64 - Callback func(int, int) + Pos int64 + Percent int + Callback Fn } -func (pr *progressReader) Read(p []byte) (int, error) { +func (pr *ProgressReader) Read(p []byte) (int, error) { n, err := pr.Reader.Read(p) if err == nil { - pr.Callback(n, int(pr.Size)) + pr.Pos += int64(n) + pr.Percent = int(float64(pr.Pos) / float64(pr.Size) * 100) + pr.Callback(pr.FileId, pr.FileName, pr.Size, pr.Pos, pr.Percent) } return n, err } diff --git a/internal/config/config.go b/internal/config/config.go index 966b2b1..fff597d 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -42,6 +42,7 @@ type Config struct { SampleRateHz int `yaml:"SampleRateHz"` MaxFileSizeMb int `yaml:"MaxFileSizeMb"` UploadToAudiobookshef bool `yaml:"UploadToAudiobookshelf"` + ScanAudiobookshef bool `yaml:"ScanAudiobookshelf"` AudiobookshelfUrl string `yaml:"AudiobookshelfUrl"` AudiobookshelfUser string `yaml:"AudiobookshelfUser"` AudiobookshelfPassword string `yaml:"AudiobookshelfPassword"` @@ -84,6 +85,7 @@ func Load() { config.SampleRateHz = 44100 config.MaxFileSizeMb = 250 config.UploadToAudiobookshef = true + config.ScanAudiobookshef = true config.AudiobookshelfUser = "admin" config.AudiobookshelfPassword = "" config.AudiobookshelfLibrary = "Internet Archive" @@ -275,6 +277,14 @@ func (c *Config) IsUploadToAudiobookshef() bool { return c.UploadToAudiobookshef } +func (c *Config) SetScanAudiobookshelf(b bool) { + c.ScanAudiobookshef = b +} + +func (c *Config) IsScanAudiobookshef() bool { + return c.ScanAudiobookshef +} + func (c *Config) GetAudiobookshelfUrl() string { return c.AudiobookshelfUrl } diff --git a/internal/controller/audiobookShelfController.go b/internal/controller/audiobookShelfController.go index 8369ab3..87103b3 100644 --- a/internal/controller/audiobookShelfController.go +++ b/internal/controller/audiobookShelfController.go @@ -1,16 +1,31 @@ package controller import ( - "strconv" + "fmt" + "time" "github.com/vpoluyaktov/abb_ia/internal/audiobookshelf" "github.com/vpoluyaktov/abb_ia/internal/dto" "github.com/vpoluyaktov/abb_ia/internal/logger" "github.com/vpoluyaktov/abb_ia/internal/mq" + "github.com/vpoluyaktov/abb_ia/internal/utils" ) type AudiobookshelfController struct { - mq *mq.Dispatcher + mq *mq.Dispatcher + ab *dto.Audiobook + startTime time.Time + stopFlag bool + + // progress tracking arrays + filesUpload []fileUpload +} + +type fileUpload struct { + fileId int + fileSize int64 + bytesCopied int64 + progress int } func NewAudiobookshelfController(dispatcher *mq.Dispatcher) *AudiobookshelfController { @@ -74,15 +89,16 @@ func (c *AudiobookshelfController) audiobookshelfScan(cmd *dto.AudiobookshelfSca } logger.Info("A scan launched for library " + libraryName + " on audiobookshlf server") } + c.mq.SendMessage(mq.AudiobookshelfController, mq.BuildPage, &dto.ScanComplete{Audiobook: cmd.Audiobook}, true) } func (c *AudiobookshelfController) uploadAudiobook(cmd *dto.AudiobookshelfUploadCommand) { logger.Info(mq.AudiobookshelfController + " received " + cmd.String()) - ab := cmd.Audiobook - url := ab.Config.GetAudiobookshelfUrl() - username := ab.Config.GetAudiobookshelfUser() - password := ab.Config.GetAudiobookshelfPassword() - libraryName := ab.Config.GetAudiobookshelfLibrary() + c.ab = cmd.Audiobook + url := c.ab.Config.GetAudiobookshelfUrl() + username := c.ab.Config.GetAudiobookshelfUser() + password := c.ab.Config.GetAudiobookshelfPassword() + libraryName := c.ab.Config.GetAudiobookshelfLibrary() if url != "" && username != "" && password != "" && libraryName != "" { absClient := audiobookshelf.NewClient(url) @@ -106,20 +122,92 @@ func (c *AudiobookshelfController) uploadAudiobook(cmd *dto.AudiobookshelfUpload logger.Error("Can't get a folder for library: " + err.Error()) return } - // TODO: Check if a folder selector is needed here. Let's use first folder in a library for upload - err = absClient.UploadBook(ab, libraryID, folders[0].ID, c.updateFileUplodProgress) + folderID := folders[0].ID + + c.stopFlag = false + c.filesUpload = make([]fileUpload, len(c.ab.Parts)) + go c.updateTotalUploadProgress() + err = absClient.UploadBook(c.ab, libraryID, folderID, c.updateFileUplodProgress) + if err != nil { logger.Error("Can't upload the audiobook to audiobookshelf server: " + err.Error()) - return } + c.stopFlag = true } + c.mq.SendMessage(mq.AudiobookshelfController, mq.BuildPage, &dto.UploadComplete{Audiobook: cmd.Audiobook}, true) } -func (c *AudiobookshelfController) updateFileUplodProgress(bytesUploaded int, totalBytes int) { +func (c *AudiobookshelfController) updateFileUplodProgress(fileId int, fileName string, size int64, pos int64, percent int) { - if totalBytes != 0 { - percent := bytesUploaded / totalBytes * 100 - logger.Debug("Upload percent: " + strconv.Itoa(percent)) + if c.filesUpload[fileId].progress != percent { + // wrong calculation protection + if percent < 0 { + percent = 0 + } else if percent > 100 { + percent = 100 + } + + // sent a message only if progress changed + c.mq.SendMessage(mq.AudiobookshelfController, mq.BuildPage, &dto.UploadFileProgress{FileId: fileId, FileName: fileName, Percent: percent}, false) + } + c.filesUpload[fileId].fileId = fileId + c.filesUpload[fileId].fileSize = size + c.filesUpload[fileId].bytesCopied = pos + c.filesUpload[fileId].progress = percent +} + +func (c *AudiobookshelfController) updateTotalUploadProgress() { + var percent int = -1 + + for !c.stopFlag && percent <= 100 { + var totalSize = c.ab.TotalSize + var totalBytesCopied int64 = 0 + filesCopied := 0 + for _, f := range c.filesUpload { + totalBytesCopied += f.bytesCopied + if f.progress == 100 { + filesCopied++ + } + } + + var p int = 0 + if totalSize > 0 { + p = int(float64(totalBytesCopied) / float64(totalSize) * 100) + } + + // fix wrong incorrect calculation + if filesCopied == len(c.filesUpload) { + p = 100 + totalBytesCopied = c.ab.TotalSize + } + + if percent != p { + // sent a message only if progress changed + percent = p + + // wrong calculation protection + if percent < 0 { + percent = 0 + } else if percent > 100 { + percent = 100 + } + + elapsed := time.Since(c.startTime).Seconds() + speed := int64(float64(totalBytesCopied) / elapsed) + eta := (100 / (float64(percent) / elapsed)) - elapsed + if eta < 0 || eta > (60*60*24*365) { + eta = 0 + } + + elapsedH := utils.SecondsToTime(elapsed) + bytesH := utils.BytesToHuman(totalBytesCopied) + filesH := fmt.Sprintf("%d/%d", filesCopied, len(c.ab.Parts)) + speedH := utils.SpeedToHuman(speed) + etaH := utils.SecondsToTime(eta) + + c.mq.SendMessage(mq.AudiobookshelfController, mq.BuildPage, &dto.UploadProgress{Elapsed: elapsedH, Percent: percent, Files: filesH, Bytes: bytesH, Speed: speedH, ETA: etaH}, false) + } + time.Sleep(mq.PullFrequency) } } diff --git a/internal/controller/buildController.go b/internal/controller/buildController.go index ad0a20e..e6d2690 100644 --- a/internal/controller/buildController.go +++ b/internal/controller/buildController.go @@ -85,6 +85,7 @@ func (c *BuildController) startBuild(cmd *dto.BuildCommand) { } part.AACFile = filePath + ".aac" part.M4BFile = filePath + ".m4b" + c.files[i].fileName = part.M4BFile c.files[i].totalDuration = part.Duration } diff --git a/internal/controller/cleanupController.go b/internal/controller/cleanupController.go index 7dc6902..204f2c3 100644 --- a/internal/controller/cleanupController.go +++ b/internal/controller/cleanupController.go @@ -52,4 +52,6 @@ func (c *CleanupController) cleanUp(cmd *dto.CleanupCommand) { } } os.Remove(c.ab.CoverFile) + + c.mq.SendMessage(mq.CleanupController, mq.BuildPage, &dto.CleanupComplete{Audiobook: cmd.Audiobook}, true) } diff --git a/internal/controller/copyController.go b/internal/controller/copyController.go index a6250ba..0b2ff35 100644 --- a/internal/controller/copyController.go +++ b/internal/controller/copyController.go @@ -193,7 +193,7 @@ func (c *CopyController) updateFileCopyProgress(fileId int, fileName string, siz } // sent a message only if progress changed - c.mq.SendMessage(mq.CopyController, mq.BuildPage, &dto.CopyFileProgress{FileId: fileId, FileName: fileName, Percent: percent}, false) + c.mq.SendMessage(mq.CopyController, mq.BuildPage, &dto.UploadFileProgress{FileId: fileId, FileName: fileName, Percent: percent}, false) } c.filesCopy[fileId].fileId = fileId c.filesCopy[fileId].fileSize = size diff --git a/internal/dto/audiobookshelf.go b/internal/dto/audiobookshelf.go index 1dd99c3..ef7bb98 100644 --- a/internal/dto/audiobookshelf.go +++ b/internal/dto/audiobookshelf.go @@ -10,6 +10,14 @@ func (c *AudiobookshelfScanCommand) String() string { return fmt.Sprintf("AudiobookshelfScanCommand: %s", c.Audiobook.String()) } +type ScanComplete struct { + Audiobook *Audiobook +} + +func (c *ScanComplete) String() string { + return fmt.Sprintf("ScanComplete: %s", c.Audiobook.String()) +} + type AudiobookshelfUploadCommand struct { Audiobook *Audiobook } @@ -18,10 +26,33 @@ func (c *AudiobookshelfUploadCommand) String() string { return fmt.Sprintf("AudiobookshelfUploadCommand: %s", c.Audiobook.String()) } +type UploadFileProgress struct { + FileId int + FileName string + Percent int +} + +func (c *UploadFileProgress) String() string { + return fmt.Sprintf("UploadFileProgress: %d, %s, %d", c.FileId, c.FileName, c.Percent) +} + +type UploadProgress struct { + Elapsed string // time since started + Percent int + Files string // files encoded + Bytes string // total bytes copied + Speed string // encode speed bytes/s + ETA string // ETA in seconds +} + +func (c *UploadProgress) String() string { + return fmt.Sprintf("UploadProgress: %d", c.Percent) +} + type UploadComplete struct { Audiobook *Audiobook } func (c *UploadComplete) String() string { return fmt.Sprintf("UploadComplete: %s", c.Audiobook.String()) -} \ No newline at end of file +} diff --git a/internal/dto/cleanup.go b/internal/dto/cleanup.go index fcf3f35..dbb5238 100644 --- a/internal/dto/cleanup.go +++ b/internal/dto/cleanup.go @@ -8,4 +8,12 @@ type CleanupCommand struct { func (c *CleanupCommand) String() string { return fmt.Sprintf("CleanupCommand: %s", c.Audiobook.String()) -} \ No newline at end of file +} + +type CleanupComplete struct { + Audiobook *Audiobook +} + +func (c *CleanupComplete) String() string { + return fmt.Sprintf("CleanupComplete: %s", c.Audiobook.String()) +} diff --git a/internal/dto/copy.go b/internal/dto/copy.go index 9bc8722..2f75505 100644 --- a/internal/dto/copy.go +++ b/internal/dto/copy.go @@ -39,4 +39,4 @@ type CopyComplete struct { func (c *CopyComplete) String() string { return fmt.Sprintf("CopyComplete: %s", c.Audiobook.String()) -} \ No newline at end of file +} diff --git a/internal/mq/recepients.go b/internal/mq/recepients.go index 90c7a8c..b5659a9 100644 --- a/internal/mq/recepients.go +++ b/internal/mq/recepients.go @@ -21,7 +21,6 @@ const ( ChaptersController = "ChaptersController" BuildController = "BuildController" CopyController = "CopyController" - UploadController = "UploadController" CleanupController = "CleanupController" AudiobookshelfController = "AudiobookshelfController" ) diff --git a/internal/ui/buildPage.go b/internal/ui/buildPage.go index 6e58321..6f42149 100644 --- a/internal/ui/buildPage.go +++ b/internal/ui/buildPage.go @@ -120,7 +120,7 @@ func (p *BuildPage) dispatchMessage(m *mq.Message) { p.updateTotalBuildProgress(dto) case *dto.BuildComplete: p.buildComplete(dto) - case *dto.CopyFileProgress: + case *dto.UploadFileProgress: p.updateFileCopyProgress(dto) case *dto.CopyProgress: p.updateTotalCopyProgress(dto) @@ -128,6 +128,10 @@ func (p *BuildPage) dispatchMessage(m *mq.Message) { p.copyComplete(dto) case *dto.UploadComplete: p.uploadComplete(dto) + case *dto.ScanComplete: + p.scanComplete(dto) + case *dto.CleanupComplete: + p.cleanupComplete(dto) default: m.UnsupportedTypeError(mq.BuildPage) } @@ -207,7 +211,7 @@ func (p *BuildPage) updateTotalBuildProgress(dp *dto.BuildProgress) { p.mq.SendMessage(mq.BuildPage, mq.TUI, &dto.DrawCommand{Primitive: nil}, false) } -func (p *BuildPage) updateFileCopyProgress(dp *dto.CopyFileProgress) { +func (p *BuildPage) updateFileCopyProgress(dp *dto.UploadFileProgress) { // update file progress col := 5 w := p.copyTable.getColumnWidth(col) - 3 @@ -244,28 +248,49 @@ func (p *BuildPage) updateTotalCopyProgress(dp *dto.CopyProgress) { p.mq.SendMessage(mq.BuildPage, mq.TUI, &dto.DrawCommand{Primitive: nil}, false) } +/* + * A chain of final operations -> ?Copy -> ?Upload -> ?Scan -> Cleanup - Done msg + */ func (p *BuildPage) buildComplete(c *dto.BuildComplete) { // copy the book to Output directory if needed ab := c.Audiobook if ab.Config.IsCopyToOutputDir() { - p.mq.SendMessage(mq.BuildPage, mq.CopyController, &dto.CopyCommand{Audiobook: c.Audiobook}, true) + p.mq.SendMessage(mq.BuildPage, mq.CopyController, &dto.CopyCommand{Audiobook: ab}, true) + } else { + p.mq.SendMessage(mq.BuildPage, mq.BuildPage, &dto.CopyComplete{Audiobook: ab}, true) } +} +func (p *BuildPage) copyComplete(c *dto.CopyComplete) { + // upload the book to Audiobookshelf server if needed + ab := c.Audiobook if ab.Config.IsUploadToAudiobookshef() { - p.mq.SendMessage(mq.BuildPage, mq.AudiobookshelfController, &dto.AudiobookshelfUploadCommand{Audiobook: c.Audiobook}, true) + p.mq.SendMessage(mq.BuildPage, mq.AudiobookshelfController, &dto.AudiobookshelfUploadCommand{Audiobook: ab}, true) + } else { + p.mq.SendMessage(mq.BuildPage, mq.BuildPage, &dto.AudiobookshelfScanCommand{Audiobook: ab}, true) } } -func (p *BuildPage) copyComplete(c *dto.CopyComplete) { - p.mq.SendMessage(mq.BuildPage, mq.CleanupController, &dto.CleanupCommand{Audiobook: c.Audiobook}, true) - p.mq.SendMessage(mq.BuildPage, mq.AudiobookshelfController, &dto.AudiobookshelfScanCommand{Audiobook: c.Audiobook}, true) - newMessageDialog(p.mq, "Build Complete", "Audiobook has been created", p.buildSection) - //p.mq.SendMessage(mq.BuildPage, mq.Frame, &dto.SwitchToPageCommand{Name: "SearchPage"}, false) +func (p *BuildPage) uploadComplete(c *dto.UploadComplete) { + // launch Audiobookshelf library scan if needed + ab := c.Audiobook + if ab.Config.IsScanAudiobookshef() { + p.mq.SendMessage(mq.BuildPage, mq.AudiobookshelfController, &dto.AudiobookshelfScanCommand{Audiobook: c.Audiobook}, true) + } else { + p.mq.SendMessage(mq.BuildPage, mq.BuildPage, &dto.ScanComplete{Audiobook: ab}, true) + } } -func (p *BuildPage) uploadComplete(c *dto.UploadComplete) { +func (p *BuildPage) scanComplete(c *dto.ScanComplete) { + // clean up temporary directory p.mq.SendMessage(mq.BuildPage, mq.CleanupController, &dto.CleanupCommand{Audiobook: c.Audiobook}, true) - p.mq.SendMessage(mq.BuildPage, mq.AudiobookshelfController, &dto.AudiobookshelfScanCommand{Audiobook: c.Audiobook}, true) +} + +func (p *BuildPage) cleanupComplete(c *dto.CleanupComplete) { + p.bookReadyMgs(c.Audiobook) +} + +func (p *BuildPage) bookReadyMgs(ab *dto.Audiobook) { newMessageDialog(p.mq, "Build Complete", "Audiobook has been created", p.buildSection) //p.mq.SendMessage(mq.BuildPage, mq.Frame, &dto.SwitchToPageCommand{Name: "SearchPage"}, false) } diff --git a/internal/ui/configPage.go b/internal/ui/configPage.go index e884708..787ed6f 100644 --- a/internal/ui/configPage.go +++ b/internal/ui/configPage.go @@ -47,6 +47,7 @@ type ConfigPage struct { audiobookshelfUser *tview.InputField audiobookshelfPassword *tview.InputField audiobookshelfLibrary *tview.InputField + scanAudiobookshelf *tview.Checkbox saveConfigButton *tview.Button cancelButton *tview.Button @@ -129,9 +130,10 @@ func newConfigPage(dispatcher *mq.Dispatcher) *ConfigPage { 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.audiobookshelfLibrary = absFormLeft.AddInputField("Audiobookshelf destination Library:", "", 40, nil, func(t string) { p.configCopy.SetAudiobookshelfLibrary(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.scanAudiobookshelf = absFormLeft.AddCheckbox("Scan the Audiobookshelf library after copy/upload?", false, func(t bool) { p.configCopy.SetScanAudiobookshelf(t) }) p.absSection.AddItem(absFormLeft.f, 0, 0, 1, 1, 0, 0, true) // absFormRight := newForm() @@ -183,6 +185,7 @@ func (p *ConfigPage) displayConfig(c *dto.DisplayConfigCommand) { p.uploadToAudiobookshelf.SetChecked(p.configCopy.IsCopyToOutputDir()) p.audiobookshelfUrl.SetText(p.configCopy.GetAudiobookshelfUrl()) p.audiobookshelfLibrary.SetText(p.configCopy.GetAudiobookshelfLibrary()) + p.scanAudiobookshelf.SetChecked(p.configCopy.IsScanAudiobookshef()) p.audiobookshelfUser.SetText(p.configCopy.GetAudiobookshelfUser()) p.audiobookshelfPassword.SetText(p.configCopy.GetAudiobookshelfPassword()) diff --git a/internal/ui/encodingPage.go b/internal/ui/encodingPage.go index 625c62e..77f1bc1 100644 --- a/internal/ui/encodingPage.go +++ b/internal/ui/encodingPage.go @@ -136,7 +136,7 @@ func (p *EncodingPage) stopEncoding() { func (p *EncodingPage) updateFileProgress(dp *dto.EncodingFileProgress) { col := 5 - w := p.filesTable.getColumnWidth(col) - 5 + w := p.filesTable.getColumnWidth(col) - 4 progressText := fmt.Sprintf(" %3d%% ", dp.Percent) barWidth := int((float32((w - len(progressText))) * float32(dp.Percent) / 100)) progressBar := strings.Repeat("━", barWidth) + strings.Repeat(" ", w-len(progressText)-barWidth) From 2b6db689f5b77a0fde05ee233a2dd297bec645e4 Mon Sep 17 00:00:00 2001 From: vpoluyaktov Date: Wed, 22 Nov 2023 10:06:02 -0800 Subject: [PATCH 6/8] Imports refactored --- .goreleaser.yaml | 2 +- README.md | 2 +- cmd/root.go | 8 ++++---- go.mod | 2 +- go.sum | 3 --- internal/audiobookshelf/audiobookshelf_test.go | 4 ++-- internal/audiobookshelf/client.go | 5 ++--- internal/config/config.go | 4 ++-- internal/controller/audiobookShelfController.go | 10 +++++----- internal/controller/bootController.go | 10 +++++----- internal/controller/buildController.go | 12 ++++++------ internal/controller/chaptersController.go | 8 ++++---- internal/controller/cleanupController.go | 6 +++--- internal/controller/conductor.go | 2 +- internal/controller/configController.go | 8 ++++---- internal/controller/copyController.go | 8 ++++---- internal/controller/downloadController.go | 10 +++++----- internal/controller/encodingController.go | 12 ++++++------ internal/controller/searchController.go | 12 ++++++------ internal/dto/audiobook.go | 2 +- internal/dto/config.go | 2 +- internal/ffmpeg/ffmpeg.go | 2 +- internal/ia_client/ia_client.go | 6 +++--- internal/ia_client/is_client_test.go | 8 ++++---- internal/mq/dispatcher.go | 4 ++-- internal/mq/message.go | 4 ++-- internal/ui/buildPage.go | 6 +++--- internal/ui/chaptersPage.go | 10 +++++----- internal/ui/configPage.go | 12 ++++++------ internal/ui/dialog.go | 9 +++++---- internal/ui/downloadPage.go | 6 +++--- internal/ui/encodingPage.go | 6 +++--- internal/ui/footer.go | 6 +++--- internal/ui/frame.go | 4 ++-- internal/ui/header.go | 12 ++++++------ internal/ui/searchPage.go | 12 ++++++------ internal/ui/tui.go | 4 ++-- internal/utils/human.go | 2 +- internal/utils/job_dispatcher.go | 2 +- internal/utils/utils_test.go | 6 +++--- main.go | 8 ++++---- main_test.go | 4 ++-- tools/check_error.go | 2 +- 43 files changed, 132 insertions(+), 135 deletions(-) diff --git a/.goreleaser.yaml b/.goreleaser.yaml index 349f047..7697a55 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -8,7 +8,7 @@ builds: - id: default main: ./main.go binary: abb_ia - ldflags: -X github.com/vpoluyaktov/abb_ia/internal/config.appVersion={{.Version}} -X github.com/vpoluyaktov/abb_ia/internal/config.buildDate={{.Date}} + ldflags: -X abb_ia/internal/config.appVersion={{.Version}} -X abb_ia/internal/config.buildDate={{.Date}} env: [CGO_ENABLED=0] goos: - linux diff --git a/README.md b/README.md index 6f0fc18..f029b61 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ The Internet Archive site offers a vast collection of free "old-time radio" show To make this process easier, I developed Audiobook Builder. With this app, all you need is the name of a show or book, or a direct link on archive.org. It will download the .mp3 files for the book, re-encode them with the same bit rate, generate a list of chapters (which can be edited during the process), and ultimately create an audiobook in .m4b format.
-![abb_ia](https://github.com/vpoluyaktov/abb_ia/assets/1992836/511505d9-85b2-4562-9a5b-a71c4c25d564) +![abb_ia](https://abb_ia/assets/1992836/511505d9-85b2-4562-9a5b-a71c4c25d564)

diff --git a/cmd/root.go b/cmd/root.go index 5f0b6a2..7cdaf6e 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -1,10 +1,10 @@ package cmd import ( - "github.com/vpoluyaktov/abb_ia/internal/controller" - "github.com/vpoluyaktov/abb_ia/internal/logger" - "github.com/vpoluyaktov/abb_ia/internal/mq" - "github.com/vpoluyaktov/abb_ia/internal/ui" + "abb_ia/internal/controller" + "abb_ia/internal/logger" + "abb_ia/internal/mq" + "abb_ia/internal/ui" ) func Execute() { diff --git a/go.mod b/go.mod index a1817d1..c7e8f27 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module github.com/vpoluyaktov/abb_ia +module abb_ia go 1.18 diff --git a/go.sum b/go.sum index b9f8304..005a203 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,3 @@ -github.com/bogem/id3v2/v2 v2.1.4 h1:CEwe+lS2p6dd9UZRlPc1zbFNIha2mb2qzT1cCEoNWoI= -github.com/bogem/id3v2/v2 v2.1.4/go.mod h1:l+gR8MZ6rc9ryPTPkX77smS5Me/36gxkMgDayZ9G1vY= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 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= @@ -66,7 +64,6 @@ golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= diff --git a/internal/audiobookshelf/audiobookshelf_test.go b/internal/audiobookshelf/audiobookshelf_test.go index 945c7b8..0e28d28 100644 --- a/internal/audiobookshelf/audiobookshelf_test.go +++ b/internal/audiobookshelf/audiobookshelf_test.go @@ -3,9 +3,9 @@ package audiobookshelf_test import ( "testing" + "abb_ia/internal/audiobookshelf" + "abb_ia/internal/config" "github.com/stretchr/testify/assert" - "github.com/vpoluyaktov/abb_ia/internal/audiobookshelf" - "github.com/vpoluyaktov/abb_ia/internal/config" ) func TestLogin(t *testing.T) { diff --git a/internal/audiobookshelf/client.go b/internal/audiobookshelf/client.go index 514d2aa..7b12be4 100644 --- a/internal/audiobookshelf/client.go +++ b/internal/audiobookshelf/client.go @@ -12,7 +12,7 @@ import ( "path/filepath" "strconv" - "github.com/vpoluyaktov/abb_ia/internal/dto" + "abb_ia/internal/dto" ) type AudiobookshelfClient struct { @@ -114,7 +114,6 @@ func (c *AudiobookshelfClient) GetFolders(libraries []Library, libraryName strin return nil, fmt.Errorf("no library with name '%s' found", libraryName) } - // Call the Audiobookshelf API for a library Scan func (c *AudiobookshelfClient) ScanLibrary(libraryID string) error { url := c.url + "/api/libraries/" + libraryID + "/scan" @@ -141,7 +140,7 @@ func (c *AudiobookshelfClient) ScanLibrary(libraryID string) error { } // Upload audiobook to The Audibookshelf server -func (c *AudiobookshelfClient) UploadBook(ab *dto.Audiobook, libraryID string, folderID string, callback Fn ) error { +func (c *AudiobookshelfClient) UploadBook(ab *dto.Audiobook, libraryID string, folderID string, callback Fn) error { // Open each file for upload var filesList []*os.File for _, part := range ab.Parts { diff --git a/internal/config/config.go b/internal/config/config.go index fff597d..10af4aa 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -5,8 +5,8 @@ import ( "io/ioutil" "time" - "github.com/vpoluyaktov/abb_ia/internal/logger" - "github.com/vpoluyaktov/abb_ia/internal/utils" + "abb_ia/internal/logger" + "abb_ia/internal/utils" "gopkg.in/yaml.v3" ) diff --git a/internal/controller/audiobookShelfController.go b/internal/controller/audiobookShelfController.go index 87103b3..4dba73e 100644 --- a/internal/controller/audiobookShelfController.go +++ b/internal/controller/audiobookShelfController.go @@ -4,11 +4,11 @@ import ( "fmt" "time" - "github.com/vpoluyaktov/abb_ia/internal/audiobookshelf" - "github.com/vpoluyaktov/abb_ia/internal/dto" - "github.com/vpoluyaktov/abb_ia/internal/logger" - "github.com/vpoluyaktov/abb_ia/internal/mq" - "github.com/vpoluyaktov/abb_ia/internal/utils" + "abb_ia/internal/audiobookshelf" + "abb_ia/internal/dto" + "abb_ia/internal/logger" + "abb_ia/internal/mq" + "abb_ia/internal/utils" ) type AudiobookshelfController struct { diff --git a/internal/controller/bootController.go b/internal/controller/bootController.go index c6e8d50..3f9c7af 100644 --- a/internal/controller/bootController.go +++ b/internal/controller/bootController.go @@ -3,11 +3,11 @@ package controller import ( "time" - "github.com/vpoluyaktov/abb_ia/internal/config" - "github.com/vpoluyaktov/abb_ia/internal/dto" - "github.com/vpoluyaktov/abb_ia/internal/logger" - "github.com/vpoluyaktov/abb_ia/internal/mq" - "github.com/vpoluyaktov/abb_ia/internal/utils" + "abb_ia/internal/config" + "abb_ia/internal/dto" + "abb_ia/internal/logger" + "abb_ia/internal/mq" + "abb_ia/internal/utils" ) type BootController struct { diff --git a/internal/controller/buildController.go b/internal/controller/buildController.go index e6d2690..b92b142 100644 --- a/internal/controller/buildController.go +++ b/internal/controller/buildController.go @@ -11,12 +11,12 @@ import ( "strings" "time" - "github.com/vpoluyaktov/abb_ia/internal/dto" - "github.com/vpoluyaktov/abb_ia/internal/ffmpeg" - "github.com/vpoluyaktov/abb_ia/internal/utils" + "abb_ia/internal/dto" + "abb_ia/internal/ffmpeg" + "abb_ia/internal/utils" - "github.com/vpoluyaktov/abb_ia/internal/logger" - "github.com/vpoluyaktov/abb_ia/internal/mq" + "abb_ia/internal/logger" + "abb_ia/internal/mq" ) type BuildController struct { @@ -151,7 +151,7 @@ func (c *BuildController) createMetadata(ab *dto.Audiobook) { f.WriteString("genre=" + ab.Genre + "\n") f.WriteString("description=" + strings.ReplaceAll(ab.Description, "\n", "\\\n") + "\n") f.WriteString("copyright=" + ab.LicenseUrl + "\n") - f.WriteString("comment=This audiobook was created using the 'Audiobook Builder' tool: https://github.com/vpoluyaktov/abb_ia\\\n" + + f.WriteString("comment=This audiobook was created using the 'Audiobook Builder' tool: https://abb_ia\\\n" + "The audio files used for this book were obtained from the Internet Archive site: " + ab.IaURL + "\n") for _, chapter := range part.Chapters { diff --git a/internal/controller/chaptersController.go b/internal/controller/chaptersController.go index 1f8ef35..7748823 100644 --- a/internal/controller/chaptersController.go +++ b/internal/controller/chaptersController.go @@ -5,10 +5,10 @@ import ( "regexp" "strings" - "github.com/vpoluyaktov/abb_ia/internal/dto" - "github.com/vpoluyaktov/abb_ia/internal/ffmpeg" - "github.com/vpoluyaktov/abb_ia/internal/logger" - "github.com/vpoluyaktov/abb_ia/internal/mq" + "abb_ia/internal/dto" + "abb_ia/internal/ffmpeg" + "abb_ia/internal/logger" + "abb_ia/internal/mq" ) type ChaptersController struct { diff --git a/internal/controller/cleanupController.go b/internal/controller/cleanupController.go index 204f2c3..81b29ab 100644 --- a/internal/controller/cleanupController.go +++ b/internal/controller/cleanupController.go @@ -3,9 +3,9 @@ package controller import ( "os" - "github.com/vpoluyaktov/abb_ia/internal/dto" - "github.com/vpoluyaktov/abb_ia/internal/logger" - "github.com/vpoluyaktov/abb_ia/internal/mq" + "abb_ia/internal/dto" + "abb_ia/internal/logger" + "abb_ia/internal/mq" ) type CleanupController struct { diff --git a/internal/controller/conductor.go b/internal/controller/conductor.go index f064e00..35d8190 100644 --- a/internal/controller/conductor.go +++ b/internal/controller/conductor.go @@ -3,7 +3,7 @@ package controller import ( "time" - "github.com/vpoluyaktov/abb_ia/internal/mq" + "abb_ia/internal/mq" ) type controller interface { diff --git a/internal/controller/configController.go b/internal/controller/configController.go index 01a5b51..dc28e50 100644 --- a/internal/controller/configController.go +++ b/internal/controller/configController.go @@ -1,10 +1,10 @@ package controller import ( - "github.com/vpoluyaktov/abb_ia/internal/config" - "github.com/vpoluyaktov/abb_ia/internal/dto" - "github.com/vpoluyaktov/abb_ia/internal/logger" - "github.com/vpoluyaktov/abb_ia/internal/mq" + "abb_ia/internal/config" + "abb_ia/internal/dto" + "abb_ia/internal/logger" + "abb_ia/internal/mq" ) type ConfigController struct { diff --git a/internal/controller/copyController.go b/internal/controller/copyController.go index 0b2ff35..ac9b4e4 100644 --- a/internal/controller/copyController.go +++ b/internal/controller/copyController.go @@ -8,11 +8,11 @@ import ( "path/filepath" "time" - "github.com/vpoluyaktov/abb_ia/internal/dto" - "github.com/vpoluyaktov/abb_ia/internal/utils" + "abb_ia/internal/dto" + "abb_ia/internal/utils" - "github.com/vpoluyaktov/abb_ia/internal/logger" - "github.com/vpoluyaktov/abb_ia/internal/mq" + "abb_ia/internal/logger" + "abb_ia/internal/mq" ) /** diff --git a/internal/controller/downloadController.go b/internal/controller/downloadController.go index 1c4f8f8..7ac74bb 100644 --- a/internal/controller/downloadController.go +++ b/internal/controller/downloadController.go @@ -5,11 +5,11 @@ import ( "path/filepath" "time" - "github.com/vpoluyaktov/abb_ia/internal/dto" - "github.com/vpoluyaktov/abb_ia/internal/ia_client" - "github.com/vpoluyaktov/abb_ia/internal/logger" - "github.com/vpoluyaktov/abb_ia/internal/mq" - "github.com/vpoluyaktov/abb_ia/internal/utils" + "abb_ia/internal/dto" + "abb_ia/internal/ia_client" + "abb_ia/internal/logger" + "abb_ia/internal/mq" + "abb_ia/internal/utils" ) type DownloadController struct { diff --git a/internal/controller/encodingController.go b/internal/controller/encodingController.go index c1197cb..8b6212e 100644 --- a/internal/controller/encodingController.go +++ b/internal/controller/encodingController.go @@ -8,12 +8,12 @@ import ( "strconv" "time" - "github.com/vpoluyaktov/abb_ia/internal/config" - "github.com/vpoluyaktov/abb_ia/internal/dto" - "github.com/vpoluyaktov/abb_ia/internal/ffmpeg" - "github.com/vpoluyaktov/abb_ia/internal/logger" - "github.com/vpoluyaktov/abb_ia/internal/mq" - "github.com/vpoluyaktov/abb_ia/internal/utils" + "abb_ia/internal/config" + "abb_ia/internal/dto" + "abb_ia/internal/ffmpeg" + "abb_ia/internal/logger" + "abb_ia/internal/mq" + "abb_ia/internal/utils" ) type EncodingController struct { diff --git a/internal/controller/searchController.go b/internal/controller/searchController.go index cd409ee..225a682 100644 --- a/internal/controller/searchController.go +++ b/internal/controller/searchController.go @@ -6,13 +6,13 @@ import ( "strconv" "strings" + "abb_ia/internal/config" + "abb_ia/internal/dto" + "abb_ia/internal/ia_client" + "abb_ia/internal/logger" + "abb_ia/internal/mq" + "abb_ia/internal/utils" "github.com/rivo/tview" - "github.com/vpoluyaktov/abb_ia/internal/config" - "github.com/vpoluyaktov/abb_ia/internal/dto" - "github.com/vpoluyaktov/abb_ia/internal/ia_client" - "github.com/vpoluyaktov/abb_ia/internal/logger" - "github.com/vpoluyaktov/abb_ia/internal/mq" - "github.com/vpoluyaktov/abb_ia/internal/utils" ) type SearchController struct { diff --git a/internal/dto/audiobook.go b/internal/dto/audiobook.go index 3fdb0b8..82d775e 100644 --- a/internal/dto/audiobook.go +++ b/internal/dto/audiobook.go @@ -4,7 +4,7 @@ import ( "encoding/json" "fmt" - "github.com/vpoluyaktov/abb_ia/internal/config" + "abb_ia/internal/config" ) type Audiobook struct { diff --git a/internal/dto/config.go b/internal/dto/config.go index 4a3db36..ae8ca40 100644 --- a/internal/dto/config.go +++ b/internal/dto/config.go @@ -3,7 +3,7 @@ package dto import ( "fmt" - "github.com/vpoluyaktov/abb_ia/internal/config" + "abb_ia/internal/config" ) type DisplayConfigCommand struct { diff --git a/internal/ffmpeg/ffmpeg.go b/internal/ffmpeg/ffmpeg.go index d61fd43..3393d27 100644 --- a/internal/ffmpeg/ffmpeg.go +++ b/internal/ffmpeg/ffmpeg.go @@ -3,7 +3,7 @@ package ffmpeg import ( "os/exec" - "github.com/vpoluyaktov/abb_ia/internal/logger" + "abb_ia/internal/logger" ) type FFmpeg struct { diff --git a/internal/ia_client/ia_client.go b/internal/ia_client/ia_client.go index d55e533..02d1acf 100644 --- a/internal/ia_client/ia_client.go +++ b/internal/ia_client/ia_client.go @@ -11,9 +11,9 @@ import ( "strings" "time" + "abb_ia/internal/logger" + "abb_ia/internal/utils" "github.com/go-resty/resty/v2" - "github.com/vpoluyaktov/abb_ia/internal/logger" - "github.com/vpoluyaktov/abb_ia/internal/utils" ) const ( @@ -112,7 +112,7 @@ func (client *IAClient) GetItemDetails(itemId string) *ItemDetails { logger.Error("IAClient GetItemDetails() mock load error: " + err.Error()) } } else { - var getURL = fmt.Sprintf(IA_BASE_URL + "/details/%s/?output=json", itemId) + var getURL = fmt.Sprintf(IA_BASE_URL+"/details/%s/?output=json", itemId) _, err := client.restyClient.R().SetResult(result).Get(getURL) if err != nil { logger.Error("IAClient GetItemDetails() error: " + err.Error()) diff --git a/internal/ia_client/is_client_test.go b/internal/ia_client/is_client_test.go index 1a8ef86..f45e9a4 100644 --- a/internal/ia_client/is_client_test.go +++ b/internal/ia_client/is_client_test.go @@ -6,10 +6,10 @@ import ( "path/filepath" "testing" + "abb_ia/internal/config" + "abb_ia/internal/ia_client" + "abb_ia/internal/logger" "github.com/stretchr/testify/assert" - "github.com/vpoluyaktov/abb_ia/internal/config" - "github.com/vpoluyaktov/abb_ia/internal/ia_client" - "github.com/vpoluyaktov/abb_ia/internal/logger" ) const ( @@ -51,7 +51,7 @@ func TestGetItemById(t *testing.T) { fmt.Printf("Image: %s\n", item.Misc.Image) // for file, meta := range item.Files { - // fmt.Printf("%s -> %s\n", file, meta.Format) + // fmt.Printf("%s -> %s\n", file, meta.Format) // } } } diff --git a/internal/mq/dispatcher.go b/internal/mq/dispatcher.go index 695d7d4..3bcf2db 100644 --- a/internal/mq/dispatcher.go +++ b/internal/mq/dispatcher.go @@ -6,8 +6,8 @@ import ( "sync" "time" - "github.com/vpoluyaktov/abb_ia/internal/dto" - "github.com/vpoluyaktov/abb_ia/internal/logger" + "abb_ia/internal/dto" + "abb_ia/internal/logger" ) /** diff --git a/internal/mq/message.go b/internal/mq/message.go index c5261c1..4351d76 100644 --- a/internal/mq/message.go +++ b/internal/mq/message.go @@ -3,8 +3,8 @@ package mq import ( "fmt" - "github.com/vpoluyaktov/abb_ia/internal/dto" - "github.com/vpoluyaktov/abb_ia/internal/logger" + "abb_ia/internal/dto" + "abb_ia/internal/logger" ) type Message struct { diff --git a/internal/ui/buildPage.go b/internal/ui/buildPage.go index 6f42149..41e7e31 100644 --- a/internal/ui/buildPage.go +++ b/internal/ui/buildPage.go @@ -6,11 +6,11 @@ import ( "strconv" "strings" + "abb_ia/internal/dto" + "abb_ia/internal/mq" + "abb_ia/internal/utils" "github.com/gdamore/tcell/v2" "github.com/rivo/tview" - "github.com/vpoluyaktov/abb_ia/internal/dto" - "github.com/vpoluyaktov/abb_ia/internal/mq" - "github.com/vpoluyaktov/abb_ia/internal/utils" ) type BuildPage struct { diff --git a/internal/ui/chaptersPage.go b/internal/ui/chaptersPage.go index 1b938ea..0913ac6 100644 --- a/internal/ui/chaptersPage.go +++ b/internal/ui/chaptersPage.go @@ -6,13 +6,13 @@ import ( "strconv" "strings" + "abb_ia/internal/config" + "abb_ia/internal/dto" + "abb_ia/internal/logger" + "abb_ia/internal/mq" + "abb_ia/internal/utils" "github.com/gdamore/tcell/v2" "github.com/rivo/tview" - "github.com/vpoluyaktov/abb_ia/internal/config" - "github.com/vpoluyaktov/abb_ia/internal/dto" - "github.com/vpoluyaktov/abb_ia/internal/logger" - "github.com/vpoluyaktov/abb_ia/internal/mq" - "github.com/vpoluyaktov/abb_ia/internal/utils" ) type ChaptersPage struct { diff --git a/internal/ui/configPage.go b/internal/ui/configPage.go index 787ed6f..02a6da8 100644 --- a/internal/ui/configPage.go +++ b/internal/ui/configPage.go @@ -3,13 +3,13 @@ package ui import ( "strings" + "abb_ia/internal/config" + "abb_ia/internal/dto" + "abb_ia/internal/logger" + "abb_ia/internal/utils" "github.com/rivo/tview" - "github.com/vpoluyaktov/abb_ia/internal/config" - "github.com/vpoluyaktov/abb_ia/internal/dto" - "github.com/vpoluyaktov/abb_ia/internal/logger" - "github.com/vpoluyaktov/abb_ia/internal/utils" - "github.com/vpoluyaktov/abb_ia/internal/mq" + "abb_ia/internal/mq" ) type ConfigPage struct { @@ -47,7 +47,7 @@ type ConfigPage struct { audiobookshelfUser *tview.InputField audiobookshelfPassword *tview.InputField audiobookshelfLibrary *tview.InputField - scanAudiobookshelf *tview.Checkbox + scanAudiobookshelf *tview.Checkbox saveConfigButton *tview.Button cancelButton *tview.Button diff --git a/internal/ui/dialog.go b/internal/ui/dialog.go index 948547d..14067f6 100644 --- a/internal/ui/dialog.go +++ b/internal/ui/dialog.go @@ -1,10 +1,10 @@ package ui import ( + "abb_ia/internal/dto" + "abb_ia/internal/mq" "github.com/gdamore/tcell/v2" "github.com/rivo/tview" - "github.com/vpoluyaktov/abb_ia/internal/dto" - "github.com/vpoluyaktov/abb_ia/internal/mq" ) type dialogWindow struct { @@ -41,10 +41,11 @@ func newDialogWindow(dispatcher *mq.Dispatcher, height int, width int, focus tvi d.grid.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { switch event.Key() { - case tcell.KeyEscape: { + case tcell.KeyEscape: + { d.Close() } - } + } return event }) diff --git a/internal/ui/downloadPage.go b/internal/ui/downloadPage.go index eb44a5e..ae5b18c 100644 --- a/internal/ui/downloadPage.go +++ b/internal/ui/downloadPage.go @@ -5,11 +5,11 @@ import ( "strconv" "strings" + "abb_ia/internal/dto" + "abb_ia/internal/mq" + "abb_ia/internal/utils" "github.com/gdamore/tcell/v2" "github.com/rivo/tview" - "github.com/vpoluyaktov/abb_ia/internal/dto" - "github.com/vpoluyaktov/abb_ia/internal/mq" - "github.com/vpoluyaktov/abb_ia/internal/utils" ) type DownloadPage struct { diff --git a/internal/ui/encodingPage.go b/internal/ui/encodingPage.go index 77f1bc1..7db2673 100644 --- a/internal/ui/encodingPage.go +++ b/internal/ui/encodingPage.go @@ -5,11 +5,11 @@ import ( "strconv" "strings" + "abb_ia/internal/dto" + "abb_ia/internal/mq" + "abb_ia/internal/utils" "github.com/gdamore/tcell/v2" "github.com/rivo/tview" - "github.com/vpoluyaktov/abb_ia/internal/dto" - "github.com/vpoluyaktov/abb_ia/internal/mq" - "github.com/vpoluyaktov/abb_ia/internal/utils" ) type EncodingPage struct { diff --git a/internal/ui/footer.go b/internal/ui/footer.go index 9422b94..9cfd6cd 100644 --- a/internal/ui/footer.go +++ b/internal/ui/footer.go @@ -1,10 +1,10 @@ package ui import ( + "abb_ia/internal/config" + "abb_ia/internal/dto" + "abb_ia/internal/mq" "github.com/rivo/tview" - "github.com/vpoluyaktov/abb_ia/internal/config" - "github.com/vpoluyaktov/abb_ia/internal/dto" - "github.com/vpoluyaktov/abb_ia/internal/mq" ) type footer struct { diff --git a/internal/ui/frame.go b/internal/ui/frame.go index 6687b9c..e6bfd7a 100644 --- a/internal/ui/frame.go +++ b/internal/ui/frame.go @@ -1,9 +1,9 @@ package ui import ( + "abb_ia/internal/dto" + "abb_ia/internal/mq" "github.com/rivo/tview" - "github.com/vpoluyaktov/abb_ia/internal/dto" - "github.com/vpoluyaktov/abb_ia/internal/mq" ) type frame struct { diff --git a/internal/ui/header.go b/internal/ui/header.go index 12549f3..0b54d21 100644 --- a/internal/ui/header.go +++ b/internal/ui/header.go @@ -1,16 +1,16 @@ package ui import ( + "abb_ia/internal/dto" + "abb_ia/internal/mq" "github.com/rivo/tview" - "github.com/vpoluyaktov/abb_ia/internal/dto" - "github.com/vpoluyaktov/abb_ia/internal/mq" ) type header struct { - mq *mq.Dispatcher - grid *tview.Grid - appName *tview.TextView - version *tview.TextView + mq *mq.Dispatcher + grid *tview.Grid + appName *tview.TextView + version *tview.TextView } func newHeader(dispatcher *mq.Dispatcher) *header { diff --git a/internal/ui/searchPage.go b/internal/ui/searchPage.go index be0db2a..c87facb 100644 --- a/internal/ui/searchPage.go +++ b/internal/ui/searchPage.go @@ -4,13 +4,13 @@ import ( "fmt" "strconv" + "abb_ia/internal/config" + "abb_ia/internal/dto" + "abb_ia/internal/logger" + "abb_ia/internal/mq" + "abb_ia/internal/utils" "github.com/gdamore/tcell/v2" "github.com/rivo/tview" - "github.com/vpoluyaktov/abb_ia/internal/config" - "github.com/vpoluyaktov/abb_ia/internal/dto" - "github.com/vpoluyaktov/abb_ia/internal/logger" - "github.com/vpoluyaktov/abb_ia/internal/mq" - "github.com/vpoluyaktov/abb_ia/internal/utils" ) type SearchPage struct { @@ -263,6 +263,6 @@ func (p *SearchPage) showNewVersionMessage(dto *dto.NewAppVersionFound) { newMessageDialog(p.mq, "Notification", "New version of the application has been released: [darkblue]"+dto.NewVersion+"[black]\n"+ "Your current version is [darkblue]"+dto.CurrentVersion+"[black]\n"+ - "You can download the new version of the application from:\n[darkblue]https://github.com/vpoluyaktov/abb_ia/releases", + "You can download the new version of the application from:\n[darkblue]https://abb_ia/releases", p.searchSection) } diff --git a/internal/ui/tui.go b/internal/ui/tui.go index 4cb5293..ecfb2f1 100644 --- a/internal/ui/tui.go +++ b/internal/ui/tui.go @@ -5,9 +5,9 @@ import ( "os/exec" "time" + "abb_ia/internal/dto" + "abb_ia/internal/mq" "github.com/rivo/tview" - "github.com/vpoluyaktov/abb_ia/internal/dto" - "github.com/vpoluyaktov/abb_ia/internal/mq" ) type components interface { diff --git a/internal/utils/human.go b/internal/utils/human.go index cacf410..7465476 100644 --- a/internal/utils/human.go +++ b/internal/utils/human.go @@ -5,7 +5,7 @@ import ( "strconv" "strings" - "github.com/vpoluyaktov/abb_ia/internal/logger" + "abb_ia/internal/logger" ) // Convert time in HH:MM:SS or SSSSS.MI string format to seconds diff --git a/internal/utils/job_dispatcher.go b/internal/utils/job_dispatcher.go index 3470386..6c35d6f 100644 --- a/internal/utils/job_dispatcher.go +++ b/internal/utils/job_dispatcher.go @@ -4,7 +4,7 @@ import ( "reflect" "time" - "github.com/vpoluyaktov/abb_ia/internal/logger" + "abb_ia/internal/logger" ) type JobDispatcher struct { diff --git a/internal/utils/utils_test.go b/internal/utils/utils_test.go index ff93bbe..20861f7 100644 --- a/internal/utils/utils_test.go +++ b/internal/utils/utils_test.go @@ -4,10 +4,10 @@ import ( "os" "testing" + "abb_ia/internal/config" + "abb_ia/internal/logger" + "abb_ia/internal/utils" "github.com/stretchr/testify/assert" - "github.com/vpoluyaktov/abb_ia/internal/config" - "github.com/vpoluyaktov/abb_ia/internal/logger" - "github.com/vpoluyaktov/abb_ia/internal/utils" ) func TestMain(m *testing.M) { diff --git a/main.go b/main.go index 6bc8933..1264218 100644 --- a/main.go +++ b/main.go @@ -4,10 +4,10 @@ import ( "flag" "os" - "github.com/vpoluyaktov/abb_ia/cmd" - "github.com/vpoluyaktov/abb_ia/internal/config" - "github.com/vpoluyaktov/abb_ia/internal/logger" - "github.com/vpoluyaktov/abb_ia/internal/utils" + "abb_ia/cmd" + "abb_ia/internal/config" + "abb_ia/internal/logger" + "abb_ia/internal/utils" ) func main() { diff --git a/main_test.go b/main_test.go index 20e1351..f0ef1f8 100644 --- a/main_test.go +++ b/main_test.go @@ -4,8 +4,8 @@ import ( "os" "testing" - "github.com/vpoluyaktov/abb_ia/internal/config" - "github.com/vpoluyaktov/abb_ia/internal/logger" + "abb_ia/internal/config" + "abb_ia/internal/logger" ) func TestMain(m *testing.M) { diff --git a/tools/check_error.go b/tools/check_error.go index 58a22b4..a848876 100644 --- a/tools/check_error.go +++ b/tools/check_error.go @@ -1,6 +1,6 @@ package tools -import "github.com/vpoluyaktov/abb_ia/internal/logger" +import "abb_ia/internal/logger" func CheckError(e error) { if e != nil { From a255c4de5afded46a409755d1dc5af1789ca9687 Mon Sep 17 00:00:00 2001 From: vpoluyaktov Date: Wed, 22 Nov 2023 13:07:07 -0800 Subject: [PATCH 7/8] Multiple bitrates filter implemented --- internal/controller/search_controller.go | 186 +++++++++++++++++++++++ 1 file changed, 186 insertions(+) create mode 100644 internal/controller/search_controller.go diff --git a/internal/controller/search_controller.go b/internal/controller/search_controller.go new file mode 100644 index 0000000..412712b --- /dev/null +++ b/internal/controller/search_controller.go @@ -0,0 +1,186 @@ +package controller + +import ( + "net/url" + "sort" + "strconv" + "strings" + + "abb_ia/internal/config" + "abb_ia/internal/dto" + "abb_ia/internal/ia" + "abb_ia/internal/logger" + "abb_ia/internal/mq" + "abb_ia/internal/utils" + "github.com/rivo/tview" +) + +type SearchController struct { + mq *mq.Dispatcher +} + +func NewSearchController(dispatcher *mq.Dispatcher) *SearchController { + c := &SearchController{} + c.mq = dispatcher + c.mq.RegisterListener(mq.SearchController, c.dispatchMessage) + return c +} + +func (c *SearchController) checkMQ() { + m := c.mq.GetMessage(mq.SearchController) + if m != nil { + c.dispatchMessage(m) + } +} + +func (c *SearchController) dispatchMessage(m *mq.Message) { + switch dto := m.Dto.(type) { + case *dto.SearchCommand: + go c.performSearch(dto) + default: + m.UnsupportedTypeError(mq.SearchController) + } +} + +func (c *SearchController) performSearch(cmd *dto.SearchCommand) { + 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) + ia := ia_client.New(config.Instance().GetSearchRowsMax(), config.Instance().IsUseMock(), config.Instance().IsSaveMock()) + resp := ia.Search(cmd.SearchCondition, "audio") + if resp == nil { + logger.Error(mq.SearchController + ": Failed to perform IA search with condition: " + cmd.SearchCondition) + } + + itemsTotal := resp.Response.NumFound + itemsFetched := 0 + + docs := resp.Response.Docs + for _, doc := range docs { + item := &dto.IAItem{} + item.ID = doc.Identifier + item.Title = tview.Escape(doc.Title) + item.IaURL = ia_client.IA_BASE_URL + "/details/" + doc.Identifier + item.LicenseUrl = doc.Licenseurl + + item.AudioFiles = make([]dto.AudioFile, 0) + var totalSize int64 = 0 + var totalLength float64 = 0.0 + d := ia.GetItemDetails(doc.Identifier) + if d != nil { + item.Server = d.Server + item.Dir = d.Dir + if len(doc.Creator) > 0 && d.Metadata.Creator[0] != "" { + item.Creator = doc.Creator[0] + } else if len(d.Metadata.Creator) > 0 && d.Metadata.Creator[0] != "" { + item.Creator = d.Metadata.Creator[0] + } else if len(d.Metadata.Artist) > 0 && d.Metadata.Artist[0] != "" { + item.Creator = d.Metadata.Artist[0] + } else { + item.Creator = "Internet Archive" + } + + if len(d.Metadata.Description) > 0 { + item.Description = tview.Escape(ia.Html2Text(d.Metadata.Description[0])) + } + + for name, metadata := range d.Files { + format := metadata.Format + // collect mp3 files + // TODO: Implement filtering for mp3 files with multiple bitrates (see https://archive.org/details/voyage_moon_1512_librivox or https://archive.org/details/OTRR_Blair_of_the_Mounties_Singles for ex.) + if utils.Contains(dto.Mp3Formats, format) { + size, sErr := strconv.ParseInt(metadata.Size, 10, 64) + length, lErr := utils.TimeToSeconds(metadata.Length) + if sErr != nil || lErr != nil { + logger.Error("Can't parse the file metadata: " + name) + } else { + file := dto.AudioFile{} + file.Name = strings.TrimPrefix(name, "/") + if metadata.Title != "" { + file.Title = metadata.Title + } else { + file.Title = file.Name + } + file.Size = size + file.Length = length + file.Format = metadata.Format + // check if there is a file with the same title but different bitrate. Keep highest bitrate only + addNewFile := true + for i, oldFile := range item.AudioFiles { + if file.Title == oldFile.Title { + oldFilePriority := utils.GetIndex(dto.Mp3Formats, oldFile.Format) + newFilePriority := utils.GetIndex(dto.Mp3Formats, file.Format) + if newFilePriority > oldFilePriority { + // remove old file from the list + item.AudioFiles = append(item.AudioFiles[:i], item.AudioFiles[i+1:]...) + totalSize -= oldFile.Size + totalLength -= oldFile.Length + // and add new one + addNewFile = true + } else if newFilePriority == oldFilePriority { + // means multiple files have the same title + addNewFile = true + } else { + addNewFile = false + } + break + } + } + if addNewFile { + item.AudioFiles = append(item.AudioFiles, file) + totalSize += size + totalLength += length + } + } + } + + // collect image files + if utils.Contains(dto.CoverFormats, format) { + size, err := strconv.ParseInt(metadata.Size, 10, 64) + if err == nil { + file := dto.ImageFile{} + file.Name = strings.TrimPrefix(name, "/") + file.Size = size + file.Format = metadata.Format + item.ImageFiles = append(item.ImageFiles, file) + } + } + } + + // sort mp3 files by name TODO: Check if sort is needed + sort.Slice(item.AudioFiles, func(i, j int) bool { return item.AudioFiles[i].Name < item.AudioFiles[j].Name }) + item.TotalSize = totalSize + item.TotalLength = totalLength + + // 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++ { + if item.ImageFiles[i].Size > biggestImage.Size { + biggestImage = item.ImageFiles[i] + } + } + item.CoverUrl = (&url.URL{Scheme: "https", Host: item.Server, Path: item.Dir + "/" + biggestImage.Name}).String() + } else { + item.CoverUrl = "No cover available!" + } + + if len(item.AudioFiles) > 0 { + itemsFetched++ + sp := &dto.SearchProgress{ItemsTotal: itemsTotal, ItemsFetched: itemsFetched} + c.mq.SendMessage(mq.SearchController, mq.SearchPage, sp, false) + c.mq.SendMessage(mq.SearchController, mq.SearchPage, item, false) + } + } + logger.Debug(mq.SearchController + " fetched first " + strconv.Itoa(itemsFetched) + " items from " + strconv.Itoa(itemsTotal) + " total") + } + c.mq.SendMessage(mq.SearchController, mq.Footer, &dto.SetBusyIndicator{Busy: false}, false) + c.mq.SendMessage(mq.SearchController, mq.Footer, &dto.UpdateStatus{Message: ""}, false) + + if itemsFetched == 0 { + c.mq.SendMessage(mq.SearchController, mq.SearchPage, &dto.NothingFoundError{SearchCondition: cmd.SearchCondition}, false) + } +} From 66f813b06a6f4a6b52bb6a89a437356247ecb630 Mon Sep 17 00:00:00 2001 From: vpoluyaktov Date: Wed, 22 Nov 2023 17:07:50 -0800 Subject: [PATCH 8/8] Audiobookshelf upload UI implemented. Code refactoring --- ...{audiobookshelf_test.go => client_test.go} | 0 .../{bootController.go => boot_controller.go} | 5 +- ...buildController.go => build_controller.go} | 0 ...rsController.go => chapters_controller.go} | 18 +- ...nupController.go => cleanup_controller.go} | 0 internal/controller/conductor.go | 2 +- ...nfigController.go => config_controller.go} | 0 .../{copyController.go => copy_controller.go} | 3 +- ...adController.go => download_controller.go} | 2 +- ...ngController.go => encoding_controller.go} | 0 internal/controller/searchController.go | 155 ----------------- internal/controller/search_controller.go | 5 +- ...helfController.go => upload_controller.go} | 46 ++--- internal/dto/audiobookshelf.go | 12 +- internal/dto/ia.go | 1 + internal/dto/search.go | 2 +- internal/{utils/github.go => github/utils.go} | 2 +- .../{ia_client/ia_client.go => ia/client.go} | 0 .../is_client_test.go => ia/client_test.go} | 2 +- .../{ia_client/ia_model.go => ia/model.go} | 0 .../ia_client_utils.go => ia/utils.go} | 0 internal/mq/recepients.go | 42 ++--- internal/ui/{buildPage.go => build_page.go} | 164 ++++++++++++++---- .../ui/{chaptersPage.go => chapters_page.go} | 0 internal/ui/{configPage.go => config_page.go} | 2 +- internal/ui/dialog.go | 5 +- .../ui/{downloadPage.go => download_page.go} | 0 .../ui/{encodingPage.go => encoding_page.go} | 0 internal/ui/footer.go | 2 +- internal/ui/{searchPage.go => search_page.go} | 8 +- internal/utils/filepath.go | 40 ++++- internal/utils/misc.go | 5 + tools/check_error.go | 10 -- 33 files changed, 244 insertions(+), 289 deletions(-) rename internal/audiobookshelf/{audiobookshelf_test.go => client_test.go} (100%) rename internal/controller/{bootController.go => boot_controller.go} (87%) rename internal/controller/{buildController.go => build_controller.go} (100%) rename internal/controller/{chaptersController.go => chapters_controller.go} (87%) rename internal/controller/{cleanupController.go => cleanup_controller.go} (100%) rename internal/controller/{configController.go => config_controller.go} (100%) rename internal/controller/{copyController.go => copy_controller.go} (97%) rename internal/controller/{downloadController.go => download_controller.go} (99%) rename internal/controller/{encodingController.go => encoding_controller.go} (100%) delete mode 100644 internal/controller/searchController.go rename internal/controller/{audiobookShelfController.go => upload_controller.go} (74%) rename internal/{utils/github.go => github/utils.go} (98%) rename internal/{ia_client/ia_client.go => ia/client.go} (100%) rename internal/{ia_client/is_client_test.go => ia/client_test.go} (98%) rename internal/{ia_client/ia_model.go => ia/model.go} (100%) rename internal/{ia_client/ia_client_utils.go => ia/utils.go} (100%) rename internal/ui/{buildPage.go => build_page.go} (62%) rename internal/ui/{chaptersPage.go => chapters_page.go} (100%) rename internal/ui/{configPage.go => config_page.go} (99%) rename internal/ui/{downloadPage.go => download_page.go} (100%) rename internal/ui/{encodingPage.go => encoding_page.go} (100%) rename internal/ui/{searchPage.go => search_page.go} (98%) delete mode 100644 tools/check_error.go diff --git a/internal/audiobookshelf/audiobookshelf_test.go b/internal/audiobookshelf/client_test.go similarity index 100% rename from internal/audiobookshelf/audiobookshelf_test.go rename to internal/audiobookshelf/client_test.go diff --git a/internal/controller/bootController.go b/internal/controller/boot_controller.go similarity index 87% rename from internal/controller/bootController.go rename to internal/controller/boot_controller.go index 3f9c7af..8815067 100644 --- a/internal/controller/bootController.go +++ b/internal/controller/boot_controller.go @@ -5,6 +5,7 @@ import ( "abb_ia/internal/config" "abb_ia/internal/dto" + "abb_ia/internal/github" "abb_ia/internal/logger" "abb_ia/internal/mq" "abb_ia/internal/utils" @@ -57,13 +58,13 @@ func (c *BootController) checkFFmpeg() bool { func (c *BootController) checkNewVersion() { - latestVersion, err := utils.GetLatestVersion(config.Instance().GetRepoOwner(), config.Instance().GetRepoName()) + latestVersion, err := github.GetLatestVersion(config.Instance().GetRepoOwner(), config.Instance().GetRepoName()) if err != nil { logger.Error("Can't check new version: " + err.Error()) return } - result, err := utils.CompareVersions(latestVersion, config.Instance().AppVersion()) + result, err := github.CompareVersions(latestVersion, config.Instance().AppVersion()) if err != nil { logger.Error("Can not compare versions: " + err.Error()) return diff --git a/internal/controller/buildController.go b/internal/controller/build_controller.go similarity index 100% rename from internal/controller/buildController.go rename to internal/controller/build_controller.go diff --git a/internal/controller/chaptersController.go b/internal/controller/chapters_controller.go similarity index 87% rename from internal/controller/chaptersController.go rename to internal/controller/chapters_controller.go index 7748823..0ab1a38 100644 --- a/internal/controller/chaptersController.go +++ b/internal/controller/chapters_controller.go @@ -17,13 +17,6 @@ type ChaptersController struct { stopFlag bool } -/** - * Creates a new ChaptersController instance. - * @param dispatcher - The message queue dispatcher. - * @returns The new ChaptersController instance. - * - * This code is useful for creating a new ChaptersController instance and registering it with the message queue dispatcher. This allows the ChaptersController to receive messages from the message queue and dispatch them to the appropriate handler. - **/ func NewChaptersController(dispatcher *mq.Dispatcher) *ChaptersController { c := &ChaptersController{} c.mq = dispatcher @@ -60,15 +53,6 @@ func (c *ChaptersController) stopChapters(cmd *dto.StopCommand) { logger.Debug(mq.ChaptersController + ": Received StopChapters command") } -/** - * @description Splits an audiobook into parts and chapters. - * @param {dto.ChaptersCreate} cmd - The command to create chapters. - * @returns {void} - * - * This function is useful for splitting an audiobook into parts and chapters. - * It takes in a command object containing the audiobook and then splits the audiobook into parts and chapters. - * It then sends messages to the ChaptersPage and Footer to update the status and busy indicator. - */ func (c *ChaptersController) createChapters(cmd *dto.ChaptersCreate) { logger.Debug(mq.ChaptersController + " received " + cmd.String()) @@ -92,6 +76,7 @@ func (c *ChaptersController) createChapters(cmd *dto.ChaptersCreate) { var fileNo int = 1 var chapterNo int = 1 var offset float64 = 0 + var abSize int64 = 0 var partSize int64 = 0 var partDuration float64 = 0 var partChapters []dto.Chapter = []dto.Chapter{} @@ -102,6 +87,7 @@ func (c *ChaptersController) createChapters(cmd *dto.ChaptersCreate) { mp3, _ := ffmpeg.NewFFProbe(filePath) chapterFiles = append(chapterFiles, dto.Mp3File{Number: fileNo, FileName: file.FileName, Size: mp3.Size(), Duration: mp3.Duration()}) fileNo++ + abSize += mp3.Size() partSize += mp3.Size() partDuration += mp3.Duration() chapter := dto.Chapter{Number: chapterNo, Name: mp3.Title(), Size: mp3.Size(), Duration: mp3.Duration(), Start: offset, End: offset + mp3.Duration(), Files: chapterFiles} diff --git a/internal/controller/cleanupController.go b/internal/controller/cleanup_controller.go similarity index 100% rename from internal/controller/cleanupController.go rename to internal/controller/cleanup_controller.go diff --git a/internal/controller/conductor.go b/internal/controller/conductor.go index 35d8190..72c747a 100644 --- a/internal/controller/conductor.go +++ b/internal/controller/conductor.go @@ -25,7 +25,7 @@ func NewConductor(dispatcher *mq.Dispatcher) *Conductor { c.controllers = append(c.controllers, NewChaptersController(c.dispatcher)) c.controllers = append(c.controllers, NewBuildController(c.dispatcher)) c.controllers = append(c.controllers, NewCopyController(c.dispatcher)) - c.controllers = append(c.controllers, NewAudiobookshelfController(c.dispatcher)) + c.controllers = append(c.controllers, NewUploadController(c.dispatcher)) c.controllers = append(c.controllers, NewCleanupController(c.dispatcher)) c.controllers = append(c.controllers, NewBootController(c.dispatcher)) return c diff --git a/internal/controller/configController.go b/internal/controller/config_controller.go similarity index 100% rename from internal/controller/configController.go rename to internal/controller/config_controller.go diff --git a/internal/controller/copyController.go b/internal/controller/copy_controller.go similarity index 97% rename from internal/controller/copyController.go rename to internal/controller/copy_controller.go index ac9b4e4..de894ff 100644 --- a/internal/controller/copyController.go +++ b/internal/controller/copy_controller.go @@ -86,7 +86,6 @@ func (c *CopyController) dispatchMessage(m *mq.Message) { func (c *CopyController) startCopy(cmd *dto.CopyCommand) { c.startTime = time.Now() logger.Info(mq.CopyController + " received " + cmd.String()) - c.ab = cmd.Audiobook // update part size and whole audiobook size after re-encoding @@ -193,7 +192,7 @@ func (c *CopyController) updateFileCopyProgress(fileId int, fileName string, siz } // sent a message only if progress changed - c.mq.SendMessage(mq.CopyController, mq.BuildPage, &dto.UploadFileProgress{FileId: fileId, FileName: fileName, Percent: percent}, false) + c.mq.SendMessage(mq.CopyController, mq.BuildPage, &dto.CopyFileProgress{FileId: fileId, FileName: fileName, Percent: percent}, false) } c.filesCopy[fileId].fileId = fileId c.filesCopy[fileId].fileSize = size diff --git a/internal/controller/downloadController.go b/internal/controller/download_controller.go similarity index 99% rename from internal/controller/downloadController.go rename to internal/controller/download_controller.go index 7ac74bb..26d6422 100644 --- a/internal/controller/downloadController.go +++ b/internal/controller/download_controller.go @@ -6,7 +6,7 @@ import ( "time" "abb_ia/internal/dto" - "abb_ia/internal/ia_client" + "abb_ia/internal/ia" "abb_ia/internal/logger" "abb_ia/internal/mq" "abb_ia/internal/utils" diff --git a/internal/controller/encodingController.go b/internal/controller/encoding_controller.go similarity index 100% rename from internal/controller/encodingController.go rename to internal/controller/encoding_controller.go diff --git a/internal/controller/searchController.go b/internal/controller/searchController.go deleted file mode 100644 index 225a682..0000000 --- a/internal/controller/searchController.go +++ /dev/null @@ -1,155 +0,0 @@ -package controller - -import ( - "net/url" - "sort" - "strconv" - "strings" - - "abb_ia/internal/config" - "abb_ia/internal/dto" - "abb_ia/internal/ia_client" - "abb_ia/internal/logger" - "abb_ia/internal/mq" - "abb_ia/internal/utils" - "github.com/rivo/tview" -) - -type SearchController struct { - mq *mq.Dispatcher -} - -func NewSearchController(dispatcher *mq.Dispatcher) *SearchController { - c := &SearchController{} - c.mq = dispatcher - c.mq.RegisterListener(mq.SearchController, c.dispatchMessage) - return c -} - -func (c *SearchController) checkMQ() { - m := c.mq.GetMessage(mq.SearchController) - if m != nil { - c.dispatchMessage(m) - } -} - -func (c *SearchController) dispatchMessage(m *mq.Message) { - switch dto := m.Dto.(type) { - case *dto.SearchCommand: - go c.performSearch(dto) - default: - m.UnsupportedTypeError(mq.SearchController) - } -} - -func (c *SearchController) performSearch(cmd *dto.SearchCommand) { - 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) - ia := ia_client.New(config.Instance().GetSearchRowsMax(), config.Instance().IsUseMock(), config.Instance().IsSaveMock()) - resp := ia.Search(cmd.SearchCondition, "audio") - if resp == nil { - logger.Error(mq.SearchController + ": Failed to perform IA search with condition: " + cmd.SearchCondition) - } - - itemsTotal := resp.Response.NumFound - itemsFetched := 0 - - docs := resp.Response.Docs - for _, doc := range docs { - item := &dto.IAItem{} - item.ID = doc.Identifier - item.Title = tview.Escape(doc.Title) - item.IaURL = ia_client.IA_BASE_URL + "/details/" + doc.Identifier - item.LicenseUrl = doc.Licenseurl - - item.AudioFiles = make([]dto.AudioFile, 0) - var totalSize int64 = 0 - var totalLength float64 = 0.0 - d := ia.GetItemDetails(doc.Identifier) - if d != nil { - item.Server = d.Server - item.Dir = d.Dir - if len(doc.Creator) > 0 && d.Metadata.Creator[0] != "" { - item.Creator = doc.Creator[0] - } else if len(d.Metadata.Creator) > 0 && d.Metadata.Creator[0] != "" { - item.Creator = d.Metadata.Creator[0] - } else if len(d.Metadata.Artist) > 0 && d.Metadata.Artist[0] != "" { - item.Creator = d.Metadata.Artist[0] - } else { - item.Creator = "Internet Archive" - } - - if len(d.Metadata.Description) > 0 { - item.Description = tview.Escape(ia.Html2Text(d.Metadata.Description[0])) - } - - for name, metadata := range d.Files { - format := metadata.Format - // collect mp3 files - // TODO: Implement filtering for mp3 files with multiple bitrates (see https://archive.org/details/voyage_moon_1512_librivox for ex.) - if utils.Contains(dto.Mp3Formats, format) { - size, sErr := strconv.ParseInt(metadata.Size, 10, 64) - length, lErr := utils.TimeToSeconds(metadata.Length) - if sErr == nil && lErr == nil { - file := dto.AudioFile{} - file.Name = strings.TrimPrefix(name, "/") - file.Size = size - file.Length = length - file.Format = metadata.Format - totalSize += size - totalLength += length - item.AudioFiles = append(item.AudioFiles, file) - } - } - - // collect image files - if utils.Contains(dto.CoverFormats, format) { - size, err := strconv.ParseInt(metadata.Size, 10, 64) - if err == nil { - file := dto.ImageFile{} - file.Name = strings.TrimPrefix(name, "/") - file.Size = size - file.Format = metadata.Format - item.ImageFiles = append(item.ImageFiles, file) - } - } - } - - // sort mp3 files by name TODO: Check if sort is needed - sort.Slice(item.AudioFiles, func(i, j int) bool { return item.AudioFiles[i].Name < item.AudioFiles[j].Name }) - item.TotalSize = totalSize - item.TotalLength = totalLength - - // 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++ { - if item.ImageFiles[i].Size > biggestImage.Size { - biggestImage = item.ImageFiles[i] - } - } - item.CoverUrl = (&url.URL{Scheme: "https", Host: item.Server, Path: item.Dir + "/" + biggestImage.Name}).String() - } else { - item.CoverUrl = "No cover available!" - } - - if len(item.AudioFiles) > 0 { - itemsFetched++ - sp := &dto.SearchProgress{ItemsTotal: itemsTotal, ItemsFetched: itemsFetched} - c.mq.SendMessage(mq.SearchController, mq.SearchPage, sp, false) - c.mq.SendMessage(mq.SearchController, mq.SearchPage, item, false) - } - } - logger.Debug(mq.SearchController + " fetched first " + strconv.Itoa(itemsFetched) + " items from " + strconv.Itoa(itemsTotal) + " total") - } - c.mq.SendMessage(mq.SearchController, mq.Footer, &dto.SetBusyIndicator{Busy: false}, false) - c.mq.SendMessage(mq.SearchController, mq.Footer, &dto.UpdateStatus{Message: ""}, false) - - if itemsFetched == 0 { - c.mq.SendMessage(mq.SearchController, mq.SearchPage, &dto.NothingFoundError{SearchCondition: cmd.SearchCondition}, false) - } -} diff --git a/internal/controller/search_controller.go b/internal/controller/search_controller.go index 412712b..0764481 100644 --- a/internal/controller/search_controller.go +++ b/internal/controller/search_controller.go @@ -87,7 +87,7 @@ func (c *SearchController) performSearch(cmd *dto.SearchCommand) { for name, metadata := range d.Files { format := metadata.Format // collect mp3 files - // TODO: Implement filtering for mp3 files with multiple bitrates (see https://archive.org/details/voyage_moon_1512_librivox or https://archive.org/details/OTRR_Blair_of_the_Mounties_Singles for ex.) + if utils.Contains(dto.Mp3Formats, format) { size, sErr := strconv.ParseInt(metadata.Size, 10, 64) length, lErr := utils.TimeToSeconds(metadata.Length) @@ -99,12 +99,13 @@ func (c *SearchController) performSearch(cmd *dto.SearchCommand) { if metadata.Title != "" { file.Title = metadata.Title } else { - file.Title = file.Name + file.Title = utils.SanitizeMp3FileName(file.Name) } file.Size = size file.Length = length file.Format = metadata.Format // check if there is a file with the same title but different bitrate. Keep highest bitrate only + // see https://archive.org/details/voyage_moon_1512_librivox or https://archive.org/details/OTRR_Blair_of_the_Mounties_Singles for ex. addNewFile := true for i, oldFile := range item.AudioFiles { if file.Title == oldFile.Title { diff --git a/internal/controller/audiobookShelfController.go b/internal/controller/upload_controller.go similarity index 74% rename from internal/controller/audiobookShelfController.go rename to internal/controller/upload_controller.go index 4dba73e..dda22a1 100644 --- a/internal/controller/audiobookShelfController.go +++ b/internal/controller/upload_controller.go @@ -11,7 +11,7 @@ import ( "abb_ia/internal/utils" ) -type AudiobookshelfController struct { +type UploadController struct { mq *mq.Dispatcher ab *dto.Audiobook startTime time.Time @@ -28,33 +28,33 @@ type fileUpload struct { progress int } -func NewAudiobookshelfController(dispatcher *mq.Dispatcher) *AudiobookshelfController { - c := &AudiobookshelfController{} +func NewUploadController(dispatcher *mq.Dispatcher) *UploadController { + c := &UploadController{} c.mq = dispatcher - c.mq.RegisterListener(mq.AudiobookshelfController, c.dispatchMessage) + c.mq.RegisterListener(mq.UploadController, c.dispatchMessage) return c } -func (c *AudiobookshelfController) checkMQ() { - m := c.mq.GetMessage(mq.AudiobookshelfController) +func (c *UploadController) checkMQ() { + m := c.mq.GetMessage(mq.UploadController) if m != nil { c.dispatchMessage(m) } } -func (c *AudiobookshelfController) dispatchMessage(m *mq.Message) { +func (c *UploadController) dispatchMessage(m *mq.Message) { switch dto := m.Dto.(type) { - case *dto.AudiobookshelfScanCommand: - go c.audiobookshelfScan(dto) - case *dto.AudiobookshelfUploadCommand: - go c.uploadAudiobook(dto) + case *dto.AbsScanCommand: + go c.absScan(dto) + case *dto.AbsUploadCommand: + go c.absUpload(dto) default: - m.UnsupportedTypeError(mq.AudiobookshelfController) + m.UnsupportedTypeError(mq.UploadController) } } -func (c *AudiobookshelfController) audiobookshelfScan(cmd *dto.AudiobookshelfScanCommand) { - logger.Info(mq.AudiobookshelfController + " received " + cmd.String()) +func (c *UploadController) absScan(cmd *dto.AbsScanCommand) { + logger.Info(mq.UploadController + " received " + cmd.String()) ab := cmd.Audiobook url := ab.Config.GetAudiobookshelfUrl() username := ab.Config.GetAudiobookshelfUser() @@ -89,11 +89,12 @@ func (c *AudiobookshelfController) audiobookshelfScan(cmd *dto.AudiobookshelfSca } logger.Info("A scan launched for library " + libraryName + " on audiobookshlf server") } - c.mq.SendMessage(mq.AudiobookshelfController, mq.BuildPage, &dto.ScanComplete{Audiobook: cmd.Audiobook}, true) + c.mq.SendMessage(mq.UploadController, mq.BuildPage, &dto.ScanComplete{Audiobook: cmd.Audiobook}, true) } -func (c *AudiobookshelfController) uploadAudiobook(cmd *dto.AudiobookshelfUploadCommand) { - logger.Info(mq.AudiobookshelfController + " received " + cmd.String()) +func (c *UploadController) absUpload(cmd *dto.AbsUploadCommand) { + c.startTime = time.Now() + logger.Info(mq.UploadController + " received " + cmd.String()) c.ab = cmd.Audiobook url := c.ab.Config.GetAudiobookshelfUrl() username := c.ab.Config.GetAudiobookshelfUser() @@ -129,16 +130,15 @@ func (c *AudiobookshelfController) uploadAudiobook(cmd *dto.AudiobookshelfUpload c.filesUpload = make([]fileUpload, len(c.ab.Parts)) go c.updateTotalUploadProgress() err = absClient.UploadBook(c.ab, libraryID, folderID, c.updateFileUplodProgress) - if err != nil { logger.Error("Can't upload the audiobook to audiobookshelf server: " + err.Error()) } c.stopFlag = true } - c.mq.SendMessage(mq.AudiobookshelfController, mq.BuildPage, &dto.UploadComplete{Audiobook: cmd.Audiobook}, true) + c.mq.SendMessage(mq.UploadController, mq.BuildPage, &dto.UploadComplete{Audiobook: cmd.Audiobook}, true) } -func (c *AudiobookshelfController) updateFileUplodProgress(fileId int, fileName string, size int64, pos int64, percent int) { +func (c *UploadController) updateFileUplodProgress(fileId int, fileName string, size int64, pos int64, percent int) { if c.filesUpload[fileId].progress != percent { // wrong calculation protection @@ -149,7 +149,7 @@ func (c *AudiobookshelfController) updateFileUplodProgress(fileId int, fileName } // sent a message only if progress changed - c.mq.SendMessage(mq.AudiobookshelfController, mq.BuildPage, &dto.UploadFileProgress{FileId: fileId, FileName: fileName, Percent: percent}, false) + c.mq.SendMessage(mq.UploadController, mq.BuildPage, &dto.UploadFileProgress{FileId: fileId, FileName: fileName, Percent: percent}, false) } c.filesUpload[fileId].fileId = fileId c.filesUpload[fileId].fileSize = size @@ -157,7 +157,7 @@ func (c *AudiobookshelfController) updateFileUplodProgress(fileId int, fileName c.filesUpload[fileId].progress = percent } -func (c *AudiobookshelfController) updateTotalUploadProgress() { +func (c *UploadController) updateTotalUploadProgress() { var percent int = -1 for !c.stopFlag && percent <= 100 { @@ -206,7 +206,7 @@ func (c *AudiobookshelfController) updateTotalUploadProgress() { speedH := utils.SpeedToHuman(speed) etaH := utils.SecondsToTime(eta) - c.mq.SendMessage(mq.AudiobookshelfController, mq.BuildPage, &dto.UploadProgress{Elapsed: elapsedH, Percent: percent, Files: filesH, Bytes: bytesH, Speed: speedH, ETA: etaH}, false) + c.mq.SendMessage(mq.UploadController, mq.BuildPage, &dto.UploadProgress{Elapsed: elapsedH, Percent: percent, Files: filesH, Bytes: bytesH, Speed: speedH, ETA: etaH}, false) } time.Sleep(mq.PullFrequency) } diff --git a/internal/dto/audiobookshelf.go b/internal/dto/audiobookshelf.go index ef7bb98..9ddad1d 100644 --- a/internal/dto/audiobookshelf.go +++ b/internal/dto/audiobookshelf.go @@ -2,12 +2,12 @@ package dto import "fmt" -type AudiobookshelfScanCommand struct { +type AbsScanCommand struct { Audiobook *Audiobook } -func (c *AudiobookshelfScanCommand) String() string { - return fmt.Sprintf("AudiobookshelfScanCommand: %s", c.Audiobook.String()) +func (c *AbsScanCommand) String() string { + return fmt.Sprintf("AbsScanCommand: %s", c.Audiobook.String()) } type ScanComplete struct { @@ -18,12 +18,12 @@ func (c *ScanComplete) String() string { return fmt.Sprintf("ScanComplete: %s", c.Audiobook.String()) } -type AudiobookshelfUploadCommand struct { +type AbsUploadCommand struct { Audiobook *Audiobook } -func (c *AudiobookshelfUploadCommand) String() string { - return fmt.Sprintf("AudiobookshelfUploadCommand: %s", c.Audiobook.String()) +func (c *AbsUploadCommand) String() string { + return fmt.Sprintf("AbsUploadCommand: %s", c.Audiobook.String()) } type UploadFileProgress struct { diff --git a/internal/dto/ia.go b/internal/dto/ia.go index bdba209..63f6eb4 100644 --- a/internal/dto/ia.go +++ b/internal/dto/ia.go @@ -24,6 +24,7 @@ func (i *IAItem) String() string { type AudioFile struct { Name string + Title string Format string Length float64 Size int64 diff --git a/internal/dto/search.go b/internal/dto/search.go index f215637..78799ad 100644 --- a/internal/dto/search.go +++ b/internal/dto/search.go @@ -6,7 +6,7 @@ import "fmt" var Mp3Formats = []string{"16Kbps MP3", "24Kbps MP3", "32Kbps MP3", "40Kbps MP3", "48Kbps MP3", "56Kbps MP3", "64Kbps MP3", "80Kbps MP3", "96Kbps MP3", "112Kbps MP3", "128Kbps MP3", "144Kbps MP3", "160Kbps MP3", "224Kbps MP3", "256Kbps MP3", "320Kbps MP3", "VBR MP3"} // -var CoverFormats = []string{"JPEG"} +var CoverFormats = []string{"JPEG", "JPEG Thumb"} type SearchCommand struct { SearchCondition string diff --git a/internal/utils/github.go b/internal/github/utils.go similarity index 98% rename from internal/utils/github.go rename to internal/github/utils.go index 9fc9a95..e35eb13 100644 --- a/internal/utils/github.go +++ b/internal/github/utils.go @@ -1,4 +1,4 @@ -package utils +package github import ( "encoding/json" diff --git a/internal/ia_client/ia_client.go b/internal/ia/client.go similarity index 100% rename from internal/ia_client/ia_client.go rename to internal/ia/client.go diff --git a/internal/ia_client/is_client_test.go b/internal/ia/client_test.go similarity index 98% rename from internal/ia_client/is_client_test.go rename to internal/ia/client_test.go index f45e9a4..6f059fa 100644 --- a/internal/ia_client/is_client_test.go +++ b/internal/ia/client_test.go @@ -7,7 +7,7 @@ import ( "testing" "abb_ia/internal/config" - "abb_ia/internal/ia_client" + "abb_ia/internal/ia" "abb_ia/internal/logger" "github.com/stretchr/testify/assert" ) diff --git a/internal/ia_client/ia_model.go b/internal/ia/model.go similarity index 100% rename from internal/ia_client/ia_model.go rename to internal/ia/model.go diff --git a/internal/ia_client/ia_client_utils.go b/internal/ia/utils.go similarity index 100% rename from internal/ia_client/ia_client_utils.go rename to internal/ia/utils.go diff --git a/internal/mq/recepients.go b/internal/mq/recepients.go index b5659a9..31f784c 100644 --- a/internal/mq/recepients.go +++ b/internal/mq/recepients.go @@ -2,25 +2,25 @@ package mq // MQ recipients const ( - TUI = "TUI" - Frame = "Frame" - Header = "Header" - Footer = "Footer" - DialogWindow = "DialogWindow" - SearchPage = "SearchPage" - ConfigPage = "ConfigPage" - DownloadPage = "DownloadPage" - EncodingPage = "EncodingPage" - ChaptersPage = "ChaptersPage" - BuildPage = "BuildPage" - BootController = "BootController" - SearchController = "SearchController" - ConfigController = "ConfigController" - DownloadController = "DownloadController" - EncodingController = "EncodingController" - ChaptersController = "ChaptersController" - BuildController = "BuildController" - CopyController = "CopyController" - CleanupController = "CleanupController" - AudiobookshelfController = "AudiobookshelfController" + TUI = "TUI" + Frame = "Frame" + Header = "Header" + Footer = "Footer" + DialogWindow = "DialogWindow" + SearchPage = "SearchPage" + ConfigPage = "ConfigPage" + DownloadPage = "DownloadPage" + EncodingPage = "EncodingPage" + ChaptersPage = "ChaptersPage" + BuildPage = "BuildPage" + BootController = "BootController" + SearchController = "SearchController" + ConfigController = "ConfigController" + DownloadController = "DownloadController" + EncodingController = "EncodingController" + ChaptersController = "ChaptersController" + BuildController = "BuildController" + CopyController = "CopyController" + CleanupController = "CleanupController" + UploadController = "UploadController" ) diff --git a/internal/ui/buildPage.go b/internal/ui/build_page.go similarity index 62% rename from internal/ui/buildPage.go rename to internal/ui/build_page.go index 41e7e31..a347798 100644 --- a/internal/ui/buildPage.go +++ b/internal/ui/build_page.go @@ -9,6 +9,7 @@ import ( "abb_ia/internal/dto" "abb_ia/internal/mq" "abb_ia/internal/utils" + "github.com/gdamore/tcell/v2" "github.com/rivo/tview" ) @@ -17,10 +18,13 @@ type BuildPage struct { mq *mq.Dispatcher grid *tview.Grid infoPanel *infoPanel + infoSection *tview.Grid buildSection *tview.Grid copySection *tview.Grid + uploadSection *tview.Grid buildTable *table copyTable *table + uploadTable *table progressTable *table progressSection *tview.Grid } @@ -31,7 +35,7 @@ func newBuildPage(dispatcher *mq.Dispatcher) *BuildPage { p.mq.RegisterListener(mq.BuildPage, p.dispatchMessage) p.grid = tview.NewGrid() - p.grid.SetRows(7, -1, -1, 4) + p.grid.SetRows(7, -1, -1, -1, 4) p.grid.SetColumns(0) // Ignore mouse events when the grid has no focus @@ -44,19 +48,19 @@ func newBuildPage(dispatcher *mq.Dispatcher) *BuildPage { }) // book info section - infoSection := tview.NewGrid() - infoSection.SetColumns(-2, -1) - infoSection.SetBorder(true) - infoSection.SetTitle(" Audiobook information: ") - infoSection.SetTitleAlign(tview.AlignLeft) + p.infoSection = tview.NewGrid() + p.infoSection.SetColumns(-2, -1) + p.infoSection.SetBorder(true) + p.infoSection.SetTitle(" Audiobook information: ") + p.infoSection.SetTitleAlign(tview.AlignLeft) p.infoPanel = newInfoPanel() - infoSection.AddItem(p.infoPanel.t, 0, 0, 1, 1, 0, 0, true) + p.infoSection.AddItem(p.infoPanel.t, 0, 0, 1, 1, 0, 0, true) f := newForm() f.SetHorizontal(false) f.f.SetButtonsAlign(tview.AlignRight) f.AddButton("Stop", p.stopConfirmation) - infoSection.AddItem(f.f, 0, 1, 1, 1, 0, 0, false) - p.grid.AddItem(infoSection, 0, 0, 1, 1, 0, 0, false) + p.infoSection.AddItem(f.f, 0, 1, 1, 1, 0, 0, false) + p.grid.AddItem(p.infoSection, 0, 0, 1, 1, 0, 0, false) // audiobook build section p.buildSection = tview.NewGrid() @@ -64,7 +68,6 @@ func newBuildPage(dispatcher *mq.Dispatcher) *BuildPage { 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.setWeights(1, 2, 1, 1, 1, 5) @@ -75,30 +78,41 @@ func newBuildPage(dispatcher *mq.Dispatcher) *BuildPage { // copy section p.copySection = tview.NewGrid() p.copySection.SetColumns(-1) - p.copySection.SetTitle(" Output directory copy progress: ") + p.copySection.SetTitle(" Copying the book to the output directory: ") 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", "Total 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.t, 0, 0, 1, 1, 0, 0, true) p.grid.AddItem(p.copySection, 2, 0, 1, 1, 0, 0, true) - // build progress section - progressSection := tview.NewGrid() - progressSection.SetColumns(-1) - progressSection.SetBorder(true) - progressSection.SetTitle(" Build progress: ") - progressSection.SetTitleAlign(tview.AlignLeft) + // upload section + p.uploadSection = tview.NewGrid() + p.uploadSection.SetColumns(-1) + p.uploadSection.SetTitle(" Uploading the book to Audiobookshelf server: ") + 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.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.t, 0, 0, 1, 1, 0, 0, true) + p.grid.AddItem(p.uploadSection, 3, 0, 1, 1, 0, 0, true) + + // total progress section + p.progressSection = tview.NewGrid() + p.progressSection.SetColumns(-1) + p.progressSection.SetBorder(true) + p.progressSection.SetTitle(" Build progress: ") + p.progressSection.SetTitleAlign(tview.AlignLeft) p.progressTable = newTable() p.progressTable.setWeights(1) p.progressTable.setAlign(tview.AlignLeft) p.progressTable.t.SetSelectable(false, false) - progressSection.AddItem(p.progressTable.t, 0, 0, 1, 1, 0, 0, false) - p.grid.AddItem(progressSection, 3, 0, 1, 1, 0, 0, false) - p.progressSection = progressSection + p.progressSection.AddItem(p.progressTable.t, 0, 0, 1, 1, 0, 0, false) + p.grid.AddItem(p.progressSection, 4, 0, 1, 1, 0, 0, false) return p } @@ -120,10 +134,14 @@ func (p *BuildPage) dispatchMessage(m *mq.Message) { p.updateTotalBuildProgress(dto) case *dto.BuildComplete: p.buildComplete(dto) - case *dto.UploadFileProgress: + case *dto.CopyFileProgress: p.updateFileCopyProgress(dto) case *dto.CopyProgress: p.updateTotalCopyProgress(dto) + case *dto.UploadFileProgress: + p.updateFileUploadProgress(dto) + case *dto.UploadProgress: + p.updateTotalUploadProgress(dto) case *dto.CopyComplete: p.copyComplete(dto) case *dto.UploadComplete: @@ -138,6 +156,30 @@ func (p *BuildPage) dispatchMessage(m *mq.Message) { } func (p *BuildPage) displayBookInfo(ab *dto.Audiobook) { + + // dynamic grid layout generation + p.grid.Clear() + p.grid.SetColumns(0) + p.grid.SetRows(7, -1, 4) + p.grid.AddItem(p.infoSection, 0, 0, 1, 1, 0, 0, false) + p.grid.AddItem(p.buildSection, 1, 0, 1, 1, 0, 0, true) + if ab.Config.IsCopyToOutputDir() && ab.Config.IsUploadToAudiobookshef() { + p.grid.SetRows(7, -1, -1, -1, 4) + p.grid.AddItem(p.copySection, 2, 0, 1, 1, 0, 0, true) + p.grid.AddItem(p.uploadSection, 3, 0, 1, 1, 0, 0, true) + p.grid.AddItem(p.progressSection, 4, 0, 1, 1, 0, 0, false) + } else if ab.Config.IsCopyToOutputDir() { + p.grid.SetRows(7, -1, -1, 4) + p.grid.AddItem(p.copySection, 2, 0, 1, 1, 0, 0, true) + p.grid.AddItem(p.progressSection, 3, 0, 1, 1, 0, 0, false) + } else if ab.Config.IsUploadToAudiobookshef() { + p.grid.SetRows(7, -1, -1, 4) + p.grid.AddItem(p.uploadSection, 2, 0, 1, 1, 0, 0, true) + p.grid.AddItem(p.progressSection, 3, 0, 1, 1, 0, 0, false) + } else { + p.grid.AddItem(p.progressSection, 2, 0, 1, 1, 0, 0, false) + } + p.infoPanel.clear() // p.infoPanel.appendRow("", "") p.infoPanel.appendRow("Title:", ab.Title) @@ -153,14 +195,20 @@ func (p *BuildPage) displayBookInfo(ab *dto.Audiobook) { } p.buildTable.ScrollToBeginning() - if ab.Config.IsCopyToOutputDir() { - p.copyTable.clear() - p.copyTable.showHeader() - for i, part := range ab.Parts { - p.copyTable.appendRow(" "+strconv.Itoa(i+1)+" ", filepath.Base(part.M4BFile), part.Format, utils.SecondsToTime(part.Duration), utils.BytesToHuman(part.Size), "") - } - p.copyTable.ScrollToBeginning() + p.copyTable.clear() + p.copyTable.showHeader() + for i, part := range ab.Parts { + p.copyTable.appendRow(" "+strconv.Itoa(i+1)+" ", filepath.Base(part.M4BFile), part.Format, utils.SecondsToTime(part.Duration), utils.BytesToHuman(part.Size), "") } + p.copyTable.ScrollToBeginning() + + p.uploadTable.clear() + p.uploadTable.showHeader() + for i, part := range ab.Parts { + p.uploadTable.appendRow(" "+strconv.Itoa(i+1)+" ", filepath.Base(part.M4BFile), part.Format, utils.SecondsToTime(part.Duration), utils.BytesToHuman(part.Size), "") + } + p.uploadTable.ScrollToBeginning() + p.mq.SendMessage(mq.BuildPage, mq.TUI, &dto.SetFocusCommand{Primitive: p.buildTable.t}, true) p.mq.SendMessage(mq.BuildPage, mq.TUI, &dto.DrawCommand{Primitive: nil}, true) } @@ -172,7 +220,7 @@ func (p *BuildPage) stopConfirmation() { func (p *BuildPage) stopBuild() { // Stop the build here p.mq.SendMessage(mq.BuildPage, mq.BuildController, &dto.StopCommand{Process: "Build", Reason: "User request"}, false) - p.mq.SendMessage(mq.BuildPage, mq.Frame, &dto.SwitchToPageCommand{Name: "SearchPage"}, false) + p.switchToSearch() } func (p *BuildPage) updateFileBuildProgress(dp *dto.BuildFileProgress) { @@ -211,7 +259,7 @@ func (p *BuildPage) updateTotalBuildProgress(dp *dto.BuildProgress) { p.mq.SendMessage(mq.BuildPage, mq.TUI, &dto.DrawCommand{Primitive: nil}, false) } -func (p *BuildPage) updateFileCopyProgress(dp *dto.UploadFileProgress) { +func (p *BuildPage) updateFileCopyProgress(dp *dto.CopyFileProgress) { // update file progress col := 5 w := p.copyTable.getColumnWidth(col) - 3 @@ -248,8 +296,45 @@ func (p *BuildPage) updateTotalCopyProgress(dp *dto.CopyProgress) { p.mq.SendMessage(mq.BuildPage, mq.TUI, &dto.DrawCommand{Primitive: nil}, false) } +func (p *BuildPage) updateFileUploadProgress(dp *dto.UploadFileProgress) { + // update file progress + col := 5 + w := p.uploadTable.getColumnWidth(col) - 3 + progressText := fmt.Sprintf(" %3d%% ", dp.Percent) + barWidth := int((float32((w - len(progressText))) * float32(dp.Percent) / 100)) + progressBar := strings.Repeat("━", barWidth) + strings.Repeat(" ", w-len(progressText)-barWidth) + cell := p.uploadTable.t.GetCell(dp.FileId+1, col) + // cell.SetExpansion(0) + // cell.SetMaxWidth(50) + cell.Text = fmt.Sprintf("%s |%s|", progressText, progressBar) + // p.uploadTable.t.Select(dp.FileId+1, col) + p.mq.SendMessage(mq.BuildPage, mq.TUI, &dto.DrawCommand{Primitive: nil}, false) +} + +func (p *BuildPage) updateTotalUploadProgress(dp *dto.UploadProgress) { + if p.progressTable.GetRowCount() == 0 { + for i := 0; i < 2; i++ { + p.progressTable.appendRow("") + } + } + p.progressSection.SetTitle(" Upload progress: ") + infoCell := p.progressTable.t.GetCell(0, 0) + progressCell := p.progressTable.t.GetCell(1, 0) + infoCell.Text = fmt.Sprintf(" [yellow]Time elapsed: [white]%10s | [yellow]Files: [white]%10s | [yellow]Speed: [white]%12s | [yellow]ETA: [white]%10s", dp.Elapsed, dp.Files, dp.Speed, dp.ETA) + + col := 0 + w := p.progressTable.getColumnWidth(col) - 5 + progressText := fmt.Sprintf(" %3d%% ", dp.Percent) + barWidth := int((float32((w - len(progressText))) * float32(dp.Percent) / 100)) + progressBar := strings.Repeat("▒", barWidth) + strings.Repeat(" ", w-len(progressText)-barWidth) + // progressCell.SetExpansion(0) + // progressCell.SetMaxWidth(0) + progressCell.Text = fmt.Sprintf("%s |%s|", progressText, progressBar) + p.mq.SendMessage(mq.BuildPage, mq.TUI, &dto.DrawCommand{Primitive: nil}, false) +} + /* - * A chain of final operations -> ?Copy -> ?Upload -> ?Scan -> Cleanup - Done msg + * A chain of final operations: ?Copy -> ?Upload -> ?Scan -> Cleanup - Done msg */ func (p *BuildPage) buildComplete(c *dto.BuildComplete) { // copy the book to Output directory if needed @@ -265,9 +350,9 @@ func (p *BuildPage) copyComplete(c *dto.CopyComplete) { // upload the book to Audiobookshelf server if needed ab := c.Audiobook if ab.Config.IsUploadToAudiobookshef() { - p.mq.SendMessage(mq.BuildPage, mq.AudiobookshelfController, &dto.AudiobookshelfUploadCommand{Audiobook: ab}, true) + p.mq.SendMessage(mq.BuildPage, mq.UploadController, &dto.AbsUploadCommand{Audiobook: ab}, true) } else { - p.mq.SendMessage(mq.BuildPage, mq.BuildPage, &dto.AudiobookshelfScanCommand{Audiobook: ab}, true) + p.mq.SendMessage(mq.BuildPage, mq.BuildPage, &dto.UploadComplete{Audiobook: ab}, true) } } @@ -275,7 +360,7 @@ func (p *BuildPage) uploadComplete(c *dto.UploadComplete) { // launch Audiobookshelf library scan if needed ab := c.Audiobook if ab.Config.IsScanAudiobookshef() { - p.mq.SendMessage(mq.BuildPage, mq.AudiobookshelfController, &dto.AudiobookshelfScanCommand{Audiobook: c.Audiobook}, true) + p.mq.SendMessage(mq.BuildPage, mq.UploadController, &dto.AbsScanCommand{Audiobook: c.Audiobook}, true) } else { p.mq.SendMessage(mq.BuildPage, mq.BuildPage, &dto.ScanComplete{Audiobook: ab}, true) } @@ -291,6 +376,9 @@ func (p *BuildPage) cleanupComplete(c *dto.CleanupComplete) { } func (p *BuildPage) bookReadyMgs(ab *dto.Audiobook) { - newMessageDialog(p.mq, "Build Complete", "Audiobook has been created", p.buildSection) - //p.mq.SendMessage(mq.BuildPage, mq.Frame, &dto.SwitchToPageCommand{Name: "SearchPage"}, false) + newMessageDialog(p.mq, "Build Complete", "Audiobook has been created", p.buildSection, p.switchToSearch) +} + +func (p *BuildPage) switchToSearch() { + p.mq.SendMessage(mq.BuildPage, mq.Frame, &dto.SwitchToPageCommand{Name: "SearchPage"}, false) } diff --git a/internal/ui/chaptersPage.go b/internal/ui/chapters_page.go similarity index 100% rename from internal/ui/chaptersPage.go rename to internal/ui/chapters_page.go diff --git a/internal/ui/configPage.go b/internal/ui/config_page.go similarity index 99% rename from internal/ui/configPage.go rename to internal/ui/config_page.go index 02a6da8..1679411 100644 --- a/internal/ui/configPage.go +++ b/internal/ui/config_page.go @@ -182,7 +182,7 @@ func (p *ConfigPage) displayConfig(c *dto.DisplayConfigCommand) { p.maxFileSize.SetText(utils.ToString(p.configCopy.GetMaxFileSizeMb())) p.shortenTitles.SetChecked(p.configCopy.IsShortenTitle()) - p.uploadToAudiobookshelf.SetChecked(p.configCopy.IsCopyToOutputDir()) + p.uploadToAudiobookshelf.SetChecked(p.configCopy.IsUploadToAudiobookshef()) p.audiobookshelfUrl.SetText(p.configCopy.GetAudiobookshelfUrl()) p.audiobookshelfLibrary.SetText(p.configCopy.GetAudiobookshelfLibrary()) p.scanAudiobookshelf.SetChecked(p.configCopy.IsScanAudiobookshef()) diff --git a/internal/ui/dialog.go b/internal/ui/dialog.go index 14067f6..941b83b 100644 --- a/internal/ui/dialog.go +++ b/internal/ui/dialog.go @@ -90,7 +90,8 @@ func (d *dialogWindow) setFormAttributes() { d.form.SetButtonsAlign(tview.AlignCenter) } -func newMessageDialog(dispatcher *mq.Dispatcher, title string, message string, focus tview.Primitive) { +type OkFunc func() +func newMessageDialog(dispatcher *mq.Dispatcher, title string, message string, focus tview.Primitive, okFunc OkFunc) { d := newDialogWindow(dispatcher, 12, 80, focus) f := newForm() f.SetTitle(title) @@ -102,6 +103,7 @@ func newMessageDialog(dispatcher *mq.Dispatcher, title string, message string, f tv.SetTextAlign(tview.AlignCenter) f.AddFormItem(tv) f.AddButton("Ok", func() { + okFunc() d.Close() }) d.setForm(f.f) @@ -109,7 +111,6 @@ func newMessageDialog(dispatcher *mq.Dispatcher, title string, message string, f } type YesNoFunc func() - func newYesNoDialog(dispatcher *mq.Dispatcher, title string, message string, focus tview.Primitive, yesFunc YesNoFunc, noFunc YesNoFunc) { d := newDialogWindow(dispatcher, 11, 60, focus) f := newForm() diff --git a/internal/ui/downloadPage.go b/internal/ui/download_page.go similarity index 100% rename from internal/ui/downloadPage.go rename to internal/ui/download_page.go diff --git a/internal/ui/encodingPage.go b/internal/ui/encoding_page.go similarity index 100% rename from internal/ui/encodingPage.go rename to internal/ui/encoding_page.go diff --git a/internal/ui/footer.go b/internal/ui/footer.go index 9cfd6cd..f2a6ecc 100644 --- a/internal/ui/footer.go +++ b/internal/ui/footer.go @@ -79,7 +79,7 @@ func (f *footer) toggleBusyIndicator(c *dto.SetBusyIndicator) { f.busyFlag = true f.busyIndicator.SetTextColor(busyIndicatorFgColor) f.busyIndicator.SetBackgroundColor(busyIndicatorBgColor) - f.busyIndicator.SetText(" Busy > ") + f.busyIndicator.SetText(" Busy> ") go f.updateBusyIndicator() } else { f.busyFlag = false diff --git a/internal/ui/searchPage.go b/internal/ui/search_page.go similarity index 98% rename from internal/ui/searchPage.go rename to internal/ui/search_page.go index c87facb..601a2ed 100644 --- a/internal/ui/searchPage.go +++ b/internal/ui/search_page.go @@ -203,7 +203,7 @@ func (p *SearchPage) createBook() { // get selectet row from the results table row, _ := p.resultTable.t.GetSelection() if row <= 0 || len(p.searchResult) <= 0 || len(p.searchResult) < row { - newMessageDialog(p.mq, "Error", "Please perform a search first", p.searchSection) + newMessageDialog(p.mq, "Error", "Please perform a search first", p.searchSection, func() {}) } else { item := p.searchResult[row-1] // create new audiobook object @@ -248,7 +248,7 @@ func (p *SearchPage) showNothingFoundError(dto *dto.NothingFoundError) { newMessageDialog(p.mq, "Error", "No results were found for your search term: [darkblue]'"+dto.SearchCondition+"'[black].\n"+ "Please revise your search criteria.", - p.searchSection) + p.searchSection, func() {}) } func (p *SearchPage) showFFMPEGNotFoundError(dto *dto.FFMPEGNotFoundError) { @@ -256,7 +256,7 @@ func (p *SearchPage) showFFMPEGNotFoundError(dto *dto.FFMPEGNotFoundError) { "This application requires the utilities [darkblue]ffmpeg[black] and [darkblue]ffprobe[black].\n"+ "Please install both [darkblue]ffmpeg[black] and [darkblue]ffprobe[black] by following the instructions provided on FFMPEG website\n"+ "[darkblue]https://ffmpeg.org/download.html", - p.searchSection) + p.searchSection, func() {}) } func (p *SearchPage) showNewVersionMessage(dto *dto.NewAppVersionFound) { @@ -264,5 +264,5 @@ func (p *SearchPage) showNewVersionMessage(dto *dto.NewAppVersionFound) { "New version of the application has been released: [darkblue]"+dto.NewVersion+"[black]\n"+ "Your current version is [darkblue]"+dto.CurrentVersion+"[black]\n"+ "You can download the new version of the application from:\n[darkblue]https://abb_ia/releases", - p.searchSection) + p.searchSection, func() {}) } diff --git a/internal/utils/filepath.go b/internal/utils/filepath.go index b28e73b..0b5f8e7 100644 --- a/internal/utils/filepath.go +++ b/internal/utils/filepath.go @@ -11,7 +11,7 @@ func SanitizeFilePath(path string) string { {"!", "."}, {"?", "."}, {"…", ""}, - {"#","N"}, + {"#", "N"}, {"[", ""}, {"]", ""}, } @@ -32,3 +32,41 @@ func SanitizeFilePath(path string) string { } return path } + +// TODO: Refactor this (regex?) +func SanitizeMp3FileName(fileName string) string { + replacements := [][2]string{ + {"_64kb", ""}, + {"_24kb", ""}, + {"_32kb", ""}, + {"_40kb", ""}, + {"_48kb", ""}, + {"_56kb", ""}, + {"_64kb", ""}, + {"_80kb", ""}, + {"_96kb", ""}, + {"_112kb", ""}, + {"_128kb", ""}, + {"_144kb", ""}, + {"_160kb", ""}, + {"_224kb", ""}, + {"_256kb", ""}, + {"_320kb", ""}, + {"_vbr", ""}, + } + + for { + found := false + for _, row := range replacements { + old := row[0] + new := row[1] + if strings.Contains(fileName, old) { + fileName = strings.ReplaceAll(fileName, old, new) + } + } + if !found { + break + } + } + return fileName +} diff --git a/internal/utils/misc.go b/internal/utils/misc.go index d190b85..1207bce 100644 --- a/internal/utils/misc.go +++ b/internal/utils/misc.go @@ -28,6 +28,11 @@ func GetIndex(s []string, str string) int { return -1 } +// Remove an element from a slice +func RemoveElement(slice []interface{}, index int) []interface{} { + return append(slice[:index], slice[index+1:]...) +} + func AddSpaces(list []string) []string { var output []string for _, v := range list { diff --git a/tools/check_error.go b/tools/check_error.go deleted file mode 100644 index a848876..0000000 --- a/tools/check_error.go +++ /dev/null @@ -1,10 +0,0 @@ -package tools - -import "abb_ia/internal/logger" - -func CheckError(e error) { - if e != nil { - logger.Error(e.Error()) - panic(e) - } -}