Skip to content

Commit cd74309

Browse files
committed
Added chromaprint client and duration metadata
1 parent 6e6e29a commit cd74309

File tree

6 files changed

+86
-202
lines changed

6 files changed

+86
-202
lines changed

metadata.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@ type metadata struct {
1515
}
1616

1717
const (
18-
MetaArtwork = "artwork"
18+
MetaArtwork = "artwork" // Metadata key for artwork, sets the value as []byte
19+
MetaDuration = "duration" // Metadata key for duration, sets the value as float64 (seconds)
1920
)
2021

2122
////////////////////////////////////////////////////////////////////////////////

pkg/chromaprint/client.go

Lines changed: 32 additions & 171 deletions
Original file line numberDiff line numberDiff line change
@@ -1,225 +1,86 @@
11
package chromaprint
22

33
import (
4-
"encoding/json"
5-
"errors"
64
"fmt"
7-
"io/ioutil"
8-
"mime"
9-
"net/http"
105
"net/url"
11-
"os"
12-
"path/filepath"
136
"time"
147

8+
// Packages
9+
"github.com/mutablelogic/go-client"
10+
1511
// Namespace imports
1612
. "github.com/djthorpe/go-errors"
1713
)
1814

19-
////////////////////////////////////////////////////////////////////////////////
15+
///////////////////////////////////////////////////////////////////////////////
2016
// TYPES
2117

22-
type Config struct {
23-
Key string `yaml:"key"` // AcuostId Web Service Key
24-
Timeout time.Duration `yaml:"timeout"` // AcoustId Client timeout
25-
Rate uint `yaml:"rate"` // Maximum requests per second
26-
Base string `yaml:"base"` // Base URL
27-
}
28-
2918
type Client struct {
30-
Config
31-
*http.Client
32-
*url.URL
33-
last time.Time
19+
*client.Client
20+
key string
3421
}
3522

3623
////////////////////////////////////////////////////////////////////////////////
3724
// GLOBALS
3825

3926
const (
4027
// Register a clientId: https://acoustid.org/login
41-
defaultClientId = "-YectPtndAM"
42-
43-
// Timeout requests after 15 seconds
44-
defaultTimeout = 15 * time.Second
28+
defaultApiKey = "-YectPtndAM"
4529

4630
// The API endpoint
47-
baseUrl = "https://api.acoustid.org/v2"
31+
endPoint = "https://api.acoustid.org/v2"
4832

4933
// defaultQps rate limits number of requests per second
5034
defaultQps = 3
5135
)
5236

53-
var (
54-
ErrQueryRateExceeded = errors.New("query rate exceeded")
55-
)
37+
///////////////////////////////////////////////////////////////////////////////
38+
// LIFECYCLE
5639

57-
var (
58-
DefaultConfig = Config{
59-
Key: defaultClientId,
60-
Timeout: defaultTimeout,
61-
Rate: defaultQps,
62-
Base: baseUrl,
40+
// Create a new client
41+
func NewClient(ApiKey string, opts ...client.ClientOpt) (*Client, error) {
42+
// Check for missing API key
43+
if ApiKey == "" {
44+
ApiKey = defaultApiKey
6345
}
64-
)
65-
66-
////////////////////////////////////////////////////////////////////////////////
67-
// NEW
68-
69-
func NewClient(key string) *Client {
70-
config := DefaultConfig
71-
if key != "" {
72-
config.Key = os.ExpandEnv(key)
73-
}
74-
if client, err := NewClientWithConfig(config); err != nil {
75-
return nil
76-
} else {
77-
return client
78-
}
79-
}
80-
81-
func NewClientWithConfig(cfg Config) (*Client, error) {
82-
client := new(Client)
83-
client.Config = cfg
8446

85-
// Set parameters
86-
if client.Config.Timeout == 0 {
87-
client.Config.Timeout = DefaultConfig.Timeout
88-
}
89-
if client.Key == "" {
90-
client.Key = os.ExpandEnv(DefaultConfig.Key)
91-
}
92-
if client.Base == "" {
93-
client.Base = DefaultConfig.Base
94-
}
95-
if client.Rate == 0 {
96-
client.Rate = DefaultConfig.Rate
97-
}
98-
99-
// Create HTTP client
100-
client.Client = &http.Client{
101-
Timeout: client.Config.Timeout,
102-
}
103-
url, err := url.Parse(client.Base)
47+
// Create client
48+
opts = append(opts, client.OptEndpoint(endPoint))
49+
client, err := client.New(opts...)
10450
if err != nil {
10551
return nil, err
106-
} else {
107-
client.URL = url
10852
}
109-
// Fudge key into URL
110-
v := url.Query()
111-
v.Set("client", client.Key)
112-
url.RawQuery = v.Encode()
11353

114-
// Return success
115-
return client, nil
116-
}
117-
118-
////////////////////////////////////////////////////////////////////////////////
119-
// STRINGIFY
120-
121-
func (client *Client) String() string {
122-
str := "<chromaprint"
123-
if u := client.URL; u != nil {
124-
str += fmt.Sprintf(" url=%q", u.String())
125-
}
126-
if rate := client.Rate; rate > 0 {
127-
str += fmt.Sprintf(" rate=%dops/s", rate)
128-
}
129-
if timeout := client.Client.Timeout; timeout > 0 {
130-
str += fmt.Sprintf(" timeout=%v", timeout)
131-
}
132-
return str + ">"
54+
// Return the client
55+
return &Client{
56+
Client: client,
57+
key: ApiKey,
58+
}, nil
13359
}
13460

13561
////////////////////////////////////////////////////////////////////////////////
13662
// LOOKUP
13763

138-
func (client *Client) Lookup(fingerprint string, duration time.Duration, flags Meta) ([]*ResponseMatch, error) {
64+
// Lookup a fingerprint with a duration and the metadata that needs to be
65+
// returned
66+
func (c *Client) Lookup(fingerprint string, duration time.Duration, flags Meta) ([]*ResponseMatch, error) {
13967
// Check incoming parameters
14068
if fingerprint == "" || duration == 0 || flags == META_NONE {
14169
return nil, ErrBadParameter.With("Lookup")
14270
}
14371

144-
// Check Qps
145-
if client.last.IsZero() {
146-
if time.Since(client.last) < (time.Second / defaultQps) {
147-
return nil, ErrQueryRateExceeded
148-
}
149-
}
150-
15172
// Set URL parameters
15273
params := url.Values{}
74+
params.Set("client", c.key)
15375
params.Set("fingerprint", fingerprint)
15476
params.Set("duration", fmt.Sprint(duration.Truncate(time.Second).Seconds()))
15577
params.Set("meta", flags.String())
156-
url := client.requestUrl("lookup", params)
157-
if url == nil {
158-
return nil, ErrBadParameter.With("Lookup")
159-
}
16078

161-
//fmt.Println(url.String())
162-
163-
// Perform request
164-
now := time.Now()
165-
req, err := http.NewRequest(http.MethodGet, url.String(), nil)
166-
if err != nil {
79+
// Request -> Response
80+
var response Response
81+
if err := c.Do(nil, &response, client.OptPath("lookup"), client.OptQuery(params)); err != nil {
16782
return nil, err
83+
} else {
84+
return response.Results, nil
16885
}
169-
response, err := client.Client.Do(req)
170-
if err != nil {
171-
return nil, err
172-
}
173-
defer response.Body.Close()
174-
175-
// Read response body
176-
body, err := ioutil.ReadAll(response.Body)
177-
if err != nil {
178-
return nil, err
179-
}
180-
181-
// Decode response body
182-
var r Response
183-
if mimeType, _, err := mime.ParseMediaType(response.Header.Get("Content-type")); err != nil {
184-
return nil, ErrUnexpectedResponse.With(err)
185-
} else if mimeType != "application/json" {
186-
return nil, ErrUnexpectedResponse.With(mimeType)
187-
} else if err := json.Unmarshal(body, &r); err != nil {
188-
return nil, ErrUnexpectedResponse.With(err)
189-
}
190-
191-
// Check for errors
192-
if r.Status != "ok" {
193-
return nil, ErrBadParameter.Withf("%v (code %v)", r.Error.Message, r.Error.Code)
194-
} else if response.StatusCode != http.StatusOK {
195-
return nil, ErrBadParameter.Withf("%v (code %v)", response.Status, response.StatusCode)
196-
}
197-
198-
// Set response time for calculating qps
199-
client.last = now
200-
201-
// Return success
202-
return r.Results, nil
203-
}
204-
205-
////////////////////////////////////////////////////////////////////////////////
206-
// PRIVATE METHODS
207-
208-
func (client *Client) requestUrl(path string, v url.Values) *url.URL {
209-
url, err := url.Parse(client.URL.String())
210-
if err != nil {
211-
return nil
212-
}
213-
// Copy params
214-
params := client.URL.Query()
215-
for k := range v {
216-
params[k] = v[k]
217-
}
218-
url.RawQuery = params.Encode()
219-
220-
// Set path
221-
url.Path = filepath.Join(url.Path, path)
222-
223-
// Return URL
224-
return url
22586
}

pkg/chromaprint/client_test.go

Lines changed: 36 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@ import (
55
"testing"
66
"time"
77

8+
"github.com/stretchr/testify/assert"
9+
10+
// Namespace imports
811
. "github.com/mutablelogic/go-media/pkg/chromaprint"
912
)
1013

@@ -24,48 +27,50 @@ const (
2427
////////////////////////////////////////////////////////////////////////////////
2528
// TESTS
2629

27-
func Test_Client_001(t *testing.T) {
28-
client := NewClient("${CHROMAPRINT_KEY}")
30+
func GetKey(t *testing.T) string {
31+
key := os.Getenv("CHROMAPRINT_KEY")
32+
if key == "" {
33+
t.Skip("No API key set, set using environment variable CHROMAPRINT_KEY")
34+
}
35+
return key
36+
}
37+
38+
func Test_client_001(t *testing.T) {
39+
assert := assert.New(t)
40+
client, err := NewClient(GetKey(t))
41+
if !assert.NoError(err) {
42+
t.SkipNow()
43+
}
2944
if client == nil {
3045
t.Fatal("Failed to create client")
3146
} else {
3247
t.Log(client)
3348
}
3449
}
3550

36-
func Test_Client_002(t *testing.T) {
37-
key := os.Getenv("CHROMAPRINT_KEY")
38-
if key == "" {
39-
t.Skip("No API key set, set using environment variable CHROMAPRINT_KEY")
40-
}
41-
client, err := NewClientWithConfig(Config{Key: key})
42-
if err != nil {
43-
t.Fatal(err)
44-
}
45-
if matches, err := client.Lookup("AQAAT0mUaEkSRZEGAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", 5*time.Second, META_TRACK); err != nil {
46-
t.Error(err)
47-
} else if len(matches) != 0 {
48-
t.Error("Unexpected matches")
51+
func Test_client_002(t *testing.T) {
52+
assert := assert.New(t)
53+
client, err := NewClient(GetKey(t))
54+
if !assert.NoError(err) {
55+
t.SkipNow()
4956
}
57+
matches, err := client.Lookup("AQAAT0mUaEkSRZEGAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", 5*time.Second, META_TRACK)
58+
assert.NoError(err)
59+
t.Log(matches)
5060
}
5161

52-
func Test_Client_003(t *testing.T) {
53-
key := os.Getenv("CHROMAPRINT_KEY")
54-
if key == "" {
55-
t.Skip("No API key set, set using environment variable CHROMAPRINT_KEY")
56-
}
57-
client, err := NewClientWithConfig(Config{Key: key})
58-
if err != nil {
59-
t.Fatal(err)
62+
func Test_client_003(t *testing.T) {
63+
assert := assert.New(t)
64+
client, err := NewClient(GetKey(t))
65+
if !assert.NoError(err) {
66+
t.SkipNow()
6067
}
6168

62-
if matches, err := client.Lookup(SAMPLE_001_FINGERPRINT, SAMPLE_001_DURATION, META_ALL); err != nil {
63-
t.Error(err)
64-
} else if len(matches) == 0 {
65-
t.Error("No matches")
66-
} else {
67-
t.Log(matches)
69+
matches, err := client.Lookup(SAMPLE_001_FINGERPRINT, SAMPLE_001_DURATION, META_ALL)
70+
if !assert.NoError(err) {
71+
t.SkipNow()
6872
}
73+
t.Log(matches)
6974
}
7075

7176
/*
@@ -87,7 +92,6 @@ func Test_Client_004(t *testing.T) {
8792
t.Log(matches)
8893
}
8994
}
90-
*/
9195
9296
func Test_Client_005(t *testing.T) {
9397
key := os.Getenv("CHROMAPRINT_KEY")
@@ -107,3 +111,5 @@ func Test_Client_005(t *testing.T) {
107111
t.Log(matches)
108112
}
109113
}
114+
115+
*/

reader.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,14 @@ func (r *reader) Metadata(keys ...string) []Metadata {
208208
}
209209
}
210210

211+
// Add duration
212+
if len(keys) == 0 || slices.Contains(keys, MetaDuration) {
213+
duration := r.input.Duration()
214+
if duration > 0 {
215+
result = append(result, newMetadata(MetaDuration, float64(duration)/float64(ff.AV_TIME_BASE)))
216+
}
217+
}
218+
211219
// Obtain any artwork from the streams
212220
if slices.Contains(keys, MetaArtwork) {
213221
for _, stream := range r.input.Streams() {

sys/ffmpeg61/avformat.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -256,6 +256,10 @@ func (ctx *AVFormatContext) Flags() AVFormat {
256256
return AVFormat(ctx.flags)
257257
}
258258

259+
func (ctx *AVFormatContext) Duration() int64 {
260+
return int64(ctx.duration)
261+
}
262+
259263
////////////////////////////////////////////////////////////////////////////////
260264
// AVFormatFlag
261265

0 commit comments

Comments
 (0)