├── .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 |
8 |
9 |
10 |
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 |
--------------------------------------------------------------------------------