Compare commits
13 Commits
187546b750
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
5373a828e3
|
|||
|
cd7c3057b6
|
|||
|
49c32e3f8f
|
|||
|
fa0e2a5103
|
|||
|
1ad9a3196d
|
|||
|
797bf434e6
|
|||
|
e9fbd044d0
|
|||
|
759aa7e08b
|
|||
|
a92b632234
|
|||
|
b4e3f2f372
|
|||
|
2a5ce72ce5
|
|||
|
d9590651b2
|
|||
|
fcba178b26
|
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1 +1,2 @@
|
|||||||
/result
|
/result
|
||||||
|
/outputs
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"log/slog"
|
||||||
|
|
||||||
ripsort "git.kapelle.org/niklas/ripsort/internal"
|
ripsort "git.kapelle.org/niklas/ripsort/internal"
|
||||||
"github.com/alexflint/go-arg"
|
"github.com/alexflint/go-arg"
|
||||||
)
|
)
|
||||||
@@ -15,14 +17,19 @@ type SortCmd struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type args struct {
|
type args struct {
|
||||||
Info *InfoCmd `arg:"subcommand:info"`
|
Info *InfoCmd `arg:"subcommand:info"`
|
||||||
Sort *SortCmd `arg:"subcommand:sort"`
|
Sort *SortCmd `arg:"subcommand:sort"`
|
||||||
|
Verbose bool `arg:"-v" default:"false"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func Run() {
|
func Run() {
|
||||||
var args args
|
var args args
|
||||||
p := arg.MustParse(&args)
|
p := arg.MustParse(&args)
|
||||||
|
|
||||||
|
if args.Verbose {
|
||||||
|
slog.SetLogLoggerLevel(slog.LevelDebug)
|
||||||
|
}
|
||||||
|
|
||||||
switch {
|
switch {
|
||||||
case args.Info != nil:
|
case args.Info != nil:
|
||||||
ripsort.Scan(args.Info.File)
|
ripsort.Scan(args.Info.File)
|
||||||
|
|||||||
@@ -24,7 +24,7 @@
|
|||||||
inherit pname version;
|
inherit pname version;
|
||||||
src = ./.;
|
src = ./.;
|
||||||
|
|
||||||
vendorHash = "sha256-K40zYmELd2eM29CdOScbRcC0NSrsZRsRGPTPgWRAFPQ=";
|
vendorHash = "sha256-1YBmcKszUV+Uyj0X2GZeIj+p3jUfC+jIB9Gy8/KROsA=";
|
||||||
|
|
||||||
meta = with pkgs.lib; {
|
meta = with pkgs.lib; {
|
||||||
description = "Organize music library";
|
description = "Organize music library";
|
||||||
|
|||||||
9
go.mod
9
go.mod
@@ -3,7 +3,10 @@ module git.kapelle.org/niklas/ripsort
|
|||||||
go 1.25.7
|
go 1.25.7
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/alexflint/go-arg v1.6.1 // indirect
|
github.com/alexflint/go-arg v1.6.1
|
||||||
github.com/alexflint/go-scalar v1.2.0 // indirect
|
github.com/dhowden/tag v0.0.0-20240417053706-3d75831295e8
|
||||||
github.com/dhowden/tag v0.0.0-20240417053706-3d75831295e8 // indirect
|
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
|
||||||
|
|||||||
10
go.sum
10
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-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 h1:WR7JPKkeNpnYIOfHRa7ivM21aWAdHD0gEWHCx+WQBRw=
|
||||||
github.com/alexflint/go-scalar v1.2.0/go.mod h1:LoFvNMqS1CPrMVltza4LvnGKhaSpc3oyLEBUZVhhS2o=
|
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/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 h1:OtSeLS5y0Uy01jaKK4mA/WVIYtpzVm63vLVAPzJXigg=
|
||||||
github.com/dhowden/tag v0.0.0-20240417053706-3d75831295e8/go.mod h1:apkPC/CR3s48O2D7Y++n1XWEpgPNNCjXYga3PPbJe2E=
|
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/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.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=
|
||||||
|
|||||||
@@ -1,65 +0,0 @@
|
|||||||
package ripsort
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/dhowden/tag"
|
|
||||||
)
|
|
||||||
|
|
||||||
type metadata struct {
|
|
||||||
Format tag.Format
|
|
||||||
FileType tag.FileType
|
|
||||||
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) {
|
|
||||||
f, err := os.Open(filePath)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to open file: %w", err)
|
|
||||||
}
|
|
||||||
defer f.Close()
|
|
||||||
|
|
||||||
m, err := tag.ReadFrom(f)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to read tags: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
track, totalTracks := m.Track()
|
|
||||||
disc, totalDiscs := m.Disc()
|
|
||||||
|
|
||||||
info := &metadata{
|
|
||||||
Format: m.Format(),
|
|
||||||
FileType: m.FileType(),
|
|
||||||
Title: m.Title(),
|
|
||||||
Artist: strings.Split(m.Artist(), ";"),
|
|
||||||
Album: m.Album(),
|
|
||||||
AlbumArtist: strings.Split(m.AlbumArtist(), ";"),
|
|
||||||
Composer: strings.Split(m.Composer(), ";"),
|
|
||||||
Genre: m.Genre(),
|
|
||||||
Year: m.Year(),
|
|
||||||
Track: track,
|
|
||||||
TotalTracks: totalTracks,
|
|
||||||
Disc: disc,
|
|
||||||
TotalDiscs: totalDiscs,
|
|
||||||
Lyrics: m.Lyrics(),
|
|
||||||
Comment: m.Comment(),
|
|
||||||
HasPicture: m.Picture() != nil,
|
|
||||||
}
|
|
||||||
|
|
||||||
return info, nil
|
|
||||||
}
|
|
||||||
90
internal/metadata/metadata.go
Normal file
90
internal/metadata/metadata.go
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
package metadata
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/dhowden/tag"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Metadata struct {
|
||||||
|
Title *string
|
||||||
|
Artist []string
|
||||||
|
Album *string
|
||||||
|
AlbumArtist []string
|
||||||
|
Track int
|
||||||
|
TotalTracks int
|
||||||
|
Comment *string
|
||||||
|
ISRC *string
|
||||||
|
Date *string
|
||||||
|
}
|
||||||
|
|
||||||
|
func ReadAudioTags(filePath string) (*Metadata, error) {
|
||||||
|
ext := strings.ToLower(filepath.Ext(filePath))
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case ext == ".flac":
|
||||||
|
return readVorbisMetadata(filePath)
|
||||||
|
default:
|
||||||
|
return readGenericAudioTags(filePath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func readGenericAudioTags(filePath string) (*Metadata, error) {
|
||||||
|
f, err := os.Open(filePath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to open file: %w", err)
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
m, err := tag.ReadFrom(f)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to read tags: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
track, totalTracks := m.Track()
|
||||||
|
|
||||||
|
title := m.Title()
|
||||||
|
album := m.Album()
|
||||||
|
comment := m.Comment()
|
||||||
|
|
||||||
|
info := &Metadata{
|
||||||
|
Title: &title,
|
||||||
|
Artist: parseSeperatedTag(m.Artist()),
|
||||||
|
Album: &album,
|
||||||
|
AlbumArtist: parseSeperatedTag(m.AlbumArtist()),
|
||||||
|
Track: track,
|
||||||
|
TotalTracks: totalTracks,
|
||||||
|
Comment: &comment,
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
}
|
||||||
187
internal/metadata/vobis.go
Normal file
187
internal/metadata/vobis.go
Normal file
@@ -0,0 +1,187 @@
|
|||||||
|
package metadata
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log/slog"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/go-flac/flacvorbis/v2"
|
||||||
|
"github.com/go-flac/go-flac/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
func readVorbisMetadata(file string) (*Metadata, error) {
|
||||||
|
slog.Debug("Reading VORBIS metadata", "file", file)
|
||||||
|
|
||||||
|
f, err := os.Open(file)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
flacFile, err := flac.ParseMetadata(f)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
cmt, err := findVobisMetadata(*flacFile)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if cmt == nil {
|
||||||
|
slog.Debug("Metadata empty", "file", file)
|
||||||
|
return &Metadata{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
titles, err := cmt.Get(flacvorbis.FIELD_TITLE)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var title *string
|
||||||
|
if len(titles) > 0 {
|
||||||
|
title = &titles[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
albums, err := cmt.Get(flacvorbis.FIELD_ALBUM)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var album *string
|
||||||
|
if len(albums) > 0 {
|
||||||
|
album = &albums[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
artists, err := cmt.Get(flacvorbis.FIELD_ARTIST)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(artists) == 1 {
|
||||||
|
artists = parseSeperatedTag(artists[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
albumArtists, err := cmt.Get("ALBUMARTIST")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(albumArtists) == 1 {
|
||||||
|
albumArtists = parseSeperatedTag(albumArtists[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
comments, err := cmt.Get("COMMENT")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var comment *string
|
||||||
|
if len(comments) > 0 {
|
||||||
|
comment = &comments[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
isrcs, err := cmt.Get("ISRC")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var isrc *string
|
||||||
|
if len(isrcs) > 0 {
|
||||||
|
isrc = &isrcs[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
dates, err := cmt.Get("DATE")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var date *string
|
||||||
|
if len(dates) > 0 {
|
||||||
|
date = &dates[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
metadata := &Metadata{
|
||||||
|
Title: title,
|
||||||
|
Artist: artists,
|
||||||
|
Album: album,
|
||||||
|
AlbumArtist: albumArtists,
|
||||||
|
Comment: comment,
|
||||||
|
Date: date,
|
||||||
|
ISRC: isrc,
|
||||||
|
}
|
||||||
|
|
||||||
|
return metadata, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func findVobisMetadata(f flac.File) (*flacvorbis.MetaDataBlockVorbisComment, error) {
|
||||||
|
for _, meta := range f.Meta {
|
||||||
|
if meta.Type == flac.VorbisComment {
|
||||||
|
return flacvorbis.ParseFromMetaDataBlock(*meta)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
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()
|
||||||
|
|
||||||
|
if m.Title != nil {
|
||||||
|
vorbisMeta.Add(flacvorbis.FIELD_TITLE, *m.Title)
|
||||||
|
}
|
||||||
|
|
||||||
|
if m.Album != nil {
|
||||||
|
vorbisMeta.Add(flacvorbis.FIELD_ALBUM, *m.Album)
|
||||||
|
}
|
||||||
|
|
||||||
|
if m.Comment != nil {
|
||||||
|
vorbisMeta.Add("COMMENT", *m.Comment)
|
||||||
|
}
|
||||||
|
|
||||||
|
if m.Track != 0 {
|
||||||
|
vorbisMeta.Add("TRACKNUMBER", strconv.Itoa(m.Track))
|
||||||
|
}
|
||||||
|
|
||||||
|
if m.TotalTracks != 0 {
|
||||||
|
vorbisMeta.Add("TOTALTRACKS", strconv.Itoa(m.TotalTracks))
|
||||||
|
}
|
||||||
|
|
||||||
|
if m.ISRC != nil {
|
||||||
|
vorbisMeta.Add("ISRC", *m.ISRC)
|
||||||
|
}
|
||||||
|
|
||||||
|
if m.Date != nil {
|
||||||
|
vorbisMeta.Add("DATE", *m.Date)
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
@@ -6,30 +6,24 @@ import (
|
|||||||
"log/slog"
|
"log/slog"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
|
"git.kapelle.org/niklas/ripsort/internal/metadata"
|
||||||
)
|
)
|
||||||
|
|
||||||
func Scan(filePath string) {
|
func Scan(filePath string) {
|
||||||
info, err := ReadAudioTags(filePath)
|
info, err := metadata.ReadAudioTags(filePath)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Error("Reading metadata", "file", filePath, "err", err)
|
slog.Error("Reading metadata", "file", filePath, "err", err)
|
||||||
os.Exit(1)
|
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("Title: %s\n", info.Title)
|
||||||
fmt.Printf("Artist: %s\n", info.Artist)
|
fmt.Printf("Artist: %s\n", info.Artist)
|
||||||
fmt.Printf("Album: %s\n", info.Album)
|
fmt.Printf("Album: %s\n", info.Album)
|
||||||
fmt.Printf("Album Artist: %s\n", info.AlbumArtist)
|
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("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("Comment: %s\n", info.Comment)
|
||||||
fmt.Printf("Has Picture: %v\n", info.HasPicture)
|
|
||||||
|
|
||||||
sortPath := pathForFile(filePath, *info)
|
sortPath := pathForFile(filePath, *info)
|
||||||
fmt.Printf("Sort path: %s\n", sortPath)
|
fmt.Printf("Sort path: %s\n", sortPath)
|
||||||
@@ -42,14 +36,20 @@ func Sort(dst, path string) {
|
|||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle single file
|
||||||
if !info.IsDir() {
|
if !info.IsDir() {
|
||||||
if err := sortSong(path, dst); err != nil {
|
if !fileSupported(path) {
|
||||||
|
slog.Error("Unsupported file format", "file", path)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
if err := sortSong(path, dst, true); err != nil {
|
||||||
slog.Error("Failed to sort single file", "file", path, "err", err)
|
slog.Error("Failed to sort single file", "file", path, "err", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle directory
|
||||||
err = filepath.WalkDir(path, func(p string, d fs.DirEntry, err error) error {
|
err = filepath.WalkDir(path, func(p string, d fs.DirEntry, err error) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Error("Failed to walk path", "path", path, "file", p, "err", err)
|
slog.Error("Failed to walk path", "path", path, "file", p, "err", err)
|
||||||
@@ -57,7 +57,10 @@ func Sort(dst, path string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !d.IsDir() {
|
if !d.IsDir() {
|
||||||
if err := sortSong(p, dst); err != nil {
|
if !fileSupported(p) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if err := sortSong(p, dst, true); err != nil {
|
||||||
slog.Error("Failed to sort file", "file", p, "err", err)
|
slog.Error("Failed to sort file", "file", p, "err", err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,29 @@
|
|||||||
package ripsort
|
package ripsort
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"log/slog"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"git.kapelle.org/niklas/ripsort/internal/metadata"
|
||||||
)
|
)
|
||||||
|
|
||||||
func sortSong(src, dst string) error {
|
func fileSupported(file string) bool {
|
||||||
m, err := ReadAudioTags(src)
|
supported := map[string]bool{
|
||||||
|
".mp3": true, ".flac": true,
|
||||||
|
".m4a": true, ".ogg": true, ".dsf": true,
|
||||||
|
}
|
||||||
|
|
||||||
|
return supported[strings.ToLower(filepath.Ext(file))]
|
||||||
|
}
|
||||||
|
|
||||||
|
func sortSong(src, dst string, updateMeta bool) error {
|
||||||
|
m, err := metadata.ReadAudioTags(src)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -18,7 +31,43 @@ func sortSong(src, dst string) error {
|
|||||||
dstPath := pathForFile(src, *m)
|
dstPath := pathForFile(src, *m)
|
||||||
finalPath := filepath.Join(dst, dstPath)
|
finalPath := filepath.Join(dst, dstPath)
|
||||||
|
|
||||||
err = copyFile(src, finalPath)
|
exists, err := fileExists(finalPath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if exists {
|
||||||
|
slog.Info("File already exists", "file", src, "dst", dst)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
lrcFile := checkForLrcFile(src)
|
||||||
|
if lrcFile != nil {
|
||||||
|
extraFilePath := pathForFile(*lrcFile, *m)
|
||||||
|
finalExtraFilePath := filepath.Join(dst, extraFilePath)
|
||||||
|
slog.Info("Copy lrc file", "file", lrcFile, "dst", extraFilePath)
|
||||||
|
err = copyFile(*lrcFile, finalExtraFilePath)
|
||||||
|
if err != nil {
|
||||||
|
slog.Warn("Failed to copy lrc file", "file", lrcFile, "err", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
err = copyAlbumCover(src, finalPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -26,33 +75,33 @@ func sortSong(src, dst string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func sanitizeName(name string) *string {
|
func sanitizeName(name *string) *string {
|
||||||
if name == "" {
|
if name == nil || *name == "" {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
re := regexp.MustCompile(`[<>:"/\\|?*\x00-\x1F]`)
|
re := regexp.MustCompile(`[<>:"/\\|?*\x00-\x1F]`)
|
||||||
name = re.ReplaceAllString(name, "_")
|
dName := re.ReplaceAllString(*name, "_")
|
||||||
|
|
||||||
name = strings.Trim(name, " .")
|
dName = strings.Trim(dName, " .")
|
||||||
|
|
||||||
if name == "" {
|
if dName == "" {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return &name
|
return &dName
|
||||||
}
|
}
|
||||||
|
|
||||||
func getArtistName(m metadata) string {
|
func getArtistName(m metadata.Metadata) string {
|
||||||
var artist *string
|
var artist *string
|
||||||
|
|
||||||
if len(m.Artist) > 0 {
|
if len(m.Artist) > 0 {
|
||||||
artist = sanitizeName(m.Artist[0])
|
artist = sanitizeName(&m.Artist[0])
|
||||||
}
|
}
|
||||||
|
|
||||||
if artist == nil && len(m.AlbumArtist) > 0 {
|
if artist == nil && len(m.AlbumArtist) > 0 {
|
||||||
if aa := m.AlbumArtist[0]; aa != "" {
|
if aa := m.AlbumArtist[0]; aa != "" {
|
||||||
artist = sanitizeName(aa)
|
artist = sanitizeName(&aa)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -63,7 +112,7 @@ func getArtistName(m metadata) string {
|
|||||||
return *artist
|
return *artist
|
||||||
}
|
}
|
||||||
|
|
||||||
func getAlbumName(m metadata) string {
|
func getAlbumName(m metadata.Metadata) string {
|
||||||
album := sanitizeName(m.Album)
|
album := sanitizeName(m.Album)
|
||||||
if album == nil {
|
if album == nil {
|
||||||
return "Unknown Album"
|
return "Unknown Album"
|
||||||
@@ -71,15 +120,16 @@ func getAlbumName(m metadata) string {
|
|||||||
return *album
|
return *album
|
||||||
}
|
}
|
||||||
|
|
||||||
func getTitle(src string, m metadata) string {
|
func getTitle(src string, m metadata.Metadata) string {
|
||||||
title := sanitizeName(m.Title)
|
title := sanitizeName(m.Title)
|
||||||
if title == nil {
|
if title == nil {
|
||||||
return *sanitizeName(strings.TrimSuffix(filepath.Base(src), filepath.Ext(src)))
|
filename := strings.TrimSuffix(filepath.Base(src), filepath.Ext(src))
|
||||||
|
return *sanitizeName(&filename)
|
||||||
}
|
}
|
||||||
return *title
|
return *title
|
||||||
}
|
}
|
||||||
|
|
||||||
func pathForFile(src string, m metadata) string {
|
func pathForFile(src string, m metadata.Metadata) string {
|
||||||
ext := strings.ToLower(filepath.Ext(src))
|
ext := strings.ToLower(filepath.Ext(src))
|
||||||
|
|
||||||
artist := getArtistName(m)
|
artist := getArtistName(m)
|
||||||
@@ -91,6 +141,22 @@ func pathForFile(src string, m metadata) string {
|
|||||||
return filepath.Join(artist, album, filename)
|
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 {
|
func copyFile(src, dst string) error {
|
||||||
in, err := os.Open(src)
|
in, err := os.Open(src)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -123,3 +189,69 @@ func copyFile(src, dst string) error {
|
|||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func checkForLrcFile(originalPath string) *string {
|
||||||
|
dir := filepath.Dir(originalPath)
|
||||||
|
fileName := filepath.Base(originalPath)
|
||||||
|
fileNameNoExt := strings.TrimSuffix(fileName, filepath.Ext(fileName))
|
||||||
|
|
||||||
|
lrcPath := filepath.Join(dir, fileNameNoExt+".lrc")
|
||||||
|
stat, err := os.Stat(lrcPath)
|
||||||
|
if err == nil && !stat.IsDir() {
|
||||||
|
return &lrcPath
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func fileExists(file string) (bool, error) {
|
||||||
|
_, err := os.Stat(file)
|
||||||
|
|
||||||
|
if errors.Is(err, os.ErrNotExist) {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func copyAlbumCover(originalPath, finalPath string) error {
|
||||||
|
const coverFilename string = "cover.jpg"
|
||||||
|
finalAlbumDir := filepath.Dir(finalPath)
|
||||||
|
finalAlbumCoverPath := filepath.Join(finalAlbumDir, coverFilename)
|
||||||
|
|
||||||
|
alreadyExists, err := fileExists(finalAlbumCoverPath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if alreadyExists {
|
||||||
|
slog.Debug("Cover already exists", "file", finalAlbumCoverPath)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
originalDir := filepath.Dir(originalPath)
|
||||||
|
originalFilename := filepath.Base(originalPath)
|
||||||
|
originalFilenameNoExt := strings.TrimSuffix(originalFilename, filepath.Ext(originalFilename))
|
||||||
|
originalAlumCover := filepath.Join(originalDir, originalFilenameNoExt+".jpg")
|
||||||
|
|
||||||
|
exists, err := fileExists(originalAlumCover)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !exists {
|
||||||
|
slog.Debug("No cover found", "file", originalPath)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
err = copyFile(originalAlumCover, finalAlbumCoverPath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user