initial commit

This commit is contained in:
2026-04-07 18:58:24 +02:00
commit 6daf2f953f
9 changed files with 532 additions and 0 deletions

65
internal/metadata.go Normal file
View File

@@ -0,0 +1,65 @@
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
}

72
internal/ripsort.go Normal file
View File

@@ -0,0 +1,72 @@
package ripsort
import (
"fmt"
"io/fs"
"log/slog"
"os"
"path/filepath"
)
func Scan(filePath string) {
info, err := 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)
}
func Sort(dst, path string) {
info, err := os.Stat(path)
if err != nil {
slog.Error("Failed to stat path", "file", path, "err", err)
os.Exit(1)
}
if !info.IsDir() {
if err := sortSong(path, dst); err != nil {
slog.Error("Failed to sort single file", "file", path, "err", err)
os.Exit(1)
}
return
}
err = filepath.WalkDir(path, func(p string, d fs.DirEntry, err error) error {
if err != nil {
slog.Error("Failed to walk path", "path", path, "file", p, "err", err)
return err
}
if !d.IsDir() {
if err := sortSong(p, dst); err != nil {
slog.Error("Failed to sort file", "file", p, "err", err)
return err
}
}
return nil
})
if err != nil {
os.Exit(1)
}
}

125
internal/sorter.go Normal file
View File

@@ -0,0 +1,125 @@
package ripsort
import (
"fmt"
"io"
"os"
"path/filepath"
"regexp"
"strings"
)
func sortSong(src, dst string) error {
m, err := ReadAudioTags(src)
if err != nil {
return err
}
dstPath := pathForFile(src, *m)
finalPath := filepath.Join(dst, dstPath)
err = copyFile(src, finalPath)
if err != nil {
return err
}
return nil
}
func sanitizeName(name string) *string {
if name == "" {
return nil
}
re := regexp.MustCompile(`[<>:"/\\|?*\x00-\x1F]`)
name = re.ReplaceAllString(name, "_")
name = strings.Trim(name, " .")
if name == "" {
return nil
}
return &name
}
func getArtistName(m metadata) string {
var artist *string
if len(m.Artist) > 0 {
artist = sanitizeName(m.Artist[0])
}
if artist == nil && len(m.AlbumArtist) > 0 {
if aa := m.AlbumArtist[0]; aa != "" {
artist = sanitizeName(aa)
}
}
if artist == nil {
return "Unknown Artist"
}
return *artist
}
func getAlbumName(m metadata) string {
album := sanitizeName(m.Album)
if album == nil {
return "Unknown Album"
}
return *album
}
func getTitle(src string, m metadata) string {
title := sanitizeName(m.Title)
if title == nil {
return *sanitizeName(strings.TrimSuffix(filepath.Base(src), filepath.Ext(src)))
}
return *title
}
func pathForFile(src string, m metadata) string {
ext := strings.ToLower(filepath.Ext(src))
artist := getArtistName(m)
album := getAlbumName(m)
title := getTitle(src, m)
filename := title + ext
return filepath.Join(artist, album, filename)
}
func copyFile(src, dst string) error {
in, err := os.Open(src)
if err != nil {
return fmt.Errorf("open source file: %w", err)
}
defer in.Close()
dir := filepath.Dir(dst)
if err := os.MkdirAll(dir, 0o755); err != nil {
return fmt.Errorf("create destination directory: %w", err)
}
out, err := os.Create(dst)
if err != nil {
return fmt.Errorf("create destination file: %w", err)
}
defer func() {
if cerr := out.Close(); err == nil && cerr != nil {
err = cerr
}
}()
if _, err = io.Copy(out, in); err != nil {
return fmt.Errorf("copy file contents: %w", err)
}
if err = out.Sync(); err != nil {
return fmt.Errorf("sync destination file: %w", err)
}
return nil
}