Skip to content

Commit

Permalink
MP4 package implemented
Browse files Browse the repository at this point in the history
  • Loading branch information
vpoluyaktov committed Dec 12, 2023
1 parent d3d59ae commit da379da
Show file tree
Hide file tree
Showing 4 changed files with 274 additions and 25 deletions.
3 changes: 3 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ require (
require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/gdamore/encoding v1.0.0 // indirect
github.com/google/uuid v1.1.2 // indirect
github.com/kr/pretty v0.3.1 // indirect
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/mattn/go-runewidth v0.0.14 // indirect
Expand All @@ -25,9 +26,11 @@ require (
)

require (
github.com/abema/go-mp4 v1.1.1
github.com/hashicorp/go-version v1.6.0
github.com/rivo/tview v0.0.0-20230406072732-e22ce9588bb4
github.com/stretchr/testify v1.8.3
github.com/sunfish-shogi/bufseekio v0.1.0
golang.org/x/net v0.17.0 // indirect
jaytaylor.com/html2text v0.0.0-20230321000545-74c2419ad056
)
20 changes: 20 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
github.com/abema/go-mp4 v1.1.1 h1:OfzkdMO6SWTBR1ltNSVwlTHatrAK9I3iYLQfkdEMMuc=
github.com/abema/go-mp4 v1.1.1/go.mod h1:vPl9t5ZK7K0x68jh12/+ECWBCXoWuIDtNgPtU2f04ws=
github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
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=
github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko=
Expand All @@ -7,10 +11,16 @@ github.com/gdamore/tcell/v2 v2.6.0 h1:OKbluoP9VYmJwZwq/iLb4BxwKcwGthaa1YNBJIyCyS
github.com/gdamore/tcell/v2 v2.6.0/go.mod h1:be9omFATkdr0D9qewWW3d+MEvl5dha+Etb5y65J2H8Y=
github.com/go-resty/resty/v2 v2.7.0 h1:me+K9p3uhSmXtrBZ4k9jcEAfJmuC8IivWHwaLZwPrFY=
github.com/go-resty/resty/v2 v2.7.0/go.mod h1:9PWDzw47qPphMRFfhsyk0NnSgvluHcljSMVIq3w7q0I=
github.com/google/uuid v1.1.2 h1:EVhdT+1Kseyi1/pUmXKaFxYsDNy9RQYkMWRH68J/W7Y=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/hashicorp/go-version v1.6.0 h1:feTTfFNnjP967rlCxM/I9g701jU+RN74YKx2mOkIeek=
github.com/hashicorp/go-version v1.6.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
Expand All @@ -20,6 +30,7 @@ github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWV
github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
github.com/orcaman/writerseeker v0.0.0-20200621085525-1d3f536ff85e/go.mod h1:nBdnFKj15wFbf94Rwfq4m30eAcyY9V/IyKAGQFtqkW0=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
Expand All @@ -32,8 +43,13 @@ github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZV
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf h1:pvbZ0lM0XWPBqUKqFU8cmavspvIl9nulOYwdy6IFRRo=
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf/go.mod h1:RJID2RhlZKId02nZ62WenDCkgHFerpIOmW0iT7GKmXM=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY=
github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/sunfish-shogi/bufseekio v0.0.0-20210207115823-a4185644b365/go.mod h1:dEzdXgvImkQ3WLI+0KQpmEx8T/C/ma9KeS3AfmU899I=
github.com/sunfish-shogi/bufseekio v0.1.0 h1:zu38kFbv0KuuiwZQeuYeS02U9AM14j0pVA9xkHOCJ2A=
github.com/sunfish-shogi/bufseekio v0.1.0/go.mod h1:dEzdXgvImkQ3WLI+0KQpmEx8T/C/ma9KeS3AfmU899I=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
Expand All @@ -47,6 +63,7 @@ golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
Expand Down Expand Up @@ -74,6 +91,9 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/src-d/go-billy.v4 v4.3.2/go.mod h1:nDjArDMp+XMs1aFAESLRjfGSgfvoYN0hDfzEk0GjC98=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
jaytaylor.com/html2text v0.0.0-20230321000545-74c2419ad056 h1:6YFJoB+0fUH6X3xU/G2tQqCYg+PkGtnZ5nMR5rpw72g=
Expand Down
71 changes: 46 additions & 25 deletions internal/controller/build_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package controller
import (
"fmt"
"io"
"io/ioutil"
"net"
"net/http"
"os"
Expand All @@ -13,6 +14,7 @@ import (

"abb_ia/internal/dto"
"abb_ia/internal/ffmpeg"
"abb_ia/internal/mp4"
"abb_ia/internal/utils"

"abb_ia/internal/logger"
Expand Down Expand Up @@ -144,15 +146,6 @@ func (c *BuildController) createMetadata(ab *dto.Audiobook) {
f.WriteString("major_brand=isom\n")
f.WriteString("minor_version=512\n")
f.WriteString("compatible_brands=isomiso2mp41\n")
f.WriteString("title=" + ab.Title + "\n")
f.WriteString("artist=" + ab.Author + "\n")
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.LicenseUrl + "\n")
f.WriteString("comment=This audiobook was created using the 'Audiobook Builder' tool: https://github.com/"+ab.Config.GetRepoOwner()+"/"+ab.Config.GetRepoName()+"\\\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")
f.WriteString("TIMEBASE=1/1000\n")
Expand Down Expand Up @@ -218,22 +211,50 @@ func (c *BuildController) buildAudiobookPart(ab *dto.Audiobook, partId int) {
Run()
if err != nil {
logger.Error("FFMPEG Error: " + string(err.Error()))
} else {
// add Metadata, cover image and convert to .m4b
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))

go c.killSwitch(ffmpeg)
_, err := ffmpeg.Run()
if err != nil && !c.stopFlag {
logger.Error("FFMPEG Error: " + string(err.Error()))
}
return
}

// add chapters and convert to .m4b
ffmpeg := ffmpeg.NewFFmpeg().
Input(part.AACFile, "").
Input(part.MetadataFile, "").
Output(part.M4BFile, "-map_metadata 1 -y -vn -y -acodec copy").
Overwrite(true).
Params("-hide_banner -nostdin -nostats").
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()))
return
}

// clean up
os.Remove(part.AACFile)

// add tags and cover image
m4b, er := mp4.NewMp4(part.M4BFile)
if er != nil && !c.stopFlag {
logger.Error("Can't open m4b file for write: " + err.Error())
}
m4b.SetTag("\xa9nam", ab.Title)
m4b.SetTag("\xa9alb", ab.Title)
m4b.SetTag("\xa9ART", ab.Author)
m4b.SetTag("desc", ab.Description)
m4b.SetTag("cprt", ab.LicenseUrl)
m4b.SetTag("purl", ab.IaURL)
m4b.SetTag("\xa9cmt", "This audiobook was created using the 'Audiobook Builder' tool: https://github.com/"+ab.Config.GetRepoOwner()+"/"+ab.Config.GetRepoName()+"\n"+
"The audio files used for this book were obtained from the Internet Archive site: "+ab.IaURL)

imageData, er := ioutil.ReadFile(ab.CoverFile)
if er == nil {
m4b.SetImage(imageData, mp4.DataTypeJPEG)
}

er = m4b.Save()
if er != nil {
logger.Error("Can't save m4b file: " + err.Error())
}
}

Expand Down
205 changes: 205 additions & 0 deletions internal/mp4/mp4.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
package mp4

import (
"fmt"
"os"

"github.com/abema/go-mp4"
"github.com/sunfish-shogi/bufseekio"
)

const (
DataTypeBinary = 0
DataTypeStringUTF8 = 1
DataTypeJPEG = 14
DataTypePNG = 13
)

type Mp4 struct {
fileName string
mp4Tags Mp4Tags
}

type Mp4Tags map[string]Mp4Tag
type Mp4Tag struct {
Name string
Path string
DataType uint32
Data []byte
Exists bool
}

func NewMp4(fileName string) (*Mp4, error) {
m4b := &Mp4{fileName: fileName}
tags, err := m4b.GetMp4Tags()
if err != nil {
return nil, err
}
m4b.mp4Tags = tags
return m4b, nil
}

func (m4b *Mp4) GetMp4Tags() (Mp4Tags, error) {
if m4b.mp4Tags != nil {
return m4b.mp4Tags, nil
}
inputFile, _ := os.Open(m4b.fileName)
defer inputFile.Close()
tags := make(Mp4Tags)
r := bufseekio.NewReadSeeker(inputFile, 128*1024, 4)
mp4.ReadBoxStructure(r, func(h *mp4.ReadHandle) (interface{}, error) {
if h.BoxInfo.Context.UnderIlst && h.BoxInfo.Type != mp4.BoxTypeData() {
tags[h.BoxInfo.Type.String()] = Mp4Tag{
Name: h.BoxInfo.Type.String(),
Path: getPath(h.Path),
Exists: true,
}
} else if h.BoxInfo.Context.UnderIlstMeta && h.BoxInfo.Type == mp4.BoxTypeData() {
tagName := h.Path[len(h.Path)-2].String()
tag := tags[tagName]
box, _, err := h.ReadPayload()
if err != nil && box == nil {
return nil, err
}
boxData := box.(*mp4.Data)
tag.DataType = boxData.DataType
tag.Data = boxData.Data
tags[tagName] = tag
}
if h.BoxInfo.IsSupportedType() {
h.Expand()
}
return nil, nil
})
m4b.mp4Tags = tags
return m4b.mp4Tags, nil
}

func (m4b *Mp4) SetMp4Tag(tag *Mp4Tag) error {
t := m4b.mp4Tags[tag.Name]
if !t.Exists {
t.Name = tag.Name
t.DataType = tag.DataType
t.Exists = false
t.Data = tag.Data
} else {
t.Data = tag.Data
}
m4b.mp4Tags[tag.Name] = t
return nil
}

func (m4b *Mp4) SetTag(name string, tag string) error {
if len(name) != 4 {
return fmt.Errorf("tag name must be 4 characters exactly")
}
t := m4b.mp4Tags[name]
if !t.Exists {
t.Name = name
t.DataType = mp4.DataTypeStringUTF8
t.Exists = false
t.Data = []byte(tag)
} else {
t.Data = []byte(tag)
}
m4b.mp4Tags[name] = t
return nil
}

func (m4b *Mp4) SetImage(imageData []byte, imageType uint32) error {
t := m4b.mp4Tags["covr"]
if !t.Exists {
t.Name = "covr"
t.DataType = imageType
t.Exists = false
t.Data = imageData
} else {
t.Data = imageData
}
m4b.mp4Tags["covr"] = t
return nil
}

func (m4b *Mp4) Save() error {
inputFileName := m4b.fileName
outputFileName := inputFileName + ".tmp"

inputFile, err := os.Open(inputFileName)
if err != nil {
return fmt.Errorf("can't open %s: %v", inputFileName, err)
}
outputFile, err := os.OpenFile(outputFileName, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644)
if err != nil {
return fmt.Errorf("can't create temporary file %s: %v", outputFileName, err)
}
defer inputFile.Close()
defer outputFile.Close()

r := bufseekio.NewReadSeeker(inputFile, 128*1024, 4)
w := mp4.NewWriter(outputFile)

mp4.ReadBoxStructure(r, func(h *mp4.ReadHandle) (interface{}, error) {
if !h.BoxInfo.IsSupportedType() {
// copy all data for unsupported box types
return nil, w.CopyBox(r, &h.BoxInfo)
}

// write moov box header
_, err := w.StartBox(&h.BoxInfo)
if err != nil {
return nil, err
}

// read payload
box, _, err := h.ReadPayload()
if err != nil && box == nil {
return nil, err
}
for _, tag := range m4b.mp4Tags {
if h.BoxInfo.Type == mp4.BoxTypeIlst() && !tag.Exists {
// create new tag
w.StartBox(&mp4.BoxInfo{Type: mp4.StrToBoxType(tag.Name)}) // meta container
w.StartBox(&mp4.BoxInfo{Type: mp4.BoxTypeData()}) // data container
dataContainer := &mp4.Data{
DataType: tag.DataType,
DataLang: 0x00,
Data: []byte(tag.Data),
}
mp4.Marshal(w, dataContainer, mp4.Context{UnderIlst: true, UnderIlstMeta: true})
w.EndBox() // data container
w.EndBox() // meta container
} else if getPath(h.Path) == tag.Path+"data/" && h.BoxInfo.Type == mp4.BoxTypeData() {
// update existing tag
boxData := box.(*mp4.Data)
boxData.Data = []byte(tag.Data)
}
}

// write box playload
if _, err := mp4.Marshal(w, box, h.BoxInfo.Context); err != nil {
return nil, err
}
// expand all of offsprings
if _, err := h.Expand(); err != nil {
return nil, err
}
// rewrite box size
_, err = w.EndBox()
return nil, err
})

inputFile.Close()
outputFile.Close()
// rename temporary file to final one
os.Remove(inputFileName)
os.Rename(outputFileName, inputFileName)
return nil
}

func getPath(hPath mp4.BoxPath) string {
path := ""
for _, p := range hPath {
path += p.String() + "/"
}
return path
}

0 comments on commit da379da

Please sign in to comment.