[
  {
    "path": ".gitignore",
    "content": "#### joe made this: http://goel.io/joe\n\n#### go ####\n# Compiled Object files, Static and Dynamic libs (Shared Objects)\n*.o\n*.a\n*.so\n\n# Folders\n_obj\n_test\n\n# Architecture specific extensions/prefixes\n*.[568vq]\n[568vq].out\n\n*.cgo1.go\n*.cgo2.c\n_cgo_defun.c\n_cgo_gotypes.go\n_cgo_export.*\n\n_testmain.go\n\n*.exe\n*.test\n*.prof\n\n# Output of the go coverage tool, specifically when used with LiteIDE\n*.out\n\n# External packages folder\nvendor/\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2016 Lakshay Kalbhor\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "<p align=\"center\">\n  <b>Adds Metadata to Music files</b>\n</p>\n\n<p align=\"center\">\n    <a href=\"https://github.com/kalbhor/musicrepair/LICENSE\">\n\t\t<img alt=\"License\"  src=\"https://img.shields.io/github/license/mashape/apistatus.svg?style=flat-square\"/>\n\t</a>        \n\t<a href=\"https://github.com/kalbhor/musicrepair\">\n\t\t<img alt=\"stars\" src=\"https://img.shields.io/github/stars/kalbhor/musicrepair.svg?style=social&label=Star\"/>\n\t</a>            \n</p>\n\n## Features\n\n1. Fixes songs in nested directories recursively.\n2. Fetches metadata from [Spotify](https://www.spotify.com)\n3. Multiple options to format file (Options to revert file back)\n4. Simple binary\n\n## Dependencies  \n\n### [Spotify API](https://developer.spotify.com/my-applications)\n\n1. Create an account and register an application.\n2. Copy the Client ID and Client Secret.\n3. Set them in *config file* after running ```musicrepair -config```\n\n###### *config file* will be created after running `musicrepair -config`, and located at `$HOME/.musicrepair/config.json`\n\n### Set them using ```-config```\n```sh \n$ musicrepair -config                                               \nEnter Spotify client ID : <enter Spotify client ID>  \nEnter Spotify client secret : <enter Spotify client secret>                                     \n```\n\n## Installing\n\n### Via Binary\n\nDownload the latest binary from the [releases page](https://github.com/kalbhor/MusicRepair/releases).\n\nMake sure to add the binary to your `$PATH`\n\n### Via Go\n```sh\n$ go get -u -v github.com/kalbhor/musicrepair\n$ which musicrepair\n$ $GOPATH/bin/musicrepair\n```\n\n## Usage\n\nInitially, you'll have to add the Spotify credentials. \n```sh\n$ musicrepair -config\n```\n\nAfter that, always a simple command\n```sh\n$ musicrepair\n✨ 🍰\n```\n\n\n### Options\n```\n$ musicrepair -help\n\nUsage of musicrepair:\n  -config\n    \tIf set, MusicRepair will ask for credentials\n  -dir string\n    \tSpecifies the directory where the music files are located (default \"./\")\n  -recursive\n    \tIf set, Musicrepair will run recursively in the given directory\n  -revert\n    \tIf set, Musicrepair will revert the files\n  -threads int\n    \tSpecify the number of threads to use (default 1)\n```\n\n## Discussions/Write-Ups\n<p align=\"left\">\n    <a href=\"https://mavielinux.com/2016/12/11/musicrepair-pour-corriger-les-titresajouter-les-metadonnees-et-les-pochettes-de-vos-musiques/\">\n\t\t<img width=\"70px\" src=\"http://i.imgur.com/TklsaII.png\"/>\n\t</a>\n\t&nbsp;&nbsp;&nbsp;\n    <a href=\"http://blog.desdelinux.net/reparar-archivos-de-musica/\">\n\t\t<img width=\"160px\" src=\"http://i.imgur.com/eV1WxYZ.png\"/>\n\t</a>\n\t&nbsp;&nbsp;&nbsp;\n    <a href=\"https://www.reddit.com/r/learnpython/comments/5gzvcb/i_made_a_script_that_would_fix_your_music_files/\">\n\t\t<img width=\"160px\" src=\"http://i.imgur.com/Jk8PgIb.png\"/>\n\t</a>\n\t&nbsp;&nbsp;&nbsp;\n</p>\n\n\n## Contribute\n\nFound an issue? Post it in the [issue tracker](https://github.com/kalbhor/MusicRepair/issues).\n\nWant to add another awesome feature? [Fork](https://github.com/kalbhor/MusicRepair/fork) this repository and add your feature, then send a pull request.\n\n## License\nThe MIT License (MIT)\nCopyright (c) 2017 Lakshay Kalbhor\n"
  },
  {
    "path": "main.go",
    "content": "package main\n\nimport (\n\t\"flag\"\n\t\"fmt\"\n\t\"log\"\n\t\"os\"\n)\n\nvar (\n\troot        = flag.String(\"dir\", \"./\", \"Specifies the directory where the music files are located\")\n\tisRecursive = flag.Bool(\"recursive\", false, \"If set, Musicrepair will run recursively in the given directory\")\n\tsetConfig   = flag.Bool(\"config\", false, \"If set, MusicRepair will ask for credentials\")\n\tisRevert    = flag.Bool(\"revert\", false, \"If set, Musicrepair will revert the files\")\n\tthreads     = flag.Int(\"threads\", 1, \"Specify the number of threads to use\")\n)\n\nfunc main() {\n\tflag.Parse()\n\n\tif *setConfig {\n\t\tSetConfig()\n\t\tfmt.Println(\"Your config has been saved.\")\n\t\tos.Exit(1)\n\t}\n\n\tconfig, err := LoadConfig()\n\tif err != nil {\n\t\tlog.Fatal(err)\n\t}\n\n\tclient, err := SpotifyAuth(config.ID, config.Secret)\n\tif err != nil {\n\t\tlog.Fatal(\"Invalid spotify credentials. Error : %v\", err)\n\t}\n\n\tfileList := WalkDir(*root) // List of all files to work on\n\n\tjobs := make(chan string)\n\tresults := make(chan string)\n\n\tfor w := 1; w <= *threads; w++ {\n\t\tif *isRevert {\n\t\t\tgo RevertWorker(jobs, results)\n\t\t} else {\n\t\t\tgo RepairWorker(client, jobs, results)\n\t\t}\n\t}\n\n\tfor _, job := range fileList {\n\t\tjobs <- job\n\t}\n\tclose(jobs)\n\n\tfor r := 1; r <= len(fileList); r++ {\n\t\tfmt.Printf(\"[%v] %v\\n\", r, <-results)\n\t}\n\n}\n"
  },
  {
    "path": "repair.go",
    "content": "package main\n\nimport (\n\t\"fmt\"\n\t\"path/filepath\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/bogem/id3v2\"\n\t\"github.com/headzoo/surf/errors\"\n\t\"github.com/zmb3/spotify\"\n)\n\n\nfunc RepairWorker(client spotify.Client, job <-chan string, results chan<- string) {\n\tfor filePath := range job {\n\t\t_, fileName := filepath.Split(filePath)\n\t\tresults <- fmt.Sprintf(\"Fixing : %v\\n\", fileName)\n\t\tif err := Repair(client, filePath); err != nil {\n\t\t\tresults <- fmt.Sprintf(\"Error : %v\\n\", err)\n\t\t}\n\t}\n}\n\nfunc RevertWorker(job <-chan string, results chan<- string) {\n\tfor filePath := range job {\n\t\t_, fileName := filepath.Split(filePath)\n\t\tresults <- fmt.Sprintf(\"Reverting : %v\\n\", fileName)\n\t\tif err := Revert(filePath); err != nil {\n\t\t\tresults <- fmt.Sprintf(\"Error : %v\\n\", err)\n\t\t}\n\t}\n}\n\n\nfunc Revert(path string) error {\n\ttag, err := id3v2.Open(path, id3v2.Options{Parse: true})\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer tag.Close()\n\n\ttag.DeleteAllFrames()\n\tif err = tag.Save(); err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\nfunc Repair(client spotify.Client, path string) error {\n\ttag, err := id3v2.Open(path, id3v2.Options{Parse: true})\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif CheckFrames(tag.AllFrames()) {\n\t\treturn errors.New(\"Already contains tags\")\n\t}\n\n\t_, filename := filepath.Split(path)\n\tmetadata, err := GetMetadata(client, filename[0:len(filename)-4])\n\tif err != nil {\n\t\treturn err\n\t}\n\n\ttag.SetTitle(metadata.Title)\n\ttag.SetAlbum(metadata.Album)\n\ttag.SetArtist(strings.Join(metadata.Artists, \",\"))\n\n\tTrackNumber := strconv.Itoa(metadata.TrackNumber)\n\ttag.AddFrame(\"TRCK\", id3v2.TextFrame{id3v2.EncodingUTF8, TrackNumber})\n\n\tDiscNumber := strconv.Itoa(metadata.DiscNumber)\n\ttag.AddFrame(\"TPOS\", id3v2.TextFrame{id3v2.EncodingUTF8, DiscNumber})\n\n\tpic := id3v2.PictureFrame{\n\t\tEncoding:    id3v2.EncodingUTF8,\n\t\tMimeType:    \"image/jpeg\",\n\t\tPictureType: id3v2.PTFrontCover,\n\t\tDescription: \"Front cover\",\n\t\tPicture:     metadata.Image,\n\t}\n\n\ttag.AddAttachedPicture(pic)\n\tif err = tag.Save(); err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "spotify.go",
    "content": "package main\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"io/ioutil\"\n\t\"net/http\"\n\n\t\"golang.org/x/oauth2\"\n\t\"golang.org/x/oauth2/clientcredentials\"\n\n\t\"github.com/zmb3/spotify\"\n)\n\n//Metadata : Structure for one track's metadata\ntype Metadata struct {\n\tTitle       string\n\tArtists     []string\n\tAlbum       string\n\tImage       []byte\n\tDiscNumber  int\n\tTrackNumber int\n}\n\n//Load : Sets values from search results\nfunc (m *Metadata) Load(track spotify.FullTrack) error {\n\tm.Title = track.SimpleTrack.Name\n\tm.Album = track.Album.Name\n\tm.DiscNumber = track.SimpleTrack.DiscNumber\n\tm.TrackNumber = track.SimpleTrack.TrackNumber\n\n\tfor _, artist := range track.SimpleTrack.Artists {\n\t\tm.Artists = append(m.Artists, artist.Name)\n\t}\n\n\timageURL := track.Album.Images[0].URL\n\tresp, err := http.Get(imageURL)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer resp.Body.Close()\n\n\tb, err := ioutil.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn err\n\t}\n\tm.Image = b\n\n\treturn nil\n}\n\n//GetMetadata : Searches spotify and returns a loaded metadata struct\nfunc GetMetadata(client spotify.Client, query string) (*Metadata, error) {\n\n\tm := new(Metadata)\n\n\tresults, err := client.Search(query, spotify.SearchTypeTrack)\n\tif err != nil {\n\t\treturn nil, err\n\t} else if len(results.Tracks.Tracks) == 0 { // Search results were empty\n\t\treturn nil, errors.New(\"Couldn't fetch metadata\")\n\t}\n\n\terr = m.Load(results.Tracks.Tracks[0]) // Pass in the top result\n\tif err != nil {\n\t\treturn m, err\n\t}\n\treturn m, nil\n\n}\n\n//Auth : Returns a usable spotify \"client\" that can request spotify content\nfunc SpotifyAuth(Id, Secret string) (spotify.Client, error) {\n\tconfig := &clientcredentials.Config{\n\t\tClientID:     Id,\n\t\tClientSecret: Secret,\n\t\tTokenURL:     spotify.TokenURL,\n\t}\n\ttoken, err := config.Token(context.Background())\n\tif err != nil {\n\t\treturn spotify.Authenticator{}.NewClient(&oauth2.Token{}), err\n\t}\n\n\tclient := spotify.Authenticator{}.NewClient(token)\n\n\treturn client, nil\n}\n"
  },
  {
    "path": "utils.go",
    "content": "package main\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io/ioutil\"\n\t\"os\"\n\t\"path\"\n\t\"path/filepath\"\n\n\t\"github.com/bogem/id3v2\"\n)\n\ntype Config struct {\n\tID     string\n\tSecret string\n}\n\nvar configFolder string = path.Join(os.Getenv(\"HOME\"), \".musicrepair\")\nvar configPath string = path.Join(configFolder, \"config.json\")\n\nfunc LoadConfig() (*Config, error) {\n\tfile, err := os.Open(configPath)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdecoder := json.NewDecoder(file)\n\tconfig := new(Config)\n\terr = decoder.Decode(&config)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn config, nil\n}\n\nfunc SetConfig() error {\n\tvar id, secret string\n\tfmt.Print(\"Enter Spotify ID : \")\n\tfmt.Scanln(&id)\n\tfmt.Print(\"Enter Spotify Secret : \")\n\tfmt.Scanln(&secret)\n\n\tconfig := Config{id, secret}\n\tb, err := json.Marshal(config)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif err := os.Mkdir(configFolder, os.ModePerm); err != nil {\n\t\treturn err\n\t}\n\tif err := ioutil.WriteFile(configPath, b, os.ModePerm); err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\nfunc WalkDir(root string) (fileList []string) {\n\n\tfilepath.Walk(root, func(path string, fi os.FileInfo, err error) error {\n\t\t// Fills fileList with all mp3 files in the `root` file tree\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif !*isRecursive && filepath.Dir(path) != filepath.Dir(root) {\n\t\t\treturn filepath.SkipDir\n\t\t}\n\t\tif filepath.Ext(path) == \".mp3\" {\n\t\t\tfileList = append(fileList, path)\n\t\t}\n\t\treturn nil\n\t})\n\treturn\n}\n\n// Checks if a file already contains metadata\nfunc CheckFrames(frames map[string][]id3v2.Framer) bool {\n\tif _, ok := frames[\"TALB\"]; !ok {\n\t\treturn false\n\t}\n\tif _, ok := frames[\"TIT2\"]; !ok {\n\t\treturn false\n\t}\n\tif _, ok := frames[\"APIC\"]; !ok {\n\t\treturn false\n\t}\n\tif _, ok := frames[\"TRCK\"]; !ok {\n\t\treturn false\n\t}\n\tif _, ok := frames[\"TPOS\"]; !ok {\n\t\treturn false\n\t}\n\treturn true\n}\n"
  }
]