metal_archives 2.2.3 → 3.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 +4 -4
- data/.github/workflows/ci.yml +59 -12
- data/.rspec +1 -0
- data/.rubocop.yml +34 -20
- data/CHANGELOG.md +16 -1
- data/LICENSE.md +17 -4
- data/README.md +37 -29
- data/bin/console +8 -11
- data/config/inflections.rb +7 -0
- data/config/initializers/.keep +0 -0
- data/docker-compose.yml +10 -1
- data/lib/metal_archives.rb +57 -21
- data/lib/metal_archives/cache/base.rb +40 -0
- data/lib/metal_archives/cache/memory.rb +68 -0
- data/lib/metal_archives/cache/null.rb +22 -0
- data/lib/metal_archives/cache/redis.rb +49 -0
- data/lib/metal_archives/collection.rb +3 -5
- data/lib/metal_archives/configuration.rb +28 -21
- data/lib/metal_archives/errors.rb +9 -1
- data/lib/metal_archives/http_client.rb +42 -46
- data/lib/metal_archives/models/artist.rb +55 -26
- data/lib/metal_archives/models/band.rb +43 -36
- data/lib/metal_archives/models/{base_model.rb → base.rb} +57 -50
- data/lib/metal_archives/models/label.rb +7 -8
- data/lib/metal_archives/models/release.rb +21 -18
- data/lib/metal_archives/parsers/artist.rb +41 -36
- data/lib/metal_archives/parsers/band.rb +73 -29
- data/lib/metal_archives/parsers/base.rb +14 -0
- data/lib/metal_archives/parsers/country.rb +21 -0
- data/lib/metal_archives/parsers/date.rb +31 -0
- data/lib/metal_archives/parsers/genre.rb +67 -0
- data/lib/metal_archives/parsers/label.rb +21 -13
- data/lib/metal_archives/parsers/parser.rb +17 -77
- data/lib/metal_archives/parsers/release.rb +29 -18
- data/lib/metal_archives/parsers/year.rb +31 -0
- data/lib/metal_archives/version.rb +3 -3
- data/metal_archives.env.example +7 -4
- data/metal_archives.gemspec +7 -4
- data/nginx/default.conf +2 -2
- metadata +76 -32
- data/.github/workflows/release.yml +0 -69
- data/.rubocop_todo.yml +0 -92
- data/lib/metal_archives/lru_cache.rb +0 -61
- data/lib/metal_archives/middleware/cache_check.rb +0 -18
- data/lib/metal_archives/middleware/encoding.rb +0 -16
- data/lib/metal_archives/middleware/headers.rb +0 -38
- data/lib/metal_archives/middleware/rewrite_endpoint.rb +0 -38
- data/lib/metal_archives/nil_date.rb +0 -91
- data/lib/metal_archives/range.rb +0 -69
data/lib/metal_archives.rb
CHANGED
@@ -1,33 +1,49 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require "openssl"
|
4
|
-
|
5
3
|
require "zeitwerk"
|
6
|
-
|
7
|
-
|
8
|
-
loader.inflector.inflect(
|
9
|
-
"id" => "ID",
|
10
|
-
"api" => "API",
|
11
|
-
"http_client" => "HTTPClient",
|
12
|
-
"lru_cache" => "LRUCache"
|
13
|
-
)
|
14
|
-
loader.collapse("lib/metal_archives/models")
|
15
|
-
loader.setup
|
4
|
+
require "byebug" if ENV["METAL_ARCHIVES_ENV"] == "development"
|
5
|
+
require "active_support/all"
|
16
6
|
|
17
7
|
##
|
18
8
|
# Metal Archives Ruby API
|
19
9
|
#
|
20
10
|
module MetalArchives
|
21
11
|
class << self
|
12
|
+
# Code loader instance
|
13
|
+
attr_reader :loader
|
14
|
+
|
15
|
+
##
|
16
|
+
# Root path
|
17
|
+
#
|
18
|
+
def root
|
19
|
+
@root ||= Pathname.new(File.expand_path(File.join("..", ".."), __FILE__))
|
20
|
+
end
|
21
|
+
|
22
|
+
##
|
23
|
+
# HTTP client
|
24
|
+
#
|
25
|
+
def http
|
26
|
+
@http ||= HTTPClient.new
|
27
|
+
end
|
28
|
+
|
22
29
|
##
|
23
30
|
# API configuration
|
24
31
|
#
|
25
32
|
# Instance of rdoc-ref:MetalArchives::Configuration
|
26
33
|
#
|
27
34
|
def config
|
28
|
-
|
35
|
+
@config ||= Configuration.new
|
36
|
+
end
|
29
37
|
|
30
|
-
|
38
|
+
##
|
39
|
+
# Cache instance
|
40
|
+
#
|
41
|
+
def cache
|
42
|
+
raise MetalArchives::Errors::InvalidConfigurationError, "cache has not been configured" unless config.cache_strategy
|
43
|
+
|
44
|
+
@cache ||= Cache
|
45
|
+
.const_get(loader.inflector.camelize(config.cache_strategy, root))
|
46
|
+
.new(config.cache_options)
|
31
47
|
end
|
32
48
|
|
33
49
|
##
|
@@ -40,17 +56,37 @@ module MetalArchives
|
|
40
56
|
# - rdoc-ref:InvalidConfigurationException
|
41
57
|
#
|
42
58
|
def configure
|
43
|
-
raise
|
59
|
+
raise Errors::InvalidConfigurationError, "no configuration block given" unless block_given?
|
44
60
|
|
45
|
-
|
46
|
-
yield @config
|
61
|
+
yield config
|
47
62
|
|
48
|
-
|
49
|
-
|
63
|
+
config.validate!
|
64
|
+
end
|
50
65
|
|
51
|
-
|
66
|
+
##
|
67
|
+
# Set up application framework
|
68
|
+
#
|
69
|
+
def setup
|
70
|
+
@loader = Zeitwerk::Loader.for_gem
|
52
71
|
|
53
|
-
|
72
|
+
# Register inflections
|
73
|
+
require root.join("config/inflections.rb")
|
74
|
+
|
75
|
+
# Set up code loader
|
76
|
+
loader.enable_reloading if ENV["METAL_ARCHIVES_ENV"] == "development"
|
77
|
+
loader.collapse(root.join("lib/metal_archives/models"))
|
78
|
+
loader.do_not_eager_load(root.join("lib/metal_archives/cache"))
|
79
|
+
loader.setup
|
80
|
+
loader.eager_load
|
81
|
+
|
82
|
+
# Load initializers
|
83
|
+
Dir[root.join("config/initializers/*.rb")].sort.each { |f| require f }
|
54
84
|
end
|
55
85
|
end
|
56
86
|
end
|
87
|
+
|
88
|
+
def reload!
|
89
|
+
MetalArchives.loader.reload
|
90
|
+
end
|
91
|
+
|
92
|
+
MetalArchives.setup
|
@@ -0,0 +1,40 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module MetalArchives
|
4
|
+
module Cache
|
5
|
+
##
|
6
|
+
# Generic cache interface
|
7
|
+
#
|
8
|
+
class Base
|
9
|
+
attr_accessor :options
|
10
|
+
|
11
|
+
def initialize(options = {})
|
12
|
+
@options = options
|
13
|
+
|
14
|
+
validate!
|
15
|
+
end
|
16
|
+
|
17
|
+
def validate!; end
|
18
|
+
|
19
|
+
def []
|
20
|
+
raise NotImplementedError
|
21
|
+
end
|
22
|
+
|
23
|
+
def []=(_key, _value)
|
24
|
+
raise NotImplementedError
|
25
|
+
end
|
26
|
+
|
27
|
+
def clear
|
28
|
+
raise NotImplementedError
|
29
|
+
end
|
30
|
+
|
31
|
+
def include?(_key)
|
32
|
+
raise NotImplementedError
|
33
|
+
end
|
34
|
+
|
35
|
+
def delete(_key)
|
36
|
+
raise NotImplementedError
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module MetalArchives
|
4
|
+
module Cache
|
5
|
+
##
|
6
|
+
# Generic LRU memory cache
|
7
|
+
#
|
8
|
+
class Memory < Base
|
9
|
+
def validate!
|
10
|
+
raise Errors::InvalidConfigurationError, "size has not been configured" if options[:size].blank?
|
11
|
+
raise Errors::InvalidConfigurationError, "size must be a number" unless options[:size].is_a? Integer
|
12
|
+
end
|
13
|
+
|
14
|
+
def [](key)
|
15
|
+
if keys.include? key
|
16
|
+
MetalArchives.config.logger.debug "Cache hit for #{key}"
|
17
|
+
keys.delete key
|
18
|
+
keys << key
|
19
|
+
else
|
20
|
+
MetalArchives.config.logger.debug "Cache miss for #{key}"
|
21
|
+
end
|
22
|
+
|
23
|
+
cache[key]
|
24
|
+
end
|
25
|
+
|
26
|
+
def []=(key, value)
|
27
|
+
cache[key] = value
|
28
|
+
|
29
|
+
keys.delete key if keys.include? key
|
30
|
+
|
31
|
+
keys << key
|
32
|
+
|
33
|
+
pop if keys.size > options[:size]
|
34
|
+
end
|
35
|
+
|
36
|
+
def clear
|
37
|
+
cache.clear
|
38
|
+
keys.clear
|
39
|
+
end
|
40
|
+
|
41
|
+
def include?(key)
|
42
|
+
cache.include? key
|
43
|
+
end
|
44
|
+
|
45
|
+
def delete(key)
|
46
|
+
cache.delete key
|
47
|
+
end
|
48
|
+
|
49
|
+
private
|
50
|
+
|
51
|
+
def cache
|
52
|
+
# Underlying data store
|
53
|
+
@cache ||= {}
|
54
|
+
end
|
55
|
+
|
56
|
+
def keys
|
57
|
+
# Array of keys in order of insertion
|
58
|
+
@keys ||= []
|
59
|
+
end
|
60
|
+
|
61
|
+
def pop
|
62
|
+
to_remove = keys.shift(keys.size - options[:size])
|
63
|
+
|
64
|
+
to_remove.each { |key| cache.delete key }
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module MetalArchives
|
4
|
+
module Cache
|
5
|
+
##
|
6
|
+
# Null cache
|
7
|
+
#
|
8
|
+
class Null < Base
|
9
|
+
def [](_key); end
|
10
|
+
|
11
|
+
def []=(_key, _value); end
|
12
|
+
|
13
|
+
def clear; end
|
14
|
+
|
15
|
+
def include?(_key)
|
16
|
+
false
|
17
|
+
end
|
18
|
+
|
19
|
+
def delete(_key); end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "redis"
|
4
|
+
|
5
|
+
module MetalArchives
|
6
|
+
module Cache
|
7
|
+
##
|
8
|
+
# Redis-backed cache
|
9
|
+
#
|
10
|
+
class Redis < Base
|
11
|
+
def initialize(options = {})
|
12
|
+
super
|
13
|
+
|
14
|
+
# Default TTL is 1 month
|
15
|
+
options[:ttl] ||= (30 * 24 * 60 * 60)
|
16
|
+
end
|
17
|
+
|
18
|
+
def [](key)
|
19
|
+
redis.get cache_key_for(key)
|
20
|
+
end
|
21
|
+
|
22
|
+
def []=(key, value)
|
23
|
+
redis.set cache_key_for(key), value, ex: options[:ttl]
|
24
|
+
end
|
25
|
+
|
26
|
+
def clear
|
27
|
+
redis.keys(cache_key_for("*")).each { |key| redis.del key }
|
28
|
+
end
|
29
|
+
|
30
|
+
def include?(key)
|
31
|
+
redis.exists? cache_key_for(key)
|
32
|
+
end
|
33
|
+
|
34
|
+
def delete(key)
|
35
|
+
redis.del cache_key_for(key)
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
def cache_key_for(key)
|
41
|
+
"metal_archives.cache.#{key}"
|
42
|
+
end
|
43
|
+
|
44
|
+
def redis
|
45
|
+
@redis ||= ::Redis.new(**options.except(:ttl))
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -22,15 +22,13 @@ module MetalArchives
|
|
22
22
|
# Calls the given block once for each element, passing that element as a parameter.
|
23
23
|
# If no block is given, an Enumerator is returned.
|
24
24
|
#
|
25
|
-
def each
|
26
|
-
return to_enum :each unless
|
25
|
+
def each(&block)
|
26
|
+
return to_enum :each unless block
|
27
27
|
|
28
28
|
loop do
|
29
29
|
items = instance_exec(&@proc)
|
30
30
|
|
31
|
-
items.each
|
32
|
-
yield item
|
33
|
-
end
|
31
|
+
items.each(&block)
|
34
32
|
|
35
33
|
break if items.empty?
|
36
34
|
end
|
@@ -3,6 +3,8 @@
|
|
3
3
|
require "logger"
|
4
4
|
|
5
5
|
module MetalArchives
|
6
|
+
CACHE_STRATEGIES = %w(memory redis).freeze
|
7
|
+
|
6
8
|
##
|
7
9
|
# Contains configuration options
|
8
10
|
#
|
@@ -23,10 +25,9 @@ module MetalArchives
|
|
23
25
|
attr_accessor :app_contact
|
24
26
|
|
25
27
|
##
|
26
|
-
# Override Metal Archives endpoint (defaults to
|
28
|
+
# Override Metal Archives endpoint (defaults to https://www.metal-archives.com/)
|
27
29
|
#
|
28
30
|
attr_accessor :endpoint
|
29
|
-
attr_reader :default_endpoint
|
30
31
|
|
31
32
|
##
|
32
33
|
# Endpoint HTTP Basic authentication
|
@@ -35,39 +36,45 @@ module MetalArchives
|
|
35
36
|
attr_accessor :endpoint_password
|
36
37
|
|
37
38
|
##
|
38
|
-
#
|
39
|
+
# Logger instance
|
39
40
|
#
|
40
|
-
attr_accessor :
|
41
|
+
attr_accessor :logger
|
41
42
|
|
42
43
|
##
|
43
|
-
#
|
44
|
+
# Cache strategy
|
44
45
|
#
|
45
|
-
attr_accessor :
|
46
|
+
attr_accessor :cache_strategy
|
46
47
|
|
47
48
|
##
|
48
|
-
#
|
49
|
+
# Cache strategy options
|
49
50
|
#
|
50
|
-
attr_accessor :
|
51
|
+
attr_accessor :cache_options
|
51
52
|
|
52
53
|
##
|
53
|
-
#
|
54
|
+
# Default configuration values
|
54
55
|
#
|
55
|
-
|
56
|
+
def initialize(**attributes)
|
57
|
+
attributes.each { |key, value| send(:"#{key}=", value) }
|
56
58
|
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
59
|
+
@endpoint ||= "https://www.metal-archives.com/"
|
60
|
+
@logger ||= Logger.new $stdout
|
61
|
+
|
62
|
+
@cache_strategy ||= "memory"
|
63
|
+
@cache_options ||= { size: 100 }
|
64
|
+
end
|
61
65
|
|
62
66
|
##
|
63
|
-
#
|
67
|
+
# Validate configuration
|
68
|
+
#
|
69
|
+
# [Raises]
|
70
|
+
# - rdoc-ref:MetalArchives::Errors::ConfigurationError when configuration is invalid
|
64
71
|
#
|
65
|
-
def
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
72
|
+
def validate!
|
73
|
+
raise Errors::InvalidConfigurationError, "app_name has not been configured" if app_name.blank?
|
74
|
+
raise Errors::InvalidConfigurationError, "app_version has not been configured" if app_version.blank?
|
75
|
+
raise Errors::InvalidConfigurationError, "app_contact has not been configured" if app_contact.blank?
|
76
|
+
raise Errors::InvalidConfigurationError, "cache_strategy has not been configured" if cache_strategy.blank?
|
77
|
+
raise Errors::InvalidConfigurationError, "cache_strategy must be one of: #{CACHE_STRATEGIES.join(', ')}" if CACHE_STRATEGIES.exclude?(cache_strategy.to_s)
|
71
78
|
end
|
72
79
|
end
|
73
80
|
end
|
@@ -32,7 +32,15 @@ module MetalArchives
|
|
32
32
|
##
|
33
33
|
# Error in backend response
|
34
34
|
#
|
35
|
-
class APIError < Error
|
35
|
+
class APIError < Error
|
36
|
+
attr_reader :code
|
37
|
+
|
38
|
+
def initialize(response)
|
39
|
+
super("#{response.reason}: #{response.body}")
|
40
|
+
|
41
|
+
@code = response.code
|
42
|
+
end
|
43
|
+
end
|
36
44
|
|
37
45
|
##
|
38
46
|
# Error in method argument
|
@@ -1,66 +1,62 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require "
|
4
|
-
require "faraday_throttler"
|
3
|
+
require "http"
|
5
4
|
|
6
5
|
module MetalArchives
|
7
6
|
##
|
8
|
-
# HTTP
|
7
|
+
# Generic HTTP client
|
9
8
|
#
|
10
|
-
class HTTPClient
|
11
|
-
|
12
|
-
##
|
13
|
-
# Retrieve a HTTP resource
|
14
|
-
#
|
15
|
-
# [Raises]
|
16
|
-
# - rdoc-ref:MetalArchives::Errors::InvalidIDError when receiving a status code == 404n
|
17
|
-
# - rdoc-ref:MetalArchives::Errors::APIError when receiving a status code >= 400 (except 404)
|
18
|
-
#
|
19
|
-
def get(*params)
|
20
|
-
response = client.get(*params)
|
9
|
+
class HTTPClient
|
10
|
+
attr_reader :endpoint, :metrics
|
21
11
|
|
22
|
-
|
23
|
-
|
12
|
+
def initialize(endpoint = MetalArchives.config.endpoint)
|
13
|
+
@endpoint = endpoint
|
14
|
+
@metrics = { hit: 0, miss: 0 }
|
15
|
+
end
|
24
16
|
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
raise Errors::APIError, e
|
29
|
-
end
|
17
|
+
def get(path, params = {})
|
18
|
+
response = http
|
19
|
+
.get(url_for(path), params: params)
|
30
20
|
|
31
|
-
|
21
|
+
# Log cache status
|
22
|
+
status = response.headers["x-cache-status"]&.downcase&.to_sym
|
23
|
+
MetalArchives.config.logger.info "Cache #{status} for #{path}" if status
|
32
24
|
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
25
|
+
case status
|
26
|
+
when :hit
|
27
|
+
metrics[:hit] += 1
|
28
|
+
when :miss, :bypass, :expired, :stale, :updating, :revalidated
|
29
|
+
metrics[:miss] += 1
|
30
|
+
end
|
31
|
+
raise Errors::InvalidIDError, response if response.code == 404
|
32
|
+
raise Errors::APIError, response unless response.status.success?
|
39
33
|
|
40
|
-
|
41
|
-
|
42
|
-
f.response :logger, MetalArchives.config.logger
|
34
|
+
response
|
35
|
+
end
|
43
36
|
|
44
|
-
|
45
|
-
f.use MetalArchives::Middleware::CacheCheck
|
46
|
-
f.use MetalArchives::Middleware::RewriteEndpoint
|
47
|
-
f.use MetalArchives::Middleware::Encoding
|
37
|
+
private
|
48
38
|
|
49
|
-
|
39
|
+
def http
|
40
|
+
@http ||= HTTP
|
41
|
+
.headers(headers)
|
42
|
+
.use(logging: { logger: MetalArchives.config.logger })
|
43
|
+
.encoding("utf-8")
|
50
44
|
|
51
|
-
|
52
|
-
rate: MetalArchives.config.request_rate,
|
53
|
-
wait: MetalArchives.config.request_timeout,
|
54
|
-
logger: MetalArchives.config.logger
|
45
|
+
return @http unless MetalArchives.config.endpoint_user && MetalArchives.config.endpoint_password
|
55
46
|
|
56
|
-
|
47
|
+
@http
|
48
|
+
.basic_auth(user: MetalArchives.config.endpoint_user, pass: MetalArchives.config.endpoint_password)
|
49
|
+
end
|
57
50
|
|
58
|
-
|
51
|
+
def headers
|
52
|
+
{
|
53
|
+
user_agent: "#{MetalArchives.config.app_name}/#{MetalArchives.config.app_version} (#{MetalArchives.config.app_contact})",
|
54
|
+
accept: "application/json",
|
55
|
+
}
|
56
|
+
end
|
59
57
|
|
60
|
-
|
61
|
-
|
62
|
-
end
|
63
|
-
end
|
58
|
+
def url_for(path)
|
59
|
+
"#{endpoint}#{path}"
|
64
60
|
end
|
65
61
|
end
|
66
62
|
end
|