From 2324a38b4ba4579661eadb8aa93772c4df366647 Mon Sep 17 00:00:00 2001 From: Eric Daugherty Date: Sat, 29 Apr 2023 17:12:27 -0600 Subject: [PATCH] Added handling of playlist folders --- app.go | 5 ++++- export.go | 53 +++++++++++++++++++++++++++++++++++++++++++---------- library.go | 13 +++++++++++++ 3 files changed, 60 insertions(+), 11 deletions(-) diff --git a/app.go b/app.go index acb404b..a011ddc 100644 --- a/app.go +++ b/app.go @@ -36,6 +36,7 @@ Flags: FLAT Copies all the music into the output folder. -musicPath Base path to the music files. This will override the Music Folder path from iTunes. -musicPathOrig When using -musicPath this allows you to override the Music Folder value that is replaced. + -includeFolders Playlists within folders will include the full path in the name. ` UsageErrorMessage = `Unable to parse command line parameters. %v @@ -64,6 +65,7 @@ var ( copyType string musicPath string musicPathOrig string + includeFolders bool exportSettings ExportSettings ) @@ -84,6 +86,7 @@ func main() { flags.StringVar(©Type, "copy", "NONE", "") flags.StringVar(&musicPath, "musicPath", "", "") flags.StringVar(&musicPathOrig, "musicPathOrig", "", "") + flags.BoolVar(&includeFolders, "includeFolders", false, "") err := flags.Parse(os.Args[1:]) if err != nil { @@ -138,7 +141,7 @@ func main() { } libraryPath = filepath.Clean(libraryPath) - fmt.Printf("Include: %v, Exclude %v", includePlaylistNames, excludePlaylistNames) + fmt.Printf("Include: %v, Exclude %v ", includePlaylistNames, excludePlaylistNames) fmt.Println("Loading Library:", libraryPath) library, err := LoadLibrary(libraryPath) diff --git a/export.go b/export.go index 5bc21a7..e888c3a 100644 --- a/export.go +++ b/export.go @@ -9,7 +9,6 @@ import ( "path/filepath" "strings" "time" - "regexp" ) const ( @@ -44,14 +43,22 @@ func ExportPlaylists(exportSettings *ExportSettings, library *Library) error { start := time.Now() for _, playlist := range exportSettings.Playlists { + // Skip Folders + if playlist.Folder { + continue + } fmt.Printf("Exporting Playlist %v\n", playlist.Name) - // Prevent illegal characters in playlist filenames - illegalChars := regexp.MustCompile(`[*?<>|]`) - safePlaylistName := illegalChars.ReplaceAllString(playlist.Name, "_") - safePlaylistName = strings.Replace(safePlaylistName, ":", " - ", -1) + filePath := "" + if includeFolders && playlist.ParentPersistentId != "" { + filePath = buildPlaylistPath(playlist, library) + } + + if filePath != "" { + os.MkdirAll(filepath.Join(exportSettings.OutputPath, filePath), 0777) + } - fileName := filepath.Join(exportSettings.OutputPath, safePlaylistName+"."+exportSettings.Extension) + fileName := filepath.Join(exportSettings.OutputPath, filePath, playlist.SafeName()+"."+exportSettings.Extension) file, err := os.OpenFile(fileName, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0666) if err != nil { @@ -87,7 +94,7 @@ func ExportPlaylists(exportSettings *ExportSettings, library *Library) error { sourceFileLocation, errParse := url.QueryUnescape(track.Location) sourceFileLocation = trimTrackLocationPrefix(sourceFileLocation) - destFileLocation, err := copyTrack(exportSettings, &playlist, &track, sourceFileLocation) + destFileLocation, err := copyTrack(library, exportSettings, &playlist, &track, sourceFileLocation) if err != nil { fmt.Printf("Unable to copy file %v: %v\n", sourceFileLocation, err.Error()) continue @@ -119,11 +126,15 @@ func ExportPlaylists(exportSettings *ExportSettings, library *Library) error { // copyTrack copies a file from the provided sourceFileLocation to another location. The new location // depends on the CopyType selected in exportSettings. If COPY_NONE is selected, the sourceFileLocation is returned. -func copyTrack(exportSettings *ExportSettings, playlist *Playlist, track *Track, sourceFileLocation string) (string, error) { +func copyTrack(library *Library, exportSettings *ExportSettings, playlist *Playlist, track *Track, sourceFileLocation string) (string, error) { var destinationPath string switch exportSettings.CopyType { case COPY_PLAYLIST: - destinationPath = filepath.Join(exportSettings.OutputPath, playlist.Name) + filePath := "" + if includeFolders && playlist.ParentPersistentId != "" { + filePath = buildPlaylistPath(*playlist, library) + } + destinationPath = filepath.Join(exportSettings.OutputPath, filePath, playlist.SafeName()) case COPY_ITUNES: destinationPath = filepath.Join(exportSettings.OutputPath, track.Artist, track.Album) case COPY_FLAT: @@ -145,13 +156,14 @@ func copyTrack(exportSettings *ExportSettings, playlist *Playlist, track *Track, } func copyFile(src, dest string) error { + src = strings.Replace(src, "file://", "", 1) sourceFileInfo, err := os.Stat(src) if err != nil { return err } if !sourceFileInfo.Mode().IsRegular() { - return errors.New("source file is not a regular file.") + return errors.New("source file is not a regular file") } _, err = os.Stat(dest) @@ -196,3 +208,24 @@ func copyFileData(src, dest string) error { } return out.Sync() } + +// buildPlaylistPath checks to see if the playlist has any parent folders. +// If so, it returns the full path of those folders. +func buildPlaylistPath(playlist Playlist, library *Library) string { + if playlist.ParentPersistentId == "" { + if playlist.Folder { + return playlist.SafeName() + } + return "" + } + + parent, ok := library.PlaylistIdMap[playlist.ParentPersistentId] + if !ok { + return "" + } + pathSeg := "" + if playlist.Folder { + pathSeg = playlist.SafeName() + } + return filepath.Join(buildPlaylistPath(parent, library), pathSeg) +} diff --git a/library.go b/library.go index 974c3d2..39dcbcc 100644 --- a/library.go +++ b/library.go @@ -2,12 +2,16 @@ package main import ( "os" + "regexp" "strconv" "time" plist "howett.net/plist" ) +// Prevent illegal characters in playlist filenames +var illegalChars = regexp.MustCompile(`[\[\]\\:/*?<>|]`) + type Library struct { MajorVersion int `plist:"Major Version"` MinorVersion int `plist:"Minor Version"` @@ -20,6 +24,7 @@ type Library struct { Tracks map[string]Track Playlists []Playlist PlaylistMap map[string]Playlist + PlaylistIdMap map[string]Playlist } type Track struct { @@ -76,14 +81,20 @@ type Playlist struct { Master bool PlaylistId int `plist:"Playlist ID"` PlaylistPersistentId string `plist:"Playlist Persistent ID"` + ParentPersistentId string `plist:"Parent Persistent ID"` DistinguishedKind int `plist:"Distinguished Kind"` Visible bool AllItems bool `plist:"All Items"` + Folder bool `plist:"Folder"` SmartInfo []byte `plist:"Smart Info"` SmartCriteria []byte `plist:"Smart Criteria"` PlaylistItems []PlaylistItem `plist:"Playlist Items"` } +func (p Playlist) SafeName() string { + return illegalChars.ReplaceAllString(p.Name, "_") +} + type PlaylistItem struct { TrackId int `plist:"Track ID"` } @@ -107,8 +118,10 @@ func LoadLibrary(fileLocation string) (*Library, error) { } library.PlaylistMap = make(map[string]Playlist) + library.PlaylistIdMap = make(map[string]Playlist) for _, value := range library.Playlists { library.PlaylistMap[value.Name] = value + library.PlaylistIdMap[value.PlaylistPersistentId] = value } return &library, nil