Skip to content

Commit

Permalink
Merge pull request #4 from vpoluyaktov/Audiobookshelf_upload
Browse files Browse the repository at this point in the history
Audiobookshelf upload feature implemented
  • Loading branch information
vpoluyaktov authored Nov 23, 2023
2 parents 8789197 + 66f813b commit 5d10ad1
Show file tree
Hide file tree
Showing 58 changed files with 1,086 additions and 480 deletions.
4 changes: 2 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
bin/
mock/
output/
tmp/
dist/
__debug_bin
__debug_bin*
abb_ia.log
config.yaml
2 changes: 1 addition & 1 deletion .goreleaser.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
<br>

![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)

<br><br>

Expand Down
8 changes: 4 additions & 4 deletions cmd/root.go
Original file line number Diff line number Diff line change
@@ -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() {
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
module github.com/vpoluyaktov/abb_ia
module abb_ia

go 1.18

Expand Down
3 changes: 0 additions & 3 deletions go.sum
Original file line number Diff line number Diff line change
@@ -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=
Expand Down Expand Up @@ -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=
Expand Down
181 changes: 156 additions & 25 deletions internal/audiobookshelf/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,75 +4,99 @@ import (
"bytes"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"mime/multipart"
"net/http"
"os"
"path/filepath"
"strconv"

"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
Expand All @@ -81,14 +105,23 @@ 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)
Expand All @@ -104,4 +137,102 @@ func ScanLibrary(url string, authToken string, libraryID string) error {
} else {
return nil
}
}
}

// Upload audiobook to The Audibookshelf server
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 {
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{
FileId: i,
FileName: filepath.Base(file.Name()),
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
}

// 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
Pos int64
Percent int
Callback Fn
}

func (pr *ProgressReader) Read(p []byte) (int, error) {
n, err := pr.Reader.Read(p)
if err == nil {
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
}
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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)
}
}

Expand All @@ -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)
}
}
}
Expand All @@ -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)
}
}
}
}
Expand Down
Loading

0 comments on commit 5d10ad1

Please sign in to comment.