├── .gitignore ├── LICENSE ├── README.md ├── main.go ├── repair.go ├── spotify.go └── utils.go /.gitignore: -------------------------------------------------------------------------------- 1 | #### joe made this: http://goel.io/joe 2 | 3 | #### go #### 4 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 5 | *.o 6 | *.a 7 | *.so 8 | 9 | # Folders 10 | _obj 11 | _test 12 | 13 | # Architecture specific extensions/prefixes 14 | *.[568vq] 15 | [568vq].out 16 | 17 | *.cgo1.go 18 | *.cgo2.c 19 | _cgo_defun.c 20 | _cgo_gotypes.go 21 | _cgo_export.* 22 | 23 | _testmain.go 24 | 25 | *.exe 26 | *.test 27 | *.prof 28 | 29 | # Output of the go coverage tool, specifically when used with LiteIDE 30 | *.out 31 | 32 | # External packages folder 33 | vendor/ 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Lakshay Kalbhor 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | Adds Metadata to Music files 3 |

4 | 5 |

6 | 7 | License 8 | 9 | 10 | stars 11 | 12 |

13 | 14 | ## Features 15 | 16 | 1. Fixes songs in nested directories recursively. 17 | 2. Fetches metadata from [Spotify](https://www.spotify.com) 18 | 3. Multiple options to format file (Options to revert file back) 19 | 4. Simple binary 20 | 21 | ## Dependencies 22 | 23 | ### [Spotify API](https://developer.spotify.com/my-applications) 24 | 25 | 1. Create an account and register an application. 26 | 2. Copy the Client ID and Client Secret. 27 | 3. Set them in *config file* after running ```musicrepair -config``` 28 | 29 | ###### *config file* will be created after running `musicrepair -config`, and located at `$HOME/.musicrepair/config.json` 30 | 31 | ### Set them using ```-config``` 32 | ```sh 33 | $ musicrepair -config 34 | Enter Spotify client ID : 35 | Enter Spotify client secret : 36 | ``` 37 | 38 | ## Installing 39 | 40 | ### Via Binary 41 | 42 | Download the latest binary from the [releases page](https://github.com/kalbhor/MusicRepair/releases). 43 | 44 | Make sure to add the binary to your `$PATH` 45 | 46 | ### Via Go 47 | ```sh 48 | $ go get -u -v github.com/kalbhor/musicrepair 49 | $ which musicrepair 50 | $ $GOPATH/bin/musicrepair 51 | ``` 52 | 53 | ## Usage 54 | 55 | Initially, you'll have to add the Spotify credentials. 56 | ```sh 57 | $ musicrepair -config 58 | ``` 59 | 60 | After that, always a simple command 61 | ```sh 62 | $ musicrepair 63 | ✨ 🍰 64 | ``` 65 | 66 | 67 | ### Options 68 | ``` 69 | $ musicrepair -help 70 | 71 | Usage of musicrepair: 72 | -config 73 | If set, MusicRepair will ask for credentials 74 | -dir string 75 | Specifies the directory where the music files are located (default "./") 76 | -recursive 77 | If set, Musicrepair will run recursively in the given directory 78 | -revert 79 | If set, Musicrepair will revert the files 80 | -threads int 81 | Specify the number of threads to use (default 1) 82 | ``` 83 | 84 | ## Discussions/Write-Ups 85 |

86 | 87 | 88 | 89 |     90 | 91 | 92 | 93 |     94 | 95 | 96 | 97 |     98 |

99 | 100 | 101 | ## Contribute 102 | 103 | Found an issue? Post it in the [issue tracker](https://github.com/kalbhor/MusicRepair/issues). 104 | 105 | Want to add another awesome feature? [Fork](https://github.com/kalbhor/MusicRepair/fork) this repository and add your feature, then send a pull request. 106 | 107 | ## License 108 | The MIT License (MIT) 109 | Copyright (c) 2017 Lakshay Kalbhor 110 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "log" 7 | "os" 8 | ) 9 | 10 | var ( 11 | root = flag.String("dir", "./", "Specifies the directory where the music files are located") 12 | isRecursive = flag.Bool("recursive", false, "If set, Musicrepair will run recursively in the given directory") 13 | setConfig = flag.Bool("config", false, "If set, MusicRepair will ask for credentials") 14 | isRevert = flag.Bool("revert", false, "If set, Musicrepair will revert the files") 15 | threads = flag.Int("threads", 1, "Specify the number of threads to use") 16 | ) 17 | 18 | func main() { 19 | flag.Parse() 20 | 21 | if *setConfig { 22 | SetConfig() 23 | fmt.Println("Your config has been saved.") 24 | os.Exit(1) 25 | } 26 | 27 | config, err := LoadConfig() 28 | if err != nil { 29 | log.Fatal(err) 30 | } 31 | 32 | client, err := SpotifyAuth(config.ID, config.Secret) 33 | if err != nil { 34 | log.Fatal("Invalid spotify credentials. Error : %v", err) 35 | } 36 | 37 | fileList := WalkDir(*root) // List of all files to work on 38 | 39 | jobs := make(chan string) 40 | results := make(chan string) 41 | 42 | for w := 1; w <= *threads; w++ { 43 | if *isRevert { 44 | go RevertWorker(jobs, results) 45 | } else { 46 | go RepairWorker(client, jobs, results) 47 | } 48 | } 49 | 50 | for _, job := range fileList { 51 | jobs <- job 52 | } 53 | close(jobs) 54 | 55 | for r := 1; r <= len(fileList); r++ { 56 | fmt.Printf("[%v] %v\n", r, <-results) 57 | } 58 | 59 | } 60 | -------------------------------------------------------------------------------- /repair.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "path/filepath" 6 | "strconv" 7 | "strings" 8 | 9 | "github.com/bogem/id3v2" 10 | "github.com/headzoo/surf/errors" 11 | "github.com/zmb3/spotify" 12 | ) 13 | 14 | 15 | func RepairWorker(client spotify.Client, job <-chan string, results chan<- string) { 16 | for filePath := range job { 17 | _, fileName := filepath.Split(filePath) 18 | results <- fmt.Sprintf("Fixing : %v\n", fileName) 19 | if err := Repair(client, filePath); err != nil { 20 | results <- fmt.Sprintf("Error : %v\n", err) 21 | } 22 | } 23 | } 24 | 25 | func RevertWorker(job <-chan string, results chan<- string) { 26 | for filePath := range job { 27 | _, fileName := filepath.Split(filePath) 28 | results <- fmt.Sprintf("Reverting : %v\n", fileName) 29 | if err := Revert(filePath); err != nil { 30 | results <- fmt.Sprintf("Error : %v\n", err) 31 | } 32 | } 33 | } 34 | 35 | 36 | func Revert(path string) error { 37 | tag, err := id3v2.Open(path, id3v2.Options{Parse: true}) 38 | if err != nil { 39 | return err 40 | } 41 | defer tag.Close() 42 | 43 | tag.DeleteAllFrames() 44 | if err = tag.Save(); err != nil { 45 | return err 46 | } 47 | 48 | return nil 49 | } 50 | 51 | func Repair(client spotify.Client, path string) error { 52 | tag, err := id3v2.Open(path, id3v2.Options{Parse: true}) 53 | if err != nil { 54 | return err 55 | } 56 | 57 | if CheckFrames(tag.AllFrames()) { 58 | return errors.New("Already contains tags") 59 | } 60 | 61 | _, filename := filepath.Split(path) 62 | metadata, err := GetMetadata(client, filename[0:len(filename)-4]) 63 | if err != nil { 64 | return err 65 | } 66 | 67 | tag.SetTitle(metadata.Title) 68 | tag.SetAlbum(metadata.Album) 69 | tag.SetArtist(strings.Join(metadata.Artists, ",")) 70 | 71 | TrackNumber := strconv.Itoa(metadata.TrackNumber) 72 | tag.AddFrame("TRCK", id3v2.TextFrame{id3v2.EncodingUTF8, TrackNumber}) 73 | 74 | DiscNumber := strconv.Itoa(metadata.DiscNumber) 75 | tag.AddFrame("TPOS", id3v2.TextFrame{id3v2.EncodingUTF8, DiscNumber}) 76 | 77 | pic := id3v2.PictureFrame{ 78 | Encoding: id3v2.EncodingUTF8, 79 | MimeType: "image/jpeg", 80 | PictureType: id3v2.PTFrontCover, 81 | Description: "Front cover", 82 | Picture: metadata.Image, 83 | } 84 | 85 | tag.AddAttachedPicture(pic) 86 | if err = tag.Save(); err != nil { 87 | return err 88 | } 89 | 90 | return nil 91 | } 92 | -------------------------------------------------------------------------------- /spotify.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "io/ioutil" 7 | "net/http" 8 | 9 | "golang.org/x/oauth2" 10 | "golang.org/x/oauth2/clientcredentials" 11 | 12 | "github.com/zmb3/spotify" 13 | ) 14 | 15 | //Metadata : Structure for one track's metadata 16 | type Metadata struct { 17 | Title string 18 | Artists []string 19 | Album string 20 | Image []byte 21 | DiscNumber int 22 | TrackNumber int 23 | } 24 | 25 | //Load : Sets values from search results 26 | func (m *Metadata) Load(track spotify.FullTrack) error { 27 | m.Title = track.SimpleTrack.Name 28 | m.Album = track.Album.Name 29 | m.DiscNumber = track.SimpleTrack.DiscNumber 30 | m.TrackNumber = track.SimpleTrack.TrackNumber 31 | 32 | for _, artist := range track.SimpleTrack.Artists { 33 | m.Artists = append(m.Artists, artist.Name) 34 | } 35 | 36 | imageURL := track.Album.Images[0].URL 37 | resp, err := http.Get(imageURL) 38 | if err != nil { 39 | return err 40 | } 41 | defer resp.Body.Close() 42 | 43 | b, err := ioutil.ReadAll(resp.Body) 44 | if err != nil { 45 | return err 46 | } 47 | m.Image = b 48 | 49 | return nil 50 | } 51 | 52 | //GetMetadata : Searches spotify and returns a loaded metadata struct 53 | func GetMetadata(client spotify.Client, query string) (*Metadata, error) { 54 | 55 | m := new(Metadata) 56 | 57 | results, err := client.Search(query, spotify.SearchTypeTrack) 58 | if err != nil { 59 | return nil, err 60 | } else if len(results.Tracks.Tracks) == 0 { // Search results were empty 61 | return nil, errors.New("Couldn't fetch metadata") 62 | } 63 | 64 | err = m.Load(results.Tracks.Tracks[0]) // Pass in the top result 65 | if err != nil { 66 | return m, err 67 | } 68 | return m, nil 69 | 70 | } 71 | 72 | //Auth : Returns a usable spotify "client" that can request spotify content 73 | func SpotifyAuth(Id, Secret string) (spotify.Client, error) { 74 | config := &clientcredentials.Config{ 75 | ClientID: Id, 76 | ClientSecret: Secret, 77 | TokenURL: spotify.TokenURL, 78 | } 79 | token, err := config.Token(context.Background()) 80 | if err != nil { 81 | return spotify.Authenticator{}.NewClient(&oauth2.Token{}), err 82 | } 83 | 84 | client := spotify.Authenticator{}.NewClient(token) 85 | 86 | return client, nil 87 | } 88 | -------------------------------------------------------------------------------- /utils.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io/ioutil" 7 | "os" 8 | "path" 9 | "path/filepath" 10 | 11 | "github.com/bogem/id3v2" 12 | ) 13 | 14 | type Config struct { 15 | ID string 16 | Secret string 17 | } 18 | 19 | var configFolder string = path.Join(os.Getenv("HOME"), ".musicrepair") 20 | var configPath string = path.Join(configFolder, "config.json") 21 | 22 | func LoadConfig() (*Config, error) { 23 | file, err := os.Open(configPath) 24 | if err != nil { 25 | return nil, err 26 | } 27 | decoder := json.NewDecoder(file) 28 | config := new(Config) 29 | err = decoder.Decode(&config) 30 | if err != nil { 31 | return nil, err 32 | } 33 | return config, nil 34 | } 35 | 36 | func SetConfig() error { 37 | var id, secret string 38 | fmt.Print("Enter Spotify ID : ") 39 | fmt.Scanln(&id) 40 | fmt.Print("Enter Spotify Secret : ") 41 | fmt.Scanln(&secret) 42 | 43 | config := Config{id, secret} 44 | b, err := json.Marshal(config) 45 | if err != nil { 46 | return err 47 | } 48 | if err := os.Mkdir(configFolder, os.ModePerm); err != nil { 49 | return err 50 | } 51 | if err := ioutil.WriteFile(configPath, b, os.ModePerm); err != nil { 52 | return err 53 | } 54 | 55 | return nil 56 | } 57 | 58 | func WalkDir(root string) (fileList []string) { 59 | 60 | filepath.Walk(root, func(path string, fi os.FileInfo, err error) error { 61 | // Fills fileList with all mp3 files in the `root` file tree 62 | if err != nil { 63 | return err 64 | } 65 | if !*isRecursive && filepath.Dir(path) != filepath.Dir(root) { 66 | return filepath.SkipDir 67 | } 68 | if filepath.Ext(path) == ".mp3" { 69 | fileList = append(fileList, path) 70 | } 71 | return nil 72 | }) 73 | return 74 | } 75 | 76 | // Checks if a file already contains metadata 77 | func CheckFrames(frames map[string][]id3v2.Framer) bool { 78 | if _, ok := frames["TALB"]; !ok { 79 | return false 80 | } 81 | if _, ok := frames["TIT2"]; !ok { 82 | return false 83 | } 84 | if _, ok := frames["APIC"]; !ok { 85 | return false 86 | } 87 | if _, ok := frames["TRCK"]; !ok { 88 | return false 89 | } 90 | if _, ok := frames["TPOS"]; !ok { 91 | return false 92 | } 93 | return true 94 | } 95 | --------------------------------------------------------------------------------