2015-06-28 02:47:45 +02:00
|
|
|
// Copyright 2015, David Howden
|
|
|
|
// Use of this source code is governed by a BSD-style
|
|
|
|
// license that can be found in the LICENSE file.
|
|
|
|
|
|
|
|
/*
|
|
|
|
The check tool performs tag lookups on full music collections (iTunes or directory tree of files).
|
|
|
|
*/
|
2015-05-24 01:01:33 +02:00
|
|
|
package main
|
|
|
|
|
|
|
|
import (
|
|
|
|
"flag"
|
|
|
|
"fmt"
|
2018-04-02 03:06:41 +02:00
|
|
|
"io"
|
2015-05-24 01:01:33 +02:00
|
|
|
"net/url"
|
|
|
|
"os"
|
|
|
|
"path/filepath"
|
|
|
|
"strings"
|
|
|
|
|
|
|
|
"github.com/dhowden/itl"
|
|
|
|
"github.com/dhowden/tag"
|
|
|
|
)
|
|
|
|
|
|
|
|
func decodeLocation(l string) (string, error) {
|
|
|
|
u, err := url.ParseRequestURI(l)
|
|
|
|
if err != nil {
|
|
|
|
return "", err
|
|
|
|
}
|
|
|
|
// Annoyingly this doesn't replace & (&)
|
|
|
|
path := strings.Replace(u.Path, "&", "&", -1)
|
|
|
|
return path, nil
|
|
|
|
}
|
|
|
|
|
2018-04-02 02:54:32 +02:00
|
|
|
var (
|
|
|
|
itlXML = flag.String("itlXML", "", "iTunes Library Path")
|
|
|
|
path = flag.String("path", "", "path to directory containing audio files")
|
|
|
|
sum = flag.Bool("sum", false, "compute the checksum of the audio file (doesn't work for .flac or .ogg yet)")
|
|
|
|
)
|
|
|
|
|
2015-05-24 01:01:33 +02:00
|
|
|
func main() {
|
|
|
|
flag.Parse()
|
|
|
|
|
2018-04-02 02:54:32 +02:00
|
|
|
if *itlXML == "" && *path == "" || *itlXML != "" && *path != "" {
|
2015-05-24 01:01:33 +02:00
|
|
|
fmt.Println("you must specify one of -itlXML or -path")
|
2015-06-07 04:54:39 +02:00
|
|
|
flag.Usage()
|
2015-05-24 01:01:33 +02:00
|
|
|
os.Exit(1)
|
|
|
|
}
|
|
|
|
|
|
|
|
var paths <-chan string
|
2018-04-02 02:54:32 +02:00
|
|
|
if *itlXML != "" {
|
2015-05-24 01:01:33 +02:00
|
|
|
var err error
|
2018-04-02 02:54:32 +02:00
|
|
|
paths, err = walkLibrary(*itlXML)
|
2015-05-24 01:01:33 +02:00
|
|
|
if err != nil {
|
|
|
|
fmt.Println(err)
|
|
|
|
os.Exit(1)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-04-02 02:54:32 +02:00
|
|
|
if *path != "" {
|
|
|
|
paths = walkPath(*path)
|
2015-05-24 01:01:33 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
p := &processor{
|
|
|
|
decodingErrors: make(map[string]int),
|
|
|
|
hashErrors: make(map[string]int),
|
|
|
|
hashes: make(map[string]int),
|
|
|
|
}
|
|
|
|
|
|
|
|
done := make(chan bool)
|
|
|
|
go func() {
|
|
|
|
p.do(paths)
|
|
|
|
fmt.Println(p)
|
|
|
|
close(done)
|
|
|
|
}()
|
|
|
|
<-done
|
|
|
|
}
|
|
|
|
|
|
|
|
func walkPath(root string) <-chan string {
|
|
|
|
ch := make(chan string)
|
|
|
|
fn := func(path string, info os.FileInfo, err error) error {
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
if info.IsDir() {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
ch <- path
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
go func() {
|
|
|
|
err := filepath.Walk(root, fn)
|
|
|
|
if err != nil {
|
|
|
|
fmt.Println(err)
|
|
|
|
}
|
|
|
|
close(ch)
|
|
|
|
}()
|
|
|
|
return ch
|
|
|
|
}
|
|
|
|
|
|
|
|
func walkLibrary(path string) (<-chan string, error) {
|
2018-04-02 02:54:32 +02:00
|
|
|
f, err := os.Open(*itlXML)
|
2015-05-24 01:01:33 +02:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
defer f.Close()
|
|
|
|
|
|
|
|
l, err := itl.ReadFromXML(f)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
paths := make(chan string)
|
|
|
|
go func() {
|
|
|
|
for _, t := range l.Tracks {
|
|
|
|
loc, err := decodeLocation(t.Location)
|
|
|
|
if err != nil {
|
|
|
|
fmt.Println(err)
|
2016-03-06 12:21:54 +01:00
|
|
|
continue
|
2015-05-24 01:01:33 +02:00
|
|
|
}
|
|
|
|
paths <- loc
|
|
|
|
}
|
|
|
|
close(paths)
|
|
|
|
}()
|
|
|
|
return paths, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
type processor struct {
|
|
|
|
decodingErrors map[string]int
|
|
|
|
hashErrors map[string]int
|
|
|
|
hashes map[string]int
|
|
|
|
}
|
|
|
|
|
|
|
|
func (p *processor) String() string {
|
|
|
|
result := ""
|
|
|
|
for k, v := range p.decodingErrors {
|
|
|
|
result += fmt.Sprintf("%v : %v\n", k, v)
|
|
|
|
}
|
|
|
|
|
|
|
|
for k, v := range p.hashErrors {
|
|
|
|
result += fmt.Sprintf("%v : %v\n", k, v)
|
|
|
|
}
|
|
|
|
|
|
|
|
for k, v := range p.hashErrors {
|
|
|
|
if v > 1 {
|
|
|
|
result += fmt.Sprintf("%v : %v\n", k, v)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return result
|
|
|
|
}
|
|
|
|
|
|
|
|
func (p *processor) do(ch <-chan string) {
|
|
|
|
for path := range ch {
|
|
|
|
func() {
|
|
|
|
defer func() {
|
|
|
|
if p := recover(); p != nil {
|
|
|
|
fmt.Printf("Panicing at: %v", path)
|
|
|
|
panic(p)
|
|
|
|
}
|
|
|
|
}()
|
|
|
|
tf, err := os.Open(path)
|
|
|
|
if err != nil {
|
|
|
|
p.decodingErrors["error opening file"]++
|
|
|
|
return
|
|
|
|
}
|
|
|
|
defer tf.Close()
|
|
|
|
|
2015-07-02 15:07:50 +02:00
|
|
|
_, _, err = tag.Identify(tf)
|
|
|
|
if err != nil {
|
|
|
|
fmt.Println("IDENTIFY:", path, err.Error())
|
|
|
|
}
|
|
|
|
|
2015-05-24 01:01:33 +02:00
|
|
|
_, err = tag.ReadFrom(tf)
|
|
|
|
if err != nil {
|
|
|
|
fmt.Println("READFROM:", path, err.Error())
|
|
|
|
p.decodingErrors[err.Error()]++
|
|
|
|
}
|
|
|
|
|
2018-04-02 02:54:32 +02:00
|
|
|
if *sum {
|
2018-04-02 03:06:41 +02:00
|
|
|
_, err = tf.Seek(0, io.SeekStart)
|
2015-05-24 01:01:33 +02:00
|
|
|
if err != nil {
|
|
|
|
fmt.Println("DIED:", path, "error seeking back to 0:", err)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
h, err := tag.Sum(tf)
|
|
|
|
if err != nil {
|
|
|
|
fmt.Println("SUM:", path, err.Error())
|
|
|
|
p.hashErrors[err.Error()]++
|
|
|
|
}
|
|
|
|
p.hashes[h]++
|
|
|
|
}
|
|
|
|
}()
|
|
|
|
}
|
|
|
|
}
|