Repository: kalbhor/MusicRepair
Branch: master
Commit: 0865ce68d543
Files: 7
Total size: 11.4 KB
Directory structure:
gitextract_xfdxu3u9/
├── .gitignore
├── LICENSE
├── README.md
├── main.go
├── repair.go
├── spotify.go
└── utils.go
================================================
FILE CONTENTS
================================================
================================================
FILE: .gitignore
================================================
#### joe made this: http://goel.io/joe
#### go ####
# Compiled Object files, Static and Dynamic libs (Shared Objects)
*.o
*.a
*.so
# Folders
_obj
_test
# Architecture specific extensions/prefixes
*.[568vq]
[568vq].out
*.cgo1.go
*.cgo2.c
_cgo_defun.c
_cgo_gotypes.go
_cgo_export.*
_testmain.go
*.exe
*.test
*.prof
# Output of the go coverage tool, specifically when used with LiteIDE
*.out
# External packages folder
vendor/
================================================
FILE: LICENSE
================================================
MIT License
Copyright (c) 2016 Lakshay Kalbhor
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: README.md
================================================
Adds Metadata to Music files
## Features
1. Fixes songs in nested directories recursively.
2. Fetches metadata from [Spotify](https://www.spotify.com)
3. Multiple options to format file (Options to revert file back)
4. Simple binary
## Dependencies
### [Spotify API](https://developer.spotify.com/my-applications)
1. Create an account and register an application.
2. Copy the Client ID and Client Secret.
3. Set them in *config file* after running ```musicrepair -config```
###### *config file* will be created after running `musicrepair -config`, and located at `$HOME/.musicrepair/config.json`
### Set them using ```-config```
```sh
$ musicrepair -config
Enter Spotify client ID :
Enter Spotify client secret :
```
## Installing
### Via Binary
Download the latest binary from the [releases page](https://github.com/kalbhor/MusicRepair/releases).
Make sure to add the binary to your `$PATH`
### Via Go
```sh
$ go get -u -v github.com/kalbhor/musicrepair
$ which musicrepair
$ $GOPATH/bin/musicrepair
```
## Usage
Initially, you'll have to add the Spotify credentials.
```sh
$ musicrepair -config
```
After that, always a simple command
```sh
$ musicrepair
✨ 🍰
```
### Options
```
$ musicrepair -help
Usage of musicrepair:
-config
If set, MusicRepair will ask for credentials
-dir string
Specifies the directory where the music files are located (default "./")
-recursive
If set, Musicrepair will run recursively in the given directory
-revert
If set, Musicrepair will revert the files
-threads int
Specify the number of threads to use (default 1)
```
## Discussions/Write-Ups
## Contribute
Found an issue? Post it in the [issue tracker](https://github.com/kalbhor/MusicRepair/issues).
Want to add another awesome feature? [Fork](https://github.com/kalbhor/MusicRepair/fork) this repository and add your feature, then send a pull request.
## License
The MIT License (MIT)
Copyright (c) 2017 Lakshay Kalbhor
================================================
FILE: main.go
================================================
package main
import (
"flag"
"fmt"
"log"
"os"
)
var (
root = flag.String("dir", "./", "Specifies the directory where the music files are located")
isRecursive = flag.Bool("recursive", false, "If set, Musicrepair will run recursively in the given directory")
setConfig = flag.Bool("config", false, "If set, MusicRepair will ask for credentials")
isRevert = flag.Bool("revert", false, "If set, Musicrepair will revert the files")
threads = flag.Int("threads", 1, "Specify the number of threads to use")
)
func main() {
flag.Parse()
if *setConfig {
SetConfig()
fmt.Println("Your config has been saved.")
os.Exit(1)
}
config, err := LoadConfig()
if err != nil {
log.Fatal(err)
}
client, err := SpotifyAuth(config.ID, config.Secret)
if err != nil {
log.Fatal("Invalid spotify credentials. Error : %v", err)
}
fileList := WalkDir(*root) // List of all files to work on
jobs := make(chan string)
results := make(chan string)
for w := 1; w <= *threads; w++ {
if *isRevert {
go RevertWorker(jobs, results)
} else {
go RepairWorker(client, jobs, results)
}
}
for _, job := range fileList {
jobs <- job
}
close(jobs)
for r := 1; r <= len(fileList); r++ {
fmt.Printf("[%v] %v\n", r, <-results)
}
}
================================================
FILE: repair.go
================================================
package main
import (
"fmt"
"path/filepath"
"strconv"
"strings"
"github.com/bogem/id3v2"
"github.com/headzoo/surf/errors"
"github.com/zmb3/spotify"
)
func RepairWorker(client spotify.Client, job <-chan string, results chan<- string) {
for filePath := range job {
_, fileName := filepath.Split(filePath)
results <- fmt.Sprintf("Fixing : %v\n", fileName)
if err := Repair(client, filePath); err != nil {
results <- fmt.Sprintf("Error : %v\n", err)
}
}
}
func RevertWorker(job <-chan string, results chan<- string) {
for filePath := range job {
_, fileName := filepath.Split(filePath)
results <- fmt.Sprintf("Reverting : %v\n", fileName)
if err := Revert(filePath); err != nil {
results <- fmt.Sprintf("Error : %v\n", err)
}
}
}
func Revert(path string) error {
tag, err := id3v2.Open(path, id3v2.Options{Parse: true})
if err != nil {
return err
}
defer tag.Close()
tag.DeleteAllFrames()
if err = tag.Save(); err != nil {
return err
}
return nil
}
func Repair(client spotify.Client, path string) error {
tag, err := id3v2.Open(path, id3v2.Options{Parse: true})
if err != nil {
return err
}
if CheckFrames(tag.AllFrames()) {
return errors.New("Already contains tags")
}
_, filename := filepath.Split(path)
metadata, err := GetMetadata(client, filename[0:len(filename)-4])
if err != nil {
return err
}
tag.SetTitle(metadata.Title)
tag.SetAlbum(metadata.Album)
tag.SetArtist(strings.Join(metadata.Artists, ","))
TrackNumber := strconv.Itoa(metadata.TrackNumber)
tag.AddFrame("TRCK", id3v2.TextFrame{id3v2.EncodingUTF8, TrackNumber})
DiscNumber := strconv.Itoa(metadata.DiscNumber)
tag.AddFrame("TPOS", id3v2.TextFrame{id3v2.EncodingUTF8, DiscNumber})
pic := id3v2.PictureFrame{
Encoding: id3v2.EncodingUTF8,
MimeType: "image/jpeg",
PictureType: id3v2.PTFrontCover,
Description: "Front cover",
Picture: metadata.Image,
}
tag.AddAttachedPicture(pic)
if err = tag.Save(); err != nil {
return err
}
return nil
}
================================================
FILE: spotify.go
================================================
package main
import (
"context"
"errors"
"io/ioutil"
"net/http"
"golang.org/x/oauth2"
"golang.org/x/oauth2/clientcredentials"
"github.com/zmb3/spotify"
)
//Metadata : Structure for one track's metadata
type Metadata struct {
Title string
Artists []string
Album string
Image []byte
DiscNumber int
TrackNumber int
}
//Load : Sets values from search results
func (m *Metadata) Load(track spotify.FullTrack) error {
m.Title = track.SimpleTrack.Name
m.Album = track.Album.Name
m.DiscNumber = track.SimpleTrack.DiscNumber
m.TrackNumber = track.SimpleTrack.TrackNumber
for _, artist := range track.SimpleTrack.Artists {
m.Artists = append(m.Artists, artist.Name)
}
imageURL := track.Album.Images[0].URL
resp, err := http.Get(imageURL)
if err != nil {
return err
}
defer resp.Body.Close()
b, err := ioutil.ReadAll(resp.Body)
if err != nil {
return err
}
m.Image = b
return nil
}
//GetMetadata : Searches spotify and returns a loaded metadata struct
func GetMetadata(client spotify.Client, query string) (*Metadata, error) {
m := new(Metadata)
results, err := client.Search(query, spotify.SearchTypeTrack)
if err != nil {
return nil, err
} else if len(results.Tracks.Tracks) == 0 { // Search results were empty
return nil, errors.New("Couldn't fetch metadata")
}
err = m.Load(results.Tracks.Tracks[0]) // Pass in the top result
if err != nil {
return m, err
}
return m, nil
}
//Auth : Returns a usable spotify "client" that can request spotify content
func SpotifyAuth(Id, Secret string) (spotify.Client, error) {
config := &clientcredentials.Config{
ClientID: Id,
ClientSecret: Secret,
TokenURL: spotify.TokenURL,
}
token, err := config.Token(context.Background())
if err != nil {
return spotify.Authenticator{}.NewClient(&oauth2.Token{}), err
}
client := spotify.Authenticator{}.NewClient(token)
return client, nil
}
================================================
FILE: utils.go
================================================
package main
import (
"encoding/json"
"fmt"
"io/ioutil"
"os"
"path"
"path/filepath"
"github.com/bogem/id3v2"
)
type Config struct {
ID string
Secret string
}
var configFolder string = path.Join(os.Getenv("HOME"), ".musicrepair")
var configPath string = path.Join(configFolder, "config.json")
func LoadConfig() (*Config, error) {
file, err := os.Open(configPath)
if err != nil {
return nil, err
}
decoder := json.NewDecoder(file)
config := new(Config)
err = decoder.Decode(&config)
if err != nil {
return nil, err
}
return config, nil
}
func SetConfig() error {
var id, secret string
fmt.Print("Enter Spotify ID : ")
fmt.Scanln(&id)
fmt.Print("Enter Spotify Secret : ")
fmt.Scanln(&secret)
config := Config{id, secret}
b, err := json.Marshal(config)
if err != nil {
return err
}
if err := os.Mkdir(configFolder, os.ModePerm); err != nil {
return err
}
if err := ioutil.WriteFile(configPath, b, os.ModePerm); err != nil {
return err
}
return nil
}
func WalkDir(root string) (fileList []string) {
filepath.Walk(root, func(path string, fi os.FileInfo, err error) error {
// Fills fileList with all mp3 files in the `root` file tree
if err != nil {
return err
}
if !*isRecursive && filepath.Dir(path) != filepath.Dir(root) {
return filepath.SkipDir
}
if filepath.Ext(path) == ".mp3" {
fileList = append(fileList, path)
}
return nil
})
return
}
// Checks if a file already contains metadata
func CheckFrames(frames map[string][]id3v2.Framer) bool {
if _, ok := frames["TALB"]; !ok {
return false
}
if _, ok := frames["TIT2"]; !ok {
return false
}
if _, ok := frames["APIC"]; !ok {
return false
}
if _, ok := frames["TRCK"]; !ok {
return false
}
if _, ok := frames["TPOS"]; !ok {
return false
}
return true
}