From a986efc7f14e0dcbdb8b11cd59b83426333d3399 Mon Sep 17 00:00:00 2001 From: zomo Date: Sat, 24 May 2025 20:53:22 -0500 Subject: [PATCH] v1.0.0 --- .env.example | 4 + .gitignore | 1 + go.mod | 8 ++ go.sum | 4 + main.go | 329 +++++++++++++++++++++++++++++++++++++++++++++++++++ readme.md | 5 + 6 files changed, 351 insertions(+) create mode 100644 .env.example create mode 100644 .gitignore create mode 100644 go.mod create mode 100644 go.sum create mode 100644 main.go create mode 100644 readme.md diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..3f0a720 --- /dev/null +++ b/.env.example @@ -0,0 +1,4 @@ +LIBRARY_ROOT= # root directory for library +LIBRARY_FILETEMPLATE_PATH= # go text/template string that represents the path in the library +LIBRARY_FILETEMPLATE_FILE= # go text/template string that represents the file name in the library path +LIBRARY_NOTAGS_ROOT= # path to put files without tags diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4c49bd7 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.env diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..7df6917 --- /dev/null +++ b/go.mod @@ -0,0 +1,8 @@ +module zomo.land/slskd-rename + +go 1.21.1 + +require ( + github.com/dhowden/tag v0.0.0-20240417053706-3d75831295e8 // indirect + github.com/joho/godotenv v1.5.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..315dc31 --- /dev/null +++ b/go.sum @@ -0,0 +1,4 @@ +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/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= diff --git a/main.go b/main.go new file mode 100644 index 0000000..efe8383 --- /dev/null +++ b/main.go @@ -0,0 +1,329 @@ +package main + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "log" + "os" + "path" + "strings" + "text/template" + + "slices" + + "github.com/dhowden/tag" + "github.com/joho/godotenv" + // _ "github.com/joho/godotenv/autoload" +) + +type DownloadDirectoryComplete struct { + Version int `json:"version"` + LocalDirectoryName string `json:"localDirectoryName"` + RemoteDirectoryName string `json:"remoteDirectoryName"` + Username string `json:"username"` +} + +func (d *DownloadDirectoryComplete) WarnVersion() { + supportedVersions := []int{ 0 } + if !slices.Contains(supportedVersions, d.Version) { + supportedVersionsStr := strings.Trim(strings.Join(strings.Fields(fmt.Sprint(supportedVersions)), ","), "[]") + log.Println("WARNING: Given data version not supported.") + log.Printf( "WARNING: Supported versions: %s\n", supportedVersionsStr) + log.Printf( "WARNING: Given version: %d\n", d.Version) + log.Println("WARNING: Continuing, but there may be demons") + } +} + +func main() { + err := mainErr() + if err != nil { + log.Fatal("ERROR: ", err) + } +} + +func mainErr() error { + if len(os.Args) < 2 { + return errors.New("missing data argument") + } + + if err := godotenv.Load(); err != nil { + return err + } + + if err := testEnv(); err != nil { + return err + } + + // parse $DATA + log.Println("Parsing slskd data") + dataArg := os.Args[1] + var data DownloadDirectoryComplete + if err := json.Unmarshal([]byte(dataArg), &data); err != nil { + return err + } + + data.WarnVersion() + + files, err := os.ReadDir(data.LocalDirectoryName) + if err != nil { + return err + } + if len(files) == 0 { + return errors.New("no files found") + } + + // read tags + tags, _ := getTagsInDir(data.LocalDirectoryName) + + // move files + if tags != nil { + log.Println("found tags, moving based on tags") + err := doMoveTags(data.LocalDirectoryName, tags) + if err != nil { + return err + } + } else { + log.Println("no tags found, moving to notags folder") + err := doMoveNotags(data.LocalDirectoryName) + if err != nil { + return err + } + } + + return nil +} + +func testEnv() error { + _, ok := os.LookupEnv("LIBRARY_ROOT") + if !ok { + return errors.New("missing LIBRARY_ROOT") + } + + _, ok = os.LookupEnv("LIBRARY_FILETEMPLATE_PATH") + if !ok { + return errors.New("missing LIBRARY_FILETEMPLATE_PATH") + } + + _, ok = os.LookupEnv("LIBRARY_FILETEMPLATE_FILE") + if !ok { + return errors.New("missing LIBRARY_FILETEMPLATE_FILE") + } + + _, ok = os.LookupEnv("LIBRARY_NOTAGS_ROOT") + if !ok { + return errors.New("missing LIBRARY_NOTAGS_ROOT") + } + + return nil +} + +func getTagsInDir(directory string) (tag.Metadata, error) { + files, err := os.ReadDir(directory) + if err != nil { + return nil, err + } + + log.Printf("DIRECTORY: %s\n", directory) + + for _, file := range files { + fname := path.Join(directory, file.Name()) + log.Printf(" FILE: %s\n", fname) + + if file.IsDir() { + log.Print(" IS DIRECTORY\n") + continue + } + + file, err := os.Open(fname) + if err != nil { + log.Print(" CANT OPEN\n") + continue + } + defer file.Close() + + tags, err := tag.ReadFrom(file) + if err != nil { + log.Print(" NO TAGS\n") + continue + } + + // found tag data + return tags, nil + } + + // no tag data in any file + return nil, errors.New("missing tag data") +} + +func doMoveNotags(directory string) error { + newRoot := os.Getenv("LIBRARY_NOTAGS_ROOT") + _, folderName := path.Split(directory) + toDirectory := path.Join(newRoot, folderName) + + return rename(directory, toDirectory) +} + +func doMoveTags(directory string, albumTags tag.Metadata) error { + // move all tagged files + files, err := os.ReadDir(directory) + if err != nil { + return err + } + + for _, file := range files { + if file.IsDir() { + continue + } + + fname := path.Join(directory, file.Name()) + file, err := os.Open(fname) + if err != nil { + continue + } + defer file.Close() + + tags, err := tag.ReadFrom(file) + if err != nil { + continue + } + + err = doMoveTagged(fname, tags) + if err != nil { + return err + } + } + + // move the rest + files, err = os.ReadDir(directory) + if err != nil { + return err + } + + for _, file := range files { + fname := path.Join(directory, file.Name()) + + err = doMoveTaggedPathonly(fname, albumTags) + if err != nil { + return err + } + } + + // delete folder + return os.Remove(directory) +} + +type LibTemplatePath struct { + Album string + AlbumArtist string + Genre string + Year int +} + +type LibTemplateFile struct { + Title string + Album string + Artist string + AlbumArtist string + Composer string + Genre string + Year int + Track int + TrackCount int + Disc int + DiscCount int +} + +func doMoveTagged(filePath string, fileTags tag.Metadata) error { + libRoot := os.Getenv("LIBRARY_ROOT") + libTemplatePath := os.Getenv("LIBRARY_FILETEMPLATE_PATH") + libTemplateFile := os.Getenv("LIBRARY_FILETEMPLATE_FILE") + fileExt := path.Ext(filePath) + + track, trackCount := fileTags.Track() + disc, discCount := fileTags.Disc() + + tmplPath := template.Must(template.New("libPath").Parse(libTemplatePath)) + tmplFile := template.Must(template.New("libFile").Parse(libTemplateFile)) + + tmplDataFile := LibTemplateFile{ + Title: fileTags.Title(), + Album: fileTags.Album(), + Artist: fileTags.Artist(), + AlbumArtist: fileTags.AlbumArtist(), + Composer: fileTags.Composer(), + Genre: fileTags.Genre(), + Year: fileTags.Year(), + Track: track, + TrackCount: trackCount, + Disc: disc, + DiscCount: discCount, + } + + tmplDataPath := LibTemplatePath{ + Album: fileTags.Album(), + AlbumArtist: fileTags.AlbumArtist(), + Genre: fileTags.Genre(), + Year: fileTags.Year(), + } + + bufPath := bytes.NewBufferString("") + err := tmplPath.Execute(bufPath, tmplDataPath) + if err != nil { + return err + } + + bufFile := bytes.NewBufferString("") + err = tmplFile.Execute(bufFile, tmplDataFile) + if err != nil { + return err + } + + libFile := path.Join(libRoot, bufPath.String(), bufFile.String() + fileExt) + + return rename(filePath, libFile) +} + +func doMoveTaggedPathonly(filePath string, albumTags tag.Metadata) error { + libRoot := os.Getenv("LIBRARY_ROOT") + libTemplatePath := os.Getenv("LIBRARY_FILETEMPLATE_PATH") + _, fileName := path.Split(filePath) + + tmplPath := template.Must(template.New("libPath").Parse(libTemplatePath)) + + tmplData := LibTemplatePath{ + Album: albumTags.Album(), + AlbumArtist: albumTags.AlbumArtist(), + Genre: albumTags.Genre(), + Year: albumTags.Year(), + } + + bufPath := bytes.NewBufferString("") + err := tmplPath.Execute(bufPath, tmplData) + if err != nil { + return err + } + + libFile := path.Join(libRoot, bufPath.String(), fileName) + + return rename(filePath, libFile) +} + +func ensureDirectoryPath(filePath string) error { + directoryPath, _ := path.Split(filePath) + return os.MkdirAll(directoryPath, 0777) +} + +func rename(from, to string) error { + if _, err := os.Stat(to); err == nil { + return fmt.Errorf("file already exists: %s", to) + } + + if err := ensureDirectoryPath(to); err != nil { + return err + } + + fmt.Printf("renaming \"%s\" => \"%s\"", from, to) + return os.Rename(from, to) +} diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..af812ce --- /dev/null +++ b/readme.md @@ -0,0 +1,5 @@ +# music rename + +move music files in a directory to an organized folder based on tags + +for now, this only supports slskd's $DATA arg as input