commit b7da87cc65041a6ffa03d83d2cf1cae9eb926015 Author: Knut Ahlers Date: Sun Sep 8 12:47:49 2019 +0200 Initial version diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..88bb961 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +id3patch diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..5065a39 --- /dev/null +++ b/go.mod @@ -0,0 +1,12 @@ +module github.com/Luzifer/id3patch + +go 1.13 + +require ( + github.com/Luzifer/rconfig/v2 v2.2.1 + github.com/bogem/id3v2 v1.1.1 + github.com/pkg/errors v0.8.1 + github.com/sirupsen/logrus v1.4.2 + golang.org/x/text v0.3.2 // indirect + gopkg.in/validator.v2 v2.0.0-20190827175613-1a84e0480e5b // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..1d13aa7 --- /dev/null +++ b/go.sum @@ -0,0 +1,29 @@ +github.com/Luzifer/rconfig v2.2.0+incompatible h1:Kle3+rshPM7LxciOheaR4EfHUzibkDDGws04sefQ5m8= +github.com/Luzifer/rconfig v2.2.0+incompatible/go.mod h1:9pet6z2+mm/UAB0jF/rf0s62USfHNolzgR6Q4KpsJI0= +github.com/Luzifer/rconfig/v2 v2.2.1 h1:zcDdLQlnlzwcBJ8E0WFzOkQE1pCMn3EbX0dFYkeTczg= +github.com/Luzifer/rconfig/v2 v2.2.1/go.mod h1:OKIX0/JRZrPJ/ZXXWklQEFXA6tBfWaljZbW37w+sqBw= +github.com/bogem/id3v2 v1.1.1 h1:FnjS2vytMeEb39tOMG09uz852MaEccA2A3asRM3XxbE= +github.com/bogem/id3v2 v1.1.1/go.mod h1:D1rDm80qF/ocBU+Ik8U4RKnwMq/oNkkB8vGcnrlMJmM= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4= +github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg= +github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894 h1:Cz4ceDQGXuKRnVBDTS23GTn/pU5OE2C0WrNTOYK1Uuc= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/validator.v2 v2.0.0-20180514200540-135c24b11c19/go.mod h1:o4V0GXN9/CAmCsvJ0oXYZvrZOe7syiDZSN1GWGZTGzc= +gopkg.in/validator.v2 v2.0.0-20190827175613-1a84e0480e5b h1:b11tvJzEfkdoOQNdYxuvkRvD7VNo5/A0U2Dqiq+wKdw= +gopkg.in/validator.v2 v2.0.0-20190827175613-1a84e0480e5b/go.mod h1:o4V0GXN9/CAmCsvJ0oXYZvrZOe7syiDZSN1GWGZTGzc= +gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/main.go b/main.go new file mode 100644 index 0000000..c25051f --- /dev/null +++ b/main.go @@ -0,0 +1,142 @@ +package main + +import ( + "bytes" + "fmt" + "io" + "io/ioutil" + "os" + + "github.com/Luzifer/rconfig/v2" + "github.com/bogem/id3v2" + "github.com/pkg/errors" + log "github.com/sirupsen/logrus" +) + +var ( + cfg = struct { + Album string `flag:"album" default:"" description:"Set album tag"` + Artist string `flag:"artist" default:"" description:"Set artist tag"` + File string `flag:"file,f" default:"" description:"File to read / write" validate:"nonzero"` + LogLevel string `flag:"log-level" default:"info" description:"Log level (debug, info, warn, error, fatal)"` + Title string `flag:"title" default:"" description:"Set title tag"` + VersionAndExit bool `flag:"version" default:"false" description:"Prints current version and exits"` + Year string `flag:"year" default:"" description:"Set year tag"` + }{} + + version = "dev" +) + +func init() { + rconfig.AutoEnv(true) + if err := rconfig.ParseAndValidate(&cfg); err != nil { + log.Fatalf("Unable to parse commandline options: %s", err) + } + + if cfg.VersionAndExit { + fmt.Printf("id3patch %s\n", version) + os.Exit(0) + } + + if l, err := log.ParseLevel(cfg.LogLevel); err != nil { + log.WithError(err).Fatal("Unable to parse log level") + } else { + log.SetLevel(l) + } +} + +func main() { + var ( + logger = log.WithField("file", cfg.File) + needsWrite bool + ) + + tag, err := id3v2.Open(cfg.File, id3v2.Options{Parse: true}) + switch err { + + case nil: + // Everything fine + + case id3v2.ErrUnsupportedVersion: + logger.Warn("No supported ID3v2 tags found") + + default: + logger.WithError(err).Fatal("Unable to open file") + + } + defer tag.Close() + + logger.WithFields(log.Fields{ + "album": tag.Album(), + "artist": tag.Artist(), + "title": tag.Title(), + "tag_version": tag.Version(), + "year": tag.Year(), + }).Info("File opened successfully") + + needsWrite = modTag(cfg.Album, tag.Album, tag.SetAlbum, needsWrite) + needsWrite = modTag(cfg.Artist, tag.Artist, tag.SetArtist, needsWrite) + needsWrite = modTag(cfg.Title, tag.Title, tag.SetTitle, needsWrite) + needsWrite = modTag(cfg.Year, tag.Year, tag.SetYear, needsWrite) + + if !needsWrite { + logger.Info("No tags changed, no write needed") + return + } + + if err := save(tag); err != nil { + logger.WithError(err).Fatal("Unable to save tags") + } + + logger.Info("Tags written successfully") +} + +func modTag(content string, contentFunc func() string, setFunc func(string), needsWrite bool) bool { + if content == "" || content == contentFunc() { + return needsWrite + } + + setFunc(content) + return true +} + +func save(tag *id3v2.Tag) error { + var err = tag.Save() + switch err { + + case id3v2.ErrNoFile: + // We need to do the save ourselves + + default: + return err + + } + + // Library does not supporting initial tag addition, assemble the + // newly added tag ourselves... + var buf = new(bytes.Buffer) + + oStat, err := os.Stat(cfg.File) + if err != nil { + return errors.Wrap(err, "Unable to determine original file stats") + } + + oFile, err := os.Open(cfg.File) + if err != nil { + return errors.Wrap(err, "Unable to open original file") + } + + if _, err = tag.WriteTo(buf); err != nil { + return errors.Wrap(err, "Unable to write tag header to buffer") + } + + if _, err = io.Copy(buf, oFile); err != nil { + return errors.Wrap(err, "Unable to copy original file contents to buffer") + } + + if err = oFile.Close(); err != nil { + return errors.Wrap(err, "Unable to close original file") + } + + return errors.Wrap(ioutil.WriteFile(cfg.File, buf.Bytes(), oStat.Mode()), "Unable to write file contents") +}