diff --git a/go.mod b/go.mod index 64b8b6f..1211b27 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,10 @@ module git.kapelle.org/niklas/ripsort go 1.25.7 require ( - github.com/alexflint/go-arg v1.6.1 // indirect - github.com/alexflint/go-scalar v1.2.0 // indirect - github.com/dhowden/tag v0.0.0-20240417053706-3d75831295e8 // indirect + github.com/alexflint/go-arg v1.6.1 + github.com/dhowden/tag v0.0.0-20240417053706-3d75831295e8 + github.com/go-flac/flacvorbis/v2 v2.0.2 + github.com/go-flac/go-flac/v2 v2.0.4 ) + +require github.com/alexflint/go-scalar v1.2.0 // indirect diff --git a/go.sum b/go.sum index f400d8c..994fb28 100644 --- a/go.sum +++ b/go.sum @@ -2,8 +2,18 @@ github.com/alexflint/go-arg v1.6.1 h1:uZogJ6VDBjcuosydKgvYYRhh9sRCusjOvoOLZopBln github.com/alexflint/go-arg v1.6.1/go.mod h1:nQ0LFYftLJ6njcaee0sU+G0iS2+2XJQfA8I062D0LGc= github.com/alexflint/go-scalar v1.2.0 h1:WR7JPKkeNpnYIOfHRa7ivM21aWAdHD0gEWHCx+WQBRw= github.com/alexflint/go-scalar v1.2.0/go.mod h1:LoFvNMqS1CPrMVltza4LvnGKhaSpc3oyLEBUZVhhS2o= +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/dhowden/tag v0.0.0-20240417053706-3d75831295e8 h1:OtSeLS5y0Uy01jaKK4mA/WVIYtpzVm63vLVAPzJXigg= github.com/dhowden/tag v0.0.0-20240417053706-3d75831295e8/go.mod h1:apkPC/CR3s48O2D7Y++n1XWEpgPNNCjXYga3PPbJe2E= +github.com/go-flac/flacvorbis/v2 v2.0.2 h1:xCL3OhxrxWkHrbWUBvGNe+6FQ03yLmBbz0v5z4V2PoQ= +github.com/go-flac/flacvorbis/v2 v2.0.2/go.mod h1:SwTB5gs13VaM/N7rstwPoUsPibiMKklgwybYP9dYo2g= +github.com/go-flac/go-flac/v2 v2.0.4 h1:atf/kFa8U9idtkA//NO22XGr+MzQLeXZecnmP9sYBf0= +github.com/go-flac/go-flac/v2 v2.0.4/go.mod h1:sYOlTKxutMW0RDYF+KlD6Zn+VOCZlIFQG/r/usPveCs= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/metadata.go b/internal/metadata/metadata.go similarity index 51% rename from internal/metadata.go rename to internal/metadata/metadata.go index 9c33574..7e62ad7 100644 --- a/internal/metadata.go +++ b/internal/metadata/metadata.go @@ -1,33 +1,28 @@ -package ripsort +package metadata import ( "fmt" + "log/slog" "os" + "path/filepath" "strings" "github.com/dhowden/tag" ) -type metadata struct { - Format tag.Format - FileType tag.FileType +type Metadata struct { Title string Artist []string Album string AlbumArtist []string - Composer []string - Genre string - Year int Track int TotalTracks int Disc int TotalDiscs int - Lyrics string Comment string - HasPicture bool } -func ReadAudioTags(filePath string) (*metadata, error) { +func ReadAudioTags(filePath string) (*Metadata, error) { f, err := os.Open(filePath) if err != nil { return nil, fmt.Errorf("failed to open file: %w", err) @@ -42,24 +37,42 @@ func ReadAudioTags(filePath string) (*metadata, error) { track, totalTracks := m.Track() disc, totalDiscs := m.Disc() - info := &metadata{ - Format: m.Format(), - FileType: m.FileType(), + info := &Metadata{ Title: m.Title(), - Artist: strings.Split(m.Artist(), ";"), + Artist: parseSeperatedTag(m.Artist()), Album: m.Album(), - AlbumArtist: strings.Split(m.AlbumArtist(), ";"), - Composer: strings.Split(m.Composer(), ";"), - Genre: m.Genre(), - Year: m.Year(), + AlbumArtist: parseSeperatedTag(m.AlbumArtist()), Track: track, TotalTracks: totalTracks, Disc: disc, TotalDiscs: totalDiscs, - Lyrics: m.Lyrics(), Comment: m.Comment(), - HasPicture: m.Picture() != nil, } return info, nil } + +func parseSeperatedTag(s string) []string { + parsed := make([]string, 0) + + for element := range strings.SplitSeq(s, ";") { + parsed = append(parsed, strings.TrimSpace(element)) + } + + return parsed +} + +func UpdateMetadata(src, dst string, m Metadata) error { + + ext := strings.ToLower(filepath.Ext(src)) + + switch { + case ext == ".flac": + return updateFlacMetadata(m, src, dst) + default: + slog.Warn("Unsupported format for updating metadata") + } + + return nil + +} diff --git a/internal/metadata/vobis.go b/internal/metadata/vobis.go new file mode 100644 index 0000000..116b35f --- /dev/null +++ b/internal/metadata/vobis.go @@ -0,0 +1,57 @@ +package metadata + +import ( + "log/slog" + "strconv" + + "github.com/go-flac/flacvorbis/v2" + "github.com/go-flac/go-flac/v2" +) + +func updateFlacMetadata(m Metadata, input, output string) error { + f, err := flac.ParseFile(input) + if err != nil { + return err + } + + vorbisMeta := createVorbisMetaBlock(m) + meta := vorbisMeta.Marshal() + + replaced := false + for i, block := range f.Meta { + if block.Type == flac.VorbisComment { + f.Meta[i] = &meta + replaced = true + break + } + } + + if !replaced { + f.Meta = append(f.Meta, &meta) + } + + return f.Save(output) +} + +func createVorbisMetaBlock(m Metadata) flacvorbis.MetaDataBlockVorbisComment { + vorbisMeta := flacvorbis.New() + + vorbisMeta.Add(flacvorbis.FIELD_TITLE, m.Title) + vorbisMeta.Add(flacvorbis.FIELD_ALBUM, m.Album) + vorbisMeta.Add("COMMENT", m.Comment) + vorbisMeta.Add("TRACKNUMBER", strconv.Itoa(m.Track)) + vorbisMeta.Add("TOTALTRACKS", strconv.Itoa(m.TotalTracks)) + vorbisMeta.Add("DISCNUMBER", strconv.Itoa(m.Disc)) + vorbisMeta.Add("TOTALDISCS", strconv.Itoa(m.TotalDiscs)) + + for _, artist := range m.Artist { + vorbisMeta.Add(flacvorbis.FIELD_ARTIST, artist) + slog.Debug("Added ARTIST filed to metadata", "artist", artist) + } + + for _, albumArtist := range m.AlbumArtist { + vorbisMeta.Add("ALBUMARTIST", albumArtist) + } + + return *vorbisMeta +} diff --git a/internal/ripsort.go b/internal/ripsort.go index c6ab40e..825aaf4 100644 --- a/internal/ripsort.go +++ b/internal/ripsort.go @@ -6,30 +6,25 @@ import ( "log/slog" "os" "path/filepath" + + "git.kapelle.org/niklas/ripsort/internal/metadata" ) func Scan(filePath string) { - info, err := ReadAudioTags(filePath) + info, err := metadata.ReadAudioTags(filePath) if err != nil { slog.Error("Reading metadata", "file", filePath, "err", err) os.Exit(1) } - fmt.Printf("Format: %v\n", info.Format) - fmt.Printf("File Type: %v\n", info.FileType) fmt.Printf("Title: %s\n", info.Title) fmt.Printf("Artist: %s\n", info.Artist) fmt.Printf("Album: %s\n", info.Album) fmt.Printf("Album Artist: %s\n", info.AlbumArtist) - fmt.Printf("Composer: %s\n", info.Composer) - fmt.Printf("Genre: %s\n", info.Genre) - fmt.Printf("Year: %d\n", info.Year) fmt.Printf("Track: %d/%d\n", info.Track, info.TotalTracks) fmt.Printf("Disc: %d/%d\n", info.Disc, info.TotalDiscs) - fmt.Printf("Lyrics: %s\n", info.Lyrics) fmt.Printf("Comment: %s\n", info.Comment) - fmt.Printf("Has Picture: %v\n", info.HasPicture) sortPath := pathForFile(filePath, *info) fmt.Printf("Sort path: %s\n", sortPath) @@ -48,7 +43,7 @@ func Sort(dst, path string) { slog.Error("Unsupported file format", "file", path) os.Exit(1) } - if err := sortSong(path, dst); err != nil { + if err := sortSong(path, dst, true); err != nil { slog.Error("Failed to sort single file", "file", path, "err", err) os.Exit(1) } @@ -66,7 +61,7 @@ func Sort(dst, path string) { if !fileSupported(p) { return nil } - if err := sortSong(p, dst); err != nil { + if err := sortSong(p, dst, true); err != nil { slog.Error("Failed to sort file", "file", p, "err", err) return err } diff --git a/internal/sorter.go b/internal/sorter.go index aa9e67b..5a030de 100644 --- a/internal/sorter.go +++ b/internal/sorter.go @@ -8,6 +8,8 @@ import ( "path/filepath" "regexp" "strings" + + "git.kapelle.org/niklas/ripsort/internal/metadata" ) func fileSupported(file string) bool { @@ -19,8 +21,8 @@ func fileSupported(file string) bool { return supported[strings.ToLower(filepath.Ext(file))] } -func sortSong(src, dst string) error { - m, err := ReadAudioTags(src) +func sortSong(src, dst string, updateMeta bool) error { + m, err := metadata.ReadAudioTags(src) if err != nil { return err } @@ -28,10 +30,20 @@ func sortSong(src, dst string) error { dstPath := pathForFile(src, *m) finalPath := filepath.Join(dst, dstPath) - slog.Info("Copying song", "file", src, "dst", dstPath) - err = copyFile(src, finalPath) - if err != nil { - return err + if updateMeta { + + slog.Info("Copying song with updated metadata", "file", src, "dst", dstPath) + err = copyFileUpdateMetadata(src, finalPath, *m) + if err != nil { + return err + } + } else { + + slog.Info("Copying song", "file", src, "dst", dstPath) + err = copyFile(src, finalPath) + if err != nil { + return err + } } extraFiles := checkForExtraFiles(src) @@ -66,7 +78,7 @@ func sanitizeName(name string) *string { return &name } -func getArtistName(m metadata) string { +func getArtistName(m metadata.Metadata) string { var artist *string if len(m.Artist) > 0 { @@ -86,7 +98,7 @@ func getArtistName(m metadata) string { return *artist } -func getAlbumName(m metadata) string { +func getAlbumName(m metadata.Metadata) string { album := sanitizeName(m.Album) if album == nil { return "Unknown Album" @@ -94,7 +106,7 @@ func getAlbumName(m metadata) string { return *album } -func getTitle(src string, m metadata) string { +func getTitle(src string, m metadata.Metadata) string { title := sanitizeName(m.Title) if title == nil { return *sanitizeName(strings.TrimSuffix(filepath.Base(src), filepath.Ext(src))) @@ -102,7 +114,7 @@ func getTitle(src string, m metadata) string { return *title } -func pathForFile(src string, m metadata) string { +func pathForFile(src string, m metadata.Metadata) string { ext := strings.ToLower(filepath.Ext(src)) artist := getArtistName(m) @@ -114,6 +126,22 @@ func pathForFile(src string, m metadata) string { return filepath.Join(artist, album, filename) } +func copyFileUpdateMetadata(src, dst string, m metadata.Metadata) error { + ext := strings.ToLower(filepath.Ext(src)) + + if ext != ".flac" { + slog.Warn("Copying file without updating metadata. Unsupported format", "file", src) + return copyFile(src, dst) + } + + dir := filepath.Dir(dst) + if err := os.MkdirAll(dir, 0o755); err != nil { + return fmt.Errorf("create destination directory: %w", err) + } + + return metadata.UpdateMetadata(src, dst, m) +} + func copyFile(src, dst string) error { in, err := os.Open(src) if err != nil {