package ripsort import ( "errors" "fmt" "io" "log/slog" "os" "path/filepath" "regexp" "strings" "git.kapelle.org/niklas/ripsort/internal/metadata" ) func fileSupported(file string) bool { 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 { return err } dstPath := pathForFile(src, *m) finalPath := filepath.Join(dst, dstPath) 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 { return err } return nil } func sanitizeName(name *string) *string { if name == nil || *name == "" { return nil } re := regexp.MustCompile(`[<>:"/\\|?*\x00-\x1F]`) dName := re.ReplaceAllString(*name, "_") dName = strings.Trim(dName, " .") if dName == "" { return nil } return &dName } func getArtistName(m metadata.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.Metadata) string { album := sanitizeName(m.Album) if album == nil { return "Unknown Album" } return *album } func getTitle(src string, m metadata.Metadata) string { title := sanitizeName(m.Title) if title == nil { filename := strings.TrimSuffix(filepath.Base(src), filepath.Ext(src)) return *sanitizeName(&filename) } return *title } func pathForFile(src string, m metadata.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 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 { 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 } 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 }