audd 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: c61c509e3b3413538a3282945c8ddd9de46d730e060f4245aa9d7c53a3f77833
4
+ data.tar.gz: 5488664c79eac72e52993021ba29a8163482e9f91ddca9a2ca1a170f9156ac23
5
+ SHA512:
6
+ metadata.gz: b87350f6c2823ecfc33da833f3ad5ac20adbcbf0b24396a784d8a53420517b1e735423f53e852e9286df23728550aa2021426c3088d9c92e99b21a387f84d57d
7
+ data.tar.gz: e4408ec839b591774d785eba13a8aa33780536af993fd8786bfcfdae2af032d3af547a23b35693e68b81b8a6ba82ebf00d2a75cedb324e198617f9f68a98225c
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
data/.standard.yml ADDED
@@ -0,0 +1,3 @@
1
+ # For available configuration options, see:
2
+ # https://github.com/standardrb/standard
3
+ ruby_version: 3.1
data/CHANGELOG.md ADDED
@@ -0,0 +1,23 @@
1
+ ## [Unreleased]
2
+
3
+ ### Added
4
+
5
+ - `Audd::Client#recognize` — the `POST api.audd.io/` recognition endpoint, via a
6
+ chainable request builder (`.return_metadata`, `.market`, `.timeout`) that sends
7
+ on `#execute!`. Takes a file path, `Pathname`, or `IO` as `file:`, or an https
8
+ url as `url:`.
9
+ - Response objects named after the AudD OpenAPI schemas and namespaced under
10
+ `Audd`: `RecognitionResult`, `RecognizeSuccessResponse`, `ErrorResponse`,
11
+ `AppleMusicMetadata`, `SpotifyMetadata`, `DeezerMetadata`, `NapsterMetadata`
12
+ and `MusicBrainzEntry`. Unmapped keys stay reachable via `#[]` and `#raw`.
13
+ - An exception hierarchy under `Audd::Error`, mapping every AudD error code to a
14
+ typed `Audd::ApiError` subclass and carrying the server's request echo.
15
+ - A project-specific `User-Agent`: `audd-ruby/<version> ruby/<ruby version> (<platform>)`.
16
+ - Contract tests running against the `AudDMusic/audd-openapi` spec and fixtures,
17
+ vendored as a submodule pinned to `v1.4.4`.
18
+
19
+ `#recognize_enterprise` and `#find_lyrics` are stubbed and raise `NotImplementedError`.
20
+
21
+ ## [0.1.0] - 2026-08-26
22
+
23
+ - Initial release
@@ -0,0 +1,132 @@
1
+ # Contributor Covenant Code of Conduct
2
+
3
+ ## Our Pledge
4
+
5
+ We as members, contributors, and leaders pledge to make participation in our
6
+ community a harassment-free experience for everyone, regardless of age, body
7
+ size, visible or invisible disability, ethnicity, sex characteristics, gender
8
+ identity and expression, level of experience, education, socio-economic status,
9
+ nationality, personal appearance, race, caste, color, religion, or sexual
10
+ identity and orientation.
11
+
12
+ We pledge to act and interact in ways that contribute to an open, welcoming,
13
+ diverse, inclusive, and healthy community.
14
+
15
+ ## Our Standards
16
+
17
+ Examples of behavior that contributes to a positive environment for our
18
+ community include:
19
+
20
+ * Demonstrating empathy and kindness toward other people
21
+ * Being respectful of differing opinions, viewpoints, and experiences
22
+ * Giving and gracefully accepting constructive feedback
23
+ * Accepting responsibility and apologizing to those affected by our mistakes,
24
+ and learning from the experience
25
+ * Focusing on what is best not just for us as individuals, but for the overall
26
+ community
27
+
28
+ Examples of unacceptable behavior include:
29
+
30
+ * The use of sexualized language or imagery, and sexual attention or advances of
31
+ any kind
32
+ * Trolling, insulting or derogatory comments, and personal or political attacks
33
+ * Public or private harassment
34
+ * Publishing others' private information, such as a physical or email address,
35
+ without their explicit permission
36
+ * Other conduct which could reasonably be considered inappropriate in a
37
+ professional setting
38
+
39
+ ## Enforcement Responsibilities
40
+
41
+ Community leaders are responsible for clarifying and enforcing our standards of
42
+ acceptable behavior and will take appropriate and fair corrective action in
43
+ response to any behavior that they deem inappropriate, threatening, offensive,
44
+ or harmful.
45
+
46
+ Community leaders have the right and responsibility to remove, edit, or reject
47
+ comments, commits, code, wiki edits, issues, and other contributions that are
48
+ not aligned to this Code of Conduct, and will communicate reasons for moderation
49
+ decisions when appropriate.
50
+
51
+ ## Scope
52
+
53
+ This Code of Conduct applies within all community spaces, and also applies when
54
+ an individual is officially representing the community in public spaces.
55
+ Examples of representing our community include using an official email address,
56
+ posting via an official social media account, or acting as an appointed
57
+ representative at an online or offline event.
58
+
59
+ ## Enforcement
60
+
61
+ Instances of abusive, harassing, or otherwise unacceptable behavior may be
62
+ reported to the community leaders responsible for enforcement at
63
+ [INSERT CONTACT METHOD].
64
+ All complaints will be reviewed and investigated promptly and fairly.
65
+
66
+ All community leaders are obligated to respect the privacy and security of the
67
+ reporter of any incident.
68
+
69
+ ## Enforcement Guidelines
70
+
71
+ Community leaders will follow these Community Impact Guidelines in determining
72
+ the consequences for any action they deem in violation of this Code of Conduct:
73
+
74
+ ### 1. Correction
75
+
76
+ **Community Impact**: Use of inappropriate language or other behavior deemed
77
+ unprofessional or unwelcome in the community.
78
+
79
+ **Consequence**: A private, written warning from community leaders, providing
80
+ clarity around the nature of the violation and an explanation of why the
81
+ behavior was inappropriate. A public apology may be requested.
82
+
83
+ ### 2. Warning
84
+
85
+ **Community Impact**: A violation through a single incident or series of
86
+ actions.
87
+
88
+ **Consequence**: A warning with consequences for continued behavior. No
89
+ interaction with the people involved, including unsolicited interaction with
90
+ those enforcing the Code of Conduct, for a specified period of time. This
91
+ includes avoiding interactions in community spaces as well as external channels
92
+ like social media. Violating these terms may lead to a temporary or permanent
93
+ ban.
94
+
95
+ ### 3. Temporary Ban
96
+
97
+ **Community Impact**: A serious violation of community standards, including
98
+ sustained inappropriate behavior.
99
+
100
+ **Consequence**: A temporary ban from any sort of interaction or public
101
+ communication with the community for a specified period of time. No public or
102
+ private interaction with the people involved, including unsolicited interaction
103
+ with those enforcing the Code of Conduct, is allowed during this period.
104
+ Violating these terms may lead to a permanent ban.
105
+
106
+ ### 4. Permanent Ban
107
+
108
+ **Community Impact**: Demonstrating a pattern of violation of community
109
+ standards, including sustained inappropriate behavior, harassment of an
110
+ individual, or aggression toward or disparagement of classes of individuals.
111
+
112
+ **Consequence**: A permanent ban from any sort of public interaction within the
113
+ community.
114
+
115
+ ## Attribution
116
+
117
+ This Code of Conduct is adapted from the [Contributor Covenant][homepage],
118
+ version 2.1, available at
119
+ [https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1].
120
+
121
+ Community Impact Guidelines were inspired by
122
+ [Mozilla's code of conduct enforcement ladder][Mozilla CoC].
123
+
124
+ For answers to common questions about this code of conduct, see the FAQ at
125
+ [https://www.contributor-covenant.org/faq][FAQ]. Translations are available at
126
+ [https://www.contributor-covenant.org/translations][translations].
127
+
128
+ [homepage]: https://www.contributor-covenant.org
129
+ [v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html
130
+ [Mozilla CoC]: https://github.com/mozilla/diversity
131
+ [FAQ]: https://www.contributor-covenant.org/faq
132
+ [translations]: https://www.contributor-covenant.org/translations
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2026 Rowan McDonald
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
13
+ all 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
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,173 @@
1
+ # Audd
2
+
3
+ Unofficial ruby sdk for AudD music recognition api. https://docs.audd.io/
4
+
5
+ I've only implemented the parts of the api that I use, please open prs to contribute any additional functionality you would like.
6
+
7
+ ## Installation
8
+
9
+ Install the gem and add to the application's Gemfile by executing:
10
+
11
+ ```bash
12
+ bundle add audd
13
+ ```
14
+
15
+ If bundler is not being used to manage dependencies, install the gem by executing:
16
+
17
+ ```bash
18
+ gem install audd
19
+ ```
20
+
21
+ ## Usage
22
+
23
+
24
+ ### Recognize a song
25
+ ```ruby
26
+ api_token = "your_token"
27
+ audd = Audd.new(api_token)
28
+
29
+ # read https://docs.audd.io/#recognize to learn about request options.
30
+ request = audd.recognize(file: file) # one of file: file or url: url
31
+ .return_metadata(:apple_music, :musicbrainz, :spotify, :deezer)
32
+ .market("us")
33
+
34
+ result = request.execute!
35
+ ```
36
+
37
+ `file:` takes a path, a `Pathname`, or an open `IO`; `url:` takes an https url that
38
+ AudD downloads itself. Pass exactly one of them.
39
+
40
+ Nothing is sent until `execute!`, which returns an `Audd::RecognitionResult` — or
41
+ `nil` when the audio was processed fine but didn't match anything. That is a
42
+ successful response, not an error.
43
+
44
+ ```ruby
45
+ result.title #=> "Everybody Wants To Rule The World"
46
+ result.artist #=> "Tears For Fears"
47
+ result.timecode #=> "00:56" — where in the song the matched fragment plays
48
+ result.apple_music #=> Audd::AppleMusicMetadata
49
+ result.public_match? #=> true (false for matches against your own catalog)
50
+ ```
51
+
52
+ The response classes are plain objects named after the [AudD OpenAPI
53
+ schemas](https://github.com/AudDMusic/audd-openapi), namespaced under `Audd`:
54
+ `RecognitionResult`, `AppleMusicMetadata`, `SpotifyMetadata`, `DeezerMetadata`,
55
+ `NapsterMetadata` and `MusicBrainzEntry`. Those schemas all allow extra
56
+ properties and AudD passes provider payloads through verbatim, so any key the
57
+ gem doesn't map is still reachable — `result.apple_music["hasLyrics"]` — and the
58
+ whole payload is on `#raw`.
59
+
60
+ Only `recognize` is implemented. `Audd::Client#recognize_enterprise` and
61
+ `Audd::Client#find_lyrics` raise `NotImplementedError`.
62
+
63
+ ### Timeouts
64
+ Because the recognize endpoint holds a long request open, the timeout for that endpoint is 60 seconds
65
+ - make sure not to make this request inside of an http transaction.
66
+
67
+ Override it per request with `.timeout(seconds)`. On expiry you get an
68
+ `Audd::TimeoutError`. Note that AudD meters the request server-side whether or
69
+ not you wait for the answer.
70
+
71
+ ### Errors
72
+
73
+ Everything raised descends from `Audd::Error`, so one `rescue` catches the lot:
74
+
75
+ ```
76
+ Audd::Error
77
+ ├── Audd::ConfigurationError # no api_token given and AUDD_API_TOKEN unset
78
+ ├── Audd::InvalidParameterError # bad arguments — raised before anything is sent
79
+ ├── Audd::ConnectionError # network, DNS, or TLS failure; nothing was received
80
+ │ └── Audd::TimeoutError # the request outran its timeout
81
+ ├── Audd::SerializationError # a 2xx response whose body wasn't parseable JSON
82
+ └── Audd::ApiError # the server answered status: "error"
83
+ ├── Audd::AuthenticationError
84
+ ├── Audd::QuotaError
85
+ ├── Audd::SubscriptionError
86
+ ├── Audd::InvalidRequestError
87
+ ├── Audd::InvalidAudioError
88
+ ├── Audd::StreamLimitError
89
+ ├── Audd::RateLimitError
90
+ ├── Audd::NotReleasedError
91
+ ├── Audd::BlockedError
92
+ ├── Audd::NeedsUpdateError
93
+ └── Audd::ServerError
94
+ ```
95
+
96
+ `Audd::ConfigurationError` and `Audd::InvalidParameterError` are raised locally,
97
+ before any HTTP happens — a missing token, neither or both of `file:`/`url:`, an
98
+ unreadable path, or a `return_metadata` source AudD doesn't accept.
99
+
100
+ The rest of the hierarchy comes from the server. Each AudD error code maps to
101
+ one class:
102
+
103
+ | Code(s) | Class | Meaning |
104
+ |---|---|---|
105
+ | 900, 901, 903 | `Audd::AuthenticationError` | The api_token is missing, wrong, or inactive |
106
+ | 902 | `Audd::QuotaError` | Quota or per-copy limit reached |
107
+ | 904, 905 | `Audd::SubscriptionError` | The endpoint isn't available on this plan |
108
+ | 50, 51, 600, 601, 602, 700, 701, 702, 906 | `Audd::InvalidRequestError` | Bad input — no file or url sent, malformed parameters |
109
+ | 300, 400, 500 | `Audd::InvalidAudioError` | The audio itself couldn't be read or fingerprinted |
110
+ | 610 | `Audd::StreamLimitError` | Subscription stream slots exhausted |
111
+ | 611 | `Audd::RateLimitError` | Per-stream daily rate limit |
112
+ | 907 | `Audd::NotReleasedError` | The song hasn't been released yet |
113
+ | 19, 31337 | `Audd::BlockedError` | Security, abuse, sanctions, IP ban, or maintenance |
114
+ | 20 | `Audd::NeedsUpdateError` | Client needs updating / paid version required |
115
+ | 100, 1000, anything else | `Audd::ServerError` | Upstream failure, and every code this gem doesn't recognise |
116
+
117
+ Code 51 is the exception to the rule: it's a soft deprecation warning. When the
118
+ server sends it *and* still returns a result, the gem warns on stderr and hands
119
+ you the result instead of raising.
120
+
121
+ Every `Audd::ApiError` carries `error_code`, `error_message`, `http_status`,
122
+ `request_id`, `request_params`, `request_method`, `branded_message` and
123
+ `raw_response` — enough to log an incident or open a ticket with AudD support.
124
+
125
+ ```ruby
126
+ begin
127
+ result = audd.recognize(file: file).execute!
128
+ rescue Audd::AuthenticationError => e
129
+ abort "check your token: #{e.message}"
130
+ rescue Audd::InvalidAudioError => e
131
+ warn "we sent something unusable: #{e.error_message}"
132
+ rescue Audd::ApiError => e
133
+ warn "AudD ##{e.error_code}: #{e.error_message} (request_id=#{e.request_id})"
134
+ end
135
+ ```
136
+
137
+
138
+ ## Development
139
+
140
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
141
+
142
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
143
+
144
+ ### Testing
145
+ We use contract testing based on the audd openapi https://github.com/AudDMusic/audd-openapi library.
146
+
147
+ It's vendored as a git submodule at `spec/audd-openapi`, pinned to a tag (`v1.4.4`)
148
+ so the contract can't move under us without a reviewable diff. Fetch it with:
149
+
150
+ ```bash
151
+ git submodule update --init
152
+ ```
153
+
154
+ The contract specs read both the spec document — to check we only send the form
155
+ fields `POST /` documents, and that every error code it enumerates maps to an
156
+ exception class — and the captured response fixtures, which they replay through
157
+ the real client. Without the submodule those specs skip rather than fail, so the
158
+ rest of the suite still runs on a fresh clone.
159
+
160
+ To take a new spec version: update the submodule to the new tag, bump
161
+ `OpenAPI::PINNED_VERSION` in `spec/support/openapi.rb`, and run `rake`.
162
+
163
+ ## Contributing
164
+
165
+ Bug reports and pull requests are welcome on GitHub at https://github.com/RowanMcDonald /audd. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/[USERNAME]/audd/blob/main/CODE_OF_CONDUCT.md).
166
+
167
+ ## License
168
+
169
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
170
+
171
+ ## Code of Conduct
172
+
173
+ Everyone interacting in the Audd project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/[USERNAME]/audd/blob/main/CODE_OF_CONDUCT.md).
data/Rakefile ADDED
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ require "standard/rake"
9
+
10
+ task default: %i[spec standard]
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "errors"
4
+ require_relative "http_client"
5
+ require_relative "recognize_request"
6
+
7
+ module Audd
8
+ class Client
9
+ attr_reader :api_token, :base_url
10
+
11
+ def initialize(api_token:, base_url:)
12
+ @api_token = api_token
13
+ @base_url = base_url
14
+ @http_client = HttpClient.new
15
+ end
16
+
17
+ def post(path, fields:, timeout:, file: nil)
18
+ @http_client.post_form(
19
+ "#{base_url}#{path}",
20
+ fields: fields.merge("api_token" => api_token),
21
+ timeout: timeout,
22
+ file: file
23
+ )
24
+ end
25
+
26
+ def inspect
27
+ "#<#{self.class.name} base_url=#{base_url.inspect} api_token=[FILTERED]>"
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,125 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Audd
4
+ # Base of everything this gem raises. `rescue Audd::Error` catches the lot.
5
+ class Error < StandardError; end
6
+
7
+ # Raised before any request is made.
8
+
9
+ # No api_token was supplied and AUDD_API_TOKEN is unset.
10
+ class ConfigurationError < Error; end
11
+
12
+ # The caller passed something the API can't accept: neither (or both) of
13
+ # `file:`/`url:`, an unknown `return` source, an unreadable file.
14
+ class InvalidParameterError < Error; end
15
+
16
+ # Raised when no usable response came back.
17
+
18
+ # Network, DNS, or TLS failure — nothing was received.
19
+ class ConnectionError < Error; end
20
+
21
+ # The request outran its timeout. Recognition holds the connection open
22
+ # while the server fingerprints the audio, so this is the one to watch.
23
+ class TimeoutError < ConnectionError; end
24
+
25
+ # A 2xx response whose body wasn't parseable JSON.
26
+ class SerializationError < Error
27
+ attr_reader :raw_body
28
+
29
+ def initialize(message, raw_body: "")
30
+ @raw_body = raw_body
31
+ super(message)
32
+ end
33
+ end
34
+
35
+ # The server answered with `status: "error"`. Carries the AudD error code
36
+ # plus the server's full echo of the request, which is what AudD support
37
+ # asks for.
38
+ class ApiError < Error
39
+ attr_reader :error_code, :error_message, :http_status, :request_id,
40
+ :request_params, :request_method, :branded_message, :raw_response
41
+
42
+ def initialize(error_code:, error_message:, http_status: nil, request_id: nil,
43
+ request_params: {}, request_method: nil, branded_message: nil, raw_response: nil)
44
+ @error_code = error_code
45
+ @error_message = error_message
46
+ @http_status = http_status
47
+ @request_id = request_id
48
+ @request_params = request_params || {}
49
+ @request_method = request_method
50
+ @branded_message = branded_message
51
+ @raw_response = raw_response
52
+ super("[##{error_code}] #{error_message}")
53
+ end
54
+ end
55
+
56
+ # 900 / 901 / 903 — the token is the problem.
57
+ class AuthenticationError < ApiError; end
58
+
59
+ # 902 — quota or per-copy limit reached.
60
+ class QuotaError < ApiError; end
61
+
62
+ # 904 / 905 — the endpoint isn't available on this plan.
63
+ class SubscriptionError < ApiError; end
64
+
65
+ # 50 / 51 / 600 / 601 / 602 / 700 / 701 / 702 / 906 — bad input from the caller.
66
+ class InvalidRequestError < ApiError; end
67
+
68
+ # 300 / 400 / 500 — the audio itself is the problem.
69
+ class InvalidAudioError < ApiError; end
70
+
71
+ # 610 — subscription stream slots exhausted.
72
+ class StreamLimitError < ApiError; end
73
+
74
+ # 611 — per-stream daily rate limit.
75
+ class RateLimitError < ApiError; end
76
+
77
+ # 907 — the song hasn't been released yet.
78
+ class NotReleasedError < ApiError; end
79
+
80
+ # 19 / 31337 — security, abuse, sanctions, IP ban, or maintenance.
81
+ class BlockedError < ApiError; end
82
+
83
+ # 20 — the client needs updating / a paid version is required.
84
+ class NeedsUpdateError < ApiError; end
85
+
86
+ # 100 / 1000, and every code this gem doesn't recognise.
87
+ class ServerError < ApiError; end
88
+
89
+ # Maps AudD's numeric error codes onto the classes above. The code list is
90
+ # the enum in the OpenAPI `ErrorResponse` schema; 40 has no documented
91
+ # meaning, so it falls through to ServerError along with anything new.
92
+ ERROR_CLASSES_BY_CODE = {
93
+ 900 => AuthenticationError,
94
+ 901 => AuthenticationError,
95
+ 903 => AuthenticationError,
96
+ 902 => QuotaError,
97
+ 904 => SubscriptionError,
98
+ 905 => SubscriptionError,
99
+ 50 => InvalidRequestError,
100
+ 51 => InvalidRequestError,
101
+ 600 => InvalidRequestError,
102
+ 601 => InvalidRequestError,
103
+ 602 => InvalidRequestError,
104
+ 700 => InvalidRequestError,
105
+ 701 => InvalidRequestError,
106
+ 702 => InvalidRequestError,
107
+ 906 => InvalidRequestError,
108
+ 300 => InvalidAudioError,
109
+ 400 => InvalidAudioError,
110
+ 500 => InvalidAudioError,
111
+ 610 => StreamLimitError,
112
+ 611 => RateLimitError,
113
+ 907 => NotReleasedError,
114
+ 19 => BlockedError,
115
+ 31337 => BlockedError,
116
+ 20 => NeedsUpdateError,
117
+ 100 => ServerError,
118
+ 1000 => ServerError
119
+ }.freeze
120
+
121
+ # Unknown codes are still errors — just not ones we can name.
122
+ def self.error_class_for(code)
123
+ ERROR_CLASSES_BY_CODE.fetch(code, ServerError)
124
+ end
125
+ end
@@ -0,0 +1,87 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "net/http"
4
+ require "openssl"
5
+ require "securerandom"
6
+ require "uri"
7
+
8
+ require_relative "errors"
9
+
10
+ module Audd
11
+ class HttpClient
12
+ Response = Struct.new(:status, :body, :request_id, keyword_init: true)
13
+
14
+ USER_AGENT = "audd-ruby/#{VERSION} ruby/#{RUBY_VERSION} (#{RUBY_PLATFORM})"
15
+
16
+ # AudD only accepts multipart uploads; the field name for the upload is
17
+ # fixed by the API.
18
+ FILE_FIELD = "file"
19
+
20
+ # POSTs `fields` (and optionally one file) to `url`.
21
+ #
22
+ # @param file [Array(String, IO), nil] a [filename, io] pair
23
+ def post_form(url, fields:, timeout:, file: nil)
24
+ uri = URI.parse(url)
25
+ boundary = SecureRandom.hex(16)
26
+
27
+ request = Net::HTTP::Post.new(uri)
28
+ request["User-Agent"] = USER_AGENT
29
+ request["Content-Type"] = "multipart/form-data; boundary=#{boundary}"
30
+ request.body = multipart_body(fields, file, boundary)
31
+
32
+ perform(uri, request, timeout)
33
+ end
34
+
35
+ private
36
+
37
+ def perform(uri, request, timeout)
38
+ http = Net::HTTP.new(uri.host, uri.port)
39
+ http.use_ssl = uri.scheme == "https"
40
+ http.open_timeout = timeout
41
+ http.read_timeout = timeout
42
+ http.write_timeout = timeout
43
+
44
+ response = http.start { |session| session.request(request) }
45
+ Response.new(
46
+ status: response.code.to_i,
47
+ body: response.body,
48
+ request_id: response["x-request-id"]
49
+ )
50
+ rescue Net::OpenTimeout, Net::ReadTimeout, Net::WriteTimeout => e
51
+ raise TimeoutError, "Request to #{uri} timed out after #{timeout}s (#{e.class})"
52
+ rescue SocketError, SystemCallError, IOError,
53
+ OpenSSL::SSL::SSLError, Net::HTTPBadResponse, Net::ProtocolError => e
54
+ raise ConnectionError, "Request to #{uri} failed: #{e.class}: #{e.message}"
55
+ end
56
+
57
+ def multipart_body(fields, file, boundary)
58
+ body = +"".b
59
+
60
+ fields.each do |name, value|
61
+ body << "--#{boundary}\r\n".b
62
+ body << "Content-Disposition: form-data; name=\"#{quote(name)}\"\r\n\r\n".b
63
+ body << value.to_s.b
64
+ body << "\r\n".b
65
+ end
66
+
67
+ if file
68
+ filename, io = file
69
+ body << "--#{boundary}\r\n".b
70
+ body << "Content-Disposition: form-data; name=\"#{FILE_FIELD}\"; " \
71
+ "filename=\"#{quote(filename)}\"\r\n".b
72
+ body << "Content-Type: application/octet-stream\r\n\r\n".b
73
+ body << io.read.b
74
+ body << "\r\n".b
75
+ end
76
+
77
+ body << "--#{boundary}--\r\n".b
78
+ body
79
+ end
80
+
81
+ # Quotes are structural in a Content-Disposition header and newlines would
82
+ # let a filename inject headers of its own.
83
+ def quote(value)
84
+ value.to_s.gsub(/["\r\n]/, "_")
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "model"
4
+
5
+ module Audd
6
+ # Mirror of the upstream Apple Music track payload, passed through verbatim
7
+ # by AudD. Field names stay camelCase to match the JSON keys.
8
+ class AppleMusicMetadata < Model
9
+ field :previews
10
+ field :artwork
11
+ field :artist_name, "artistName"
12
+ field :url
13
+ field :disc_number, "discNumber"
14
+ field :genre_names, "genreNames"
15
+ field :duration_in_millis, "durationInMillis"
16
+ field :release_date, "releaseDate"
17
+ field :name
18
+ field :isrc
19
+ field :album_name, "albumName"
20
+ field :play_params, "playParams"
21
+ field :track_number, "trackNumber"
22
+ field :composer_name, "composerName"
23
+ end
24
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "model"
4
+
5
+ module Audd
6
+ # Mirror of the upstream Deezer track payload. Absent for some tracks.
7
+ class DeezerMetadata < Model
8
+ field :id
9
+ field :readable
10
+ field :title
11
+ field :title_short
12
+ field :title_version
13
+ field :link
14
+ field :duration
15
+ field :rank
16
+ field :explicit_lyrics
17
+ field :explicit_content_lyrics
18
+ field :explicit_content_cover
19
+ field :preview
20
+ field :md5_image
21
+ field :artist
22
+ field :album
23
+ field :type
24
+ end
25
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "model"
4
+
5
+ module Audd
6
+ # Envelope returned with `status: "error"`. ResponseParser turns one of these
7
+ # into the matching Audd::ApiError subclass; it is exposed because the
8
+ # exception hands the parsed envelope back on #raw_response.
9
+ class ErrorResponse < Model
10
+ field :status
11
+ field :error
12
+ # The standard endpoint names this `request_params`; the enterprise
13
+ # endpoint names the same thing `requested_params`. Normalized in #initialize.
14
+ field :request_params
15
+ field :request_api_method
16
+ field :request_http_method
17
+ field :execution_time
18
+ field :result
19
+
20
+ def initialize(raw)
21
+ super
22
+ @request_params ||= raw["requested_params"]
23
+ end
24
+
25
+ def error_code
26
+ dig_error("error_code")
27
+ end
28
+
29
+ def error_message
30
+ dig_error("error_message")
31
+ end
32
+
33
+ # Codes 19 and 31337 may carry a "branded" artist/title instead of a real
34
+ # result. It is deliberately not surfaced as a recognition result.
35
+ def branded_message
36
+ return nil unless result.is_a?(Hash)
37
+
38
+ parts = [result["artist"], result["title"]].compact.map(&:to_s).reject(&:empty?)
39
+ parts.empty? ? nil : parts.join(" — ")
40
+ end
41
+
42
+ private
43
+
44
+ # `error` is documented as an object, but a bare string shows up in the
45
+ # wild; surface it as the message rather than blowing up on #[].
46
+ def dig_error(key)
47
+ case error
48
+ when Hash then error[key]
49
+ when String then (key == "error_message") ? error : nil
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Audd
4
+ # Base for the response objects. Subclasses declare the fields their OpenAPI
5
+ # schema documents; every instance also keeps the untouched `raw` hash,
6
+ # because those schemas are all `additionalProperties: true` — Apple, Spotify
7
+ # and friends ship new keys without warning and AudD passes them through
8
+ # verbatim. Reach unmapped keys with `result["some_new_key"]`.
9
+ class Model
10
+ class << self
11
+ # Declares a reader backed by `key` in the raw payload. An optional block
12
+ # casts the raw value (used to nest sub-models).
13
+ def field(name, key = name.to_s, &cast)
14
+ fields[name] = [key, cast]
15
+ attr_reader(name)
16
+ end
17
+
18
+ def fields
19
+ @fields ||= superclass.respond_to?(:fields) ? superclass.fields.dup : {}
20
+ end
21
+
22
+ # Anything that isn't an object (`null`, mostly) is absence, not an error.
23
+ def from_json(raw)
24
+ new(raw) if raw.is_a?(Hash)
25
+ end
26
+ end
27
+
28
+ attr_reader :raw
29
+
30
+ def initialize(raw)
31
+ @raw = raw
32
+ self.class.fields.each do |name, (key, cast)|
33
+ value = raw[key]
34
+ value = cast.call(value) if cast && !value.nil?
35
+ instance_variable_set(:"@#{name}", value)
36
+ end
37
+ end
38
+
39
+ def [](key)
40
+ raw[key.to_s]
41
+ end
42
+
43
+ def to_h
44
+ raw
45
+ end
46
+
47
+ def ==(other)
48
+ other.instance_of?(self.class) && other.raw == raw
49
+ end
50
+ alias_method :eql?, :==
51
+
52
+ def hash
53
+ [self.class, raw].hash
54
+ end
55
+
56
+ # Only the fields that are actually present — these payloads are wide and
57
+ # mostly empty, and a full dump buries the useful bits.
58
+ def inspect
59
+ populated = self.class.fields.keys.filter_map do |name|
60
+ value = public_send(name)
61
+ "#{name}=#{value.inspect}" unless value.nil?
62
+ end
63
+ "#<#{self.class.name} #{populated.join(" ")}>"
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "model"
4
+
5
+ module Audd
6
+ # A single MusicBrainz recording match. AudD returns an array of these,
7
+ # because MusicBrainz matching often surfaces several candidates.
8
+ class MusicBrainzEntry < Model
9
+ field :id
10
+ # MusicBrainz sometimes returns score as a string, sometimes as an integer.
11
+ field :score
12
+ field :title
13
+ field :length
14
+ field :disambiguation
15
+ field :video
16
+ field :artist_credit, "artist-credit"
17
+ field :releases
18
+ field :isrcs
19
+ field :tags
20
+ end
21
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "uri"
4
+
5
+ require_relative "model"
6
+ require_relative "apple_music_metadata"
7
+ require_relative "deezer_metadata"
8
+ require_relative "music_brainz_entry"
9
+ require_relative "spotify_metadata"
10
+
11
+ module Audd
12
+ # A single recognition result. Which fields are populated depends on which
13
+ # catalog matched:
14
+ #
15
+ # * public-DB match — +artist+, +title+, +album+, +release_date+, +label+
16
+ # and +song_link+ are populated and +audio_id+ is absent;
17
+ # * custom-DB match (your own fingerprint catalog) — only +timecode+ and
18
+ # +audio_id+ are populated.
19
+ #
20
+ # Use #public_match? / #custom_match? rather than testing +artist+ for nil.
21
+ class RecognitionResult < Model
22
+ # Only lis.tn-hosted song links serve cover art from `?thumb`.
23
+ THUMBNAIL_HOST = "lis.tn"
24
+
25
+ # Position in the recognized song (MM:SS) at which the matched fragment plays.
26
+ field :timecode
27
+ field :audio_id
28
+ field :artist
29
+ field :title
30
+ field :album
31
+ field :release_date
32
+ field :label
33
+ field :song_link
34
+ field(:apple_music) { |v| AppleMusicMetadata.from_json(v) }
35
+ field(:spotify) { |v| SpotifyMetadata.from_json(v) }
36
+ field(:deezer) { |v| DeezerMetadata.from_json(v) }
37
+ field(:musicbrainz) { |v| Array(v).filter_map { |e| MusicBrainzEntry.from_json(e) } }
38
+
39
+ # Matched against your own fingerprint catalog rather than the public DB.
40
+ def custom_match?
41
+ !audio_id.nil?
42
+ end
43
+
44
+ def public_match?
45
+ audio_id.nil?
46
+ end
47
+
48
+ # Cover art for the match, or nil when there's no song_link to build it
49
+ # from — appending ?thumb only means anything on lis.tn.
50
+ def thumbnail_url
51
+ return nil if song_link.nil?
52
+ return nil unless URI.parse(song_link).host == THUMBNAIL_HOST
53
+
54
+ "#{song_link}?thumb"
55
+ rescue URI::InvalidURIError
56
+ nil
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "model"
4
+ require_relative "recognition_result"
5
+
6
+ module Audd
7
+ # Envelope returned by POST api.audd.io/ on success. `result` is nil when the
8
+ # audio was processed fine but nothing matched — that is a successful
9
+ # response, not an error.
10
+ class RecognizeSuccessResponse < Model
11
+ field :status
12
+ field(:result) { |v| RecognitionResult.from_json(v) }
13
+
14
+ def match?
15
+ !result.nil?
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "model"
4
+
5
+ module Audd
6
+ # Mirror of the upstream Spotify track payload.
7
+ class SpotifyMetadata < Model
8
+ field :album
9
+ field :artists
10
+ field :available_markets
11
+ field :disc_number
12
+ field :duration_ms
13
+ field :explicit
14
+ field :external_ids
15
+ field :external_urls
16
+ field :href
17
+ field :id
18
+ field :is_local
19
+ field :is_playable
20
+ field :linked_from
21
+ field :name
22
+ field :popularity
23
+ field :track_number
24
+ field :type
25
+ field :uri
26
+ end
27
+ end
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "errors"
4
+ require_relative "models/recognize_success_response"
5
+ require_relative "response_parser"
6
+ require_relative "source"
7
+
8
+ module Audd
9
+ # Chainable builder for POST api.audd.io/. Built by Audd::Client#recognize;
10
+ # nothing is sent until #execute!.
11
+ #
12
+ # audd.recognize(file: "clip.mp3")
13
+ # .return_metadata(:apple_music, :spotify)
14
+ # .market("us")
15
+ # .execute!
16
+ class RecognizeRequest
17
+ RETURN_METADATA_SOURCES = %w[apple_music spotify deezer musicbrainz].freeze
18
+
19
+ # The server holds the connection open while it fingerprints the audio,
20
+ # so this is generous by HTTP standards.
21
+ DEFAULT_TIMEOUT = 60
22
+
23
+ attr_reader :file, :url
24
+
25
+ def initialize(client:, file: nil, url: nil, timeout: DEFAULT_TIMEOUT)
26
+ if file.nil? == url.nil?
27
+ raise InvalidParameterError,
28
+ "recognize requires exactly one of file: or url:, got " \
29
+ "#{file.nil? ? "neither" : "both"}."
30
+ end
31
+
32
+ @client = client
33
+ @file = file
34
+ @url = url
35
+ @timeout = timeout
36
+ @return_metadata = []
37
+ @market = nil
38
+ end
39
+
40
+ # Extra metadata to fetch for the match, e.g. :apple_music, :spotify.
41
+ # Repeated calls accumulate.
42
+ def return_metadata(*sources)
43
+ sources = sources.flatten.map(&:to_s)
44
+ unknown = sources - RETURN_METADATA_SOURCES
45
+
46
+ unless unknown.empty?
47
+ raise InvalidParameterError,
48
+ "unknown return_metadata source(s): #{unknown.join(", ")}. " \
49
+ "Valid sources: #{RETURN_METADATA_SOURCES.join(", ")}."
50
+ end
51
+
52
+ @return_metadata |= sources
53
+ self
54
+ end
55
+
56
+ # ISO country code for the regional Apple Music / Spotify catalogs.
57
+ def market(code)
58
+ @market = code.to_s
59
+ self
60
+ end
61
+
62
+ # Overrides DEFAULT_TIMEOUT for this request, in seconds.
63
+ def timeout(seconds)
64
+ @timeout = seconds
65
+ self
66
+ end
67
+
68
+ # Sends the request.
69
+ #
70
+ # @return [Audd::RecognitionResult, nil] nil when the audio was processed
71
+ # but nothing matched — a successful response, not an error.
72
+ def execute!
73
+ body = Source.with_file(@file) do |file_part|
74
+ response = @client.post("/", fields: form_fields, timeout: @timeout, file: file_part)
75
+ ResponseParser.decode!(response)
76
+ end
77
+
78
+ RecognizeSuccessResponse.new(body).result
79
+ end
80
+
81
+ # The multipart fields this request will send, minus the api_token the
82
+ # client adds.
83
+ def form_fields
84
+ fields = {}
85
+ fields["url"] = @url if @url
86
+ fields["return"] = @return_metadata.join(",") unless @return_metadata.empty?
87
+ fields["market"] = @market if @market
88
+ fields
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,96 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ require_relative "errors"
6
+ require_relative "models/error_response"
7
+
8
+ module Audd
9
+ # Turns a raw HTTP response into a decoded success body, or raises the
10
+ # matching Audd::Error subclass.
11
+ module ResponseParser
12
+ HTTP_CLIENT_ERROR_FLOOR = 400
13
+
14
+ # Code 51 is a soft deprecation: the server fulfilled the request but is
15
+ # warning about a parameter. When a result came back with it, warn and
16
+ # carry on rather than raising.
17
+ DEPRECATED_PARAMS_CODE = 51
18
+
19
+ class << self
20
+ # @return [Hash] the parsed `status: "success"` body
21
+ def decode!(response)
22
+ body = parse_json(response)
23
+ return handle_deprecation(body) if deprecation_pass_through?(body)
24
+
25
+ case body["status"]
26
+ when "success" then body
27
+ when "error" then raise_api_error(body, response)
28
+ else
29
+ raise ServerError.new(
30
+ error_code: 0,
31
+ error_message: "Unexpected response status: #{body["status"].inspect}",
32
+ http_status: response.status,
33
+ request_id: response.request_id,
34
+ raw_response: body
35
+ )
36
+ end
37
+ end
38
+
39
+ private
40
+
41
+ def parse_json(response)
42
+ body = begin
43
+ JSON.parse(response.body.to_s)
44
+ rescue JSON::ParserError
45
+ nil
46
+ end
47
+ return body if body.is_a?(Hash)
48
+
49
+ # A non-JSON body on a failing status is the proxy or gateway talking,
50
+ # not AudD — report it as a server error and keep the status code.
51
+ if response.status >= HTTP_CLIENT_ERROR_FLOOR
52
+ raise ServerError.new(
53
+ error_code: 0,
54
+ error_message: "HTTP #{response.status} with non-JSON response body",
55
+ http_status: response.status,
56
+ request_id: response.request_id,
57
+ raw_response: response.body
58
+ )
59
+ end
60
+
61
+ raise SerializationError.new(
62
+ "Could not parse the AudD response as JSON",
63
+ raw_body: response.body.to_s
64
+ )
65
+ end
66
+
67
+ def deprecation_pass_through?(body)
68
+ error = body["error"]
69
+ error.is_a?(Hash) &&
70
+ error["error_code"] == DEPRECATED_PARAMS_CODE &&
71
+ !body["result"].nil?
72
+ end
73
+
74
+ def handle_deprecation(body)
75
+ warn("[audd] #{body.dig("error", "error_message")}")
76
+ body.except("error").merge("status" => "success")
77
+ end
78
+
79
+ def raise_api_error(body, response)
80
+ envelope = ErrorResponse.new(body)
81
+ code = Integer(envelope.error_code, exception: false) || 0
82
+
83
+ raise Audd.error_class_for(code).new(
84
+ error_code: code,
85
+ error_message: envelope.error_message.to_s,
86
+ http_status: response.status,
87
+ request_id: response.request_id,
88
+ request_params: envelope.request_params,
89
+ request_method: envelope.request_api_method,
90
+ branded_message: envelope.branded_message,
91
+ raw_response: envelope
92
+ )
93
+ end
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "pathname"
4
+
5
+ require_relative "errors"
6
+
7
+ module Audd
8
+ # Normalizes whatever the caller passed as `file:` into the [filename, io]
9
+ # pair HttpClient wants.
10
+ module Source
11
+ FALLBACK_FILENAME = "upload.bin"
12
+
13
+ # Yields [filename, io], closing only handles we opened ourselves — a
14
+ # caller-supplied IO stays the caller's to manage.
15
+ def self.with_file(file)
16
+ return yield(nil) if file.nil?
17
+
18
+ if file.respond_to?(:read)
19
+ name = file.respond_to?(:path) ? File.basename(file.path) : FALLBACK_FILENAME
20
+ yield [name, file]
21
+ else
22
+ path = Pathname.new(file.to_s)
23
+ unless path.file?
24
+ raise InvalidParameterError,
25
+ "file: #{file.inspect} is not a readable file. Pass a path, a " \
26
+ "Pathname, or an open IO — or use url: to have AudD fetch it."
27
+ end
28
+ path.open("rb") { |io| yield [path.basename.to_s, io] }
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Audd
4
+ VERSION = "0.2.0"
5
+ end
data/lib/audd.rb ADDED
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "audd/version"
4
+ require_relative "audd/errors"
5
+
6
+ require_relative "audd/models/model"
7
+ require_relative "audd/models/apple_music_metadata"
8
+ require_relative "audd/models/deezer_metadata"
9
+ require_relative "audd/models/music_brainz_entry"
10
+ require_relative "audd/models/spotify_metadata"
11
+ require_relative "audd/models/recognition_result"
12
+ require_relative "audd/models/recognize_success_response"
13
+ require_relative "audd/models/error_response"
14
+
15
+ require_relative "audd/http_client"
16
+ require_relative "audd/response_parser"
17
+ require_relative "audd/source"
18
+ require_relative "audd/recognize_request"
19
+ require_relative "audd/client"
20
+
21
+ # Unofficial Ruby SDK for the AudD music recognition API.
22
+ #
23
+ # audd = Audd.new("your_token")
24
+ # result = audd.recognize(file: "clip.mp3").execute!
25
+ # result&.title
26
+ #
27
+ # See https://docs.audd.io/ for the API itself.
28
+ module Audd
29
+ class API
30
+ API_BASE = "https://api.audd.io"
31
+ ENTERPRISE_BASE = "https://enterprise.audd.io"
32
+
33
+ def initialize(api_token)
34
+ @api_token = api_token
35
+ end
36
+
37
+ def recognize(file: nil, url: nil)
38
+ client = Client.new(api_token:, base_url: API_BASE)
39
+
40
+ RecognizeRequest.new(client: client, file: file, url: url)
41
+ end
42
+
43
+ def recognize_enterprise(file: nil, url: nil)
44
+ raise NotImplementedError
45
+ end
46
+
47
+ def find_lyrics(query)
48
+ raise NotImplementedError
49
+ end
50
+
51
+ def inspect
52
+ "#<#{self.class.name} api_token=[FILTERED]>"
53
+ end
54
+
55
+ private
56
+
57
+ attr_reader :api_token
58
+ end
59
+
60
+ def self.new(api_token)
61
+ if api_token.nil?
62
+ raise ConfigurationError, "No AudD api_token given. Get one at https://dashboard.audd.io."
63
+ end
64
+
65
+ API.new(api_token)
66
+ end
67
+ end
data/sig/audd.rbs ADDED
@@ -0,0 +1,4 @@
1
+ module Audd
2
+ VERSION: String
3
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
4
+ end
metadata ADDED
@@ -0,0 +1,67 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: audd
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.2.0
5
+ platform: ruby
6
+ authors:
7
+ - Rowan McDonald
8
+ bindir: exe
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies: []
12
+ description: SDK for AudD music recognition software.
13
+ email:
14
+ - rowjaim@gmail.com
15
+ executables: []
16
+ extensions: []
17
+ extra_rdoc_files: []
18
+ files:
19
+ - ".rspec"
20
+ - ".standard.yml"
21
+ - CHANGELOG.md
22
+ - CODE_OF_CONDUCT.md
23
+ - LICENSE.txt
24
+ - README.md
25
+ - Rakefile
26
+ - lib/audd.rb
27
+ - lib/audd/client.rb
28
+ - lib/audd/errors.rb
29
+ - lib/audd/http_client.rb
30
+ - lib/audd/models/apple_music_metadata.rb
31
+ - lib/audd/models/deezer_metadata.rb
32
+ - lib/audd/models/error_response.rb
33
+ - lib/audd/models/model.rb
34
+ - lib/audd/models/music_brainz_entry.rb
35
+ - lib/audd/models/recognition_result.rb
36
+ - lib/audd/models/recognize_success_response.rb
37
+ - lib/audd/models/spotify_metadata.rb
38
+ - lib/audd/recognize_request.rb
39
+ - lib/audd/response_parser.rb
40
+ - lib/audd/source.rb
41
+ - lib/audd/version.rb
42
+ - sig/audd.rbs
43
+ homepage: https://github.com/RowanMcDonald/audd
44
+ licenses:
45
+ - MIT
46
+ metadata:
47
+ homepage_uri: https://github.com/RowanMcDonald/audd
48
+ source_code_uri: https://github.com/RowanMcDonald/audd
49
+ changelog_uri: https://github.com/RowanMcDonald/audd/blob/main/CHANGELOG.md
50
+ rdoc_options: []
51
+ require_paths:
52
+ - lib
53
+ required_ruby_version: !ruby/object:Gem::Requirement
54
+ requirements:
55
+ - - ">="
56
+ - !ruby/object:Gem::Version
57
+ version: 3.1.0
58
+ required_rubygems_version: !ruby/object:Gem::Requirement
59
+ requirements:
60
+ - - ">="
61
+ - !ruby/object:Gem::Version
62
+ version: '0'
63
+ requirements: []
64
+ rubygems_version: 3.6.9
65
+ specification_version: 4
66
+ summary: SDK for AudD music recognition software.
67
+ test_files: []