bandcamp-discover 0.5.0 → 0.6.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/Gemfile.lock +1 -1
- data/README.rdoc +20 -0
- data/lib/bandcamp-discover/analyzer.rb +42 -42
- data/lib/bandcamp-discover/configuration.rb +40 -0
- data/lib/bandcamp-discover/roster.rb +35 -0
- data/lib/bandcamp-discover/scrapers/base.rb +7 -1
- data/lib/bandcamp-discover/scrapers/label.rb +18 -4
- data/lib/bandcamp-discover/scrapers/music.rb +39 -10
- data/lib/bandcamp-discover/version.rb +1 -1
- data/test/analyzer_test.rb +106 -0
- data/test/roster_test.rb +52 -0
- metadata +6 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 0d06bbc06b837c9d82502a0d9e9a61d3d2062af141f2a19e28ff0cb3e7e3fe5d
|
|
4
|
+
data.tar.gz: 2b2bbe5682ed207b8911f31a74e1a1a0b8bf471326509b35a3849527b39bcab7
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 70c78eccc21549dd9c08fe183ec8ddf6a11060d79e913891ebca47de23f01248a07e7b73e27ea5fb45ba463ea93469d98f8be7f7fdcf5e99576600d5f2992c2a
|
|
7
|
+
data.tar.gz: d345ad2550cd0ea344d0310a2aa8bdf351a5a3b08b999a19395092e7669ba07b9d98d9ea18a96b7480c3e5d4e44774922db943d115c1ca066ab1592cc058ac6c
|
data/Gemfile.lock
CHANGED
data/README.rdoc
CHANGED
|
@@ -2,5 +2,25 @@
|
|
|
2
2
|
|
|
3
3
|
Describe your project here
|
|
4
4
|
|
|
5
|
+
== Configuration
|
|
6
|
+
|
|
7
|
+
When the +open_router+ gem is loaded and has an access token, +Analyzer+
|
|
8
|
+
asks a model whether a Bandcamp bio belongs to a label and whether it
|
|
9
|
+
accepts demos. The model and both prompts can be set by the host app:
|
|
10
|
+
|
|
11
|
+
BandcampDiscover.configure do |config|
|
|
12
|
+
config.model = "anthropic/claude-3.5-haiku" # default: "openrouter/auto"
|
|
13
|
+
config.label_prompt = "..." # must ask for {"answer": true|false}
|
|
14
|
+
config.demos_prompt = "..."
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
Before the bio is asked, the /music grid is read: a page whose releases carry
|
|
18
|
+
no artist credit other than the owner is an artist, not a label, and is
|
|
19
|
+
rejected without a model call. The credits that remain are appended to the
|
|
20
|
+
bio for the model and returned as +artists+ in the scrape result.
|
|
21
|
+
|
|
22
|
+
Without a token, +label?+ falls back to matching /label|platform|records/i
|
|
23
|
+
and +accepts_demos?+ is always false.
|
|
24
|
+
|
|
5
25
|
:include:bandcamp-discover.rdoc
|
|
6
26
|
|
|
@@ -1,54 +1,54 @@
|
|
|
1
|
+
require "json"
|
|
2
|
+
require_relative "configuration"
|
|
3
|
+
|
|
1
4
|
module BandcampDiscover
|
|
2
5
|
class Analyzer
|
|
3
|
-
|
|
6
|
+
# A per-call model still wins over the configured one so existing callers
|
|
7
|
+
# keep their behaviour.
|
|
8
|
+
def initialize(description, model = nil, credits: [])
|
|
4
9
|
@description = description
|
|
5
|
-
@
|
|
10
|
+
@credits = credits
|
|
11
|
+
@model = model || BandcampDiscover.configuration.model
|
|
6
12
|
end
|
|
7
13
|
|
|
8
14
|
def label?
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
{ role: "system", content: "You are given a description that could or could not be that of a record label. Analyze and answer witha JSON object {\"answer\": true|false}. Be critical: Individuals and bands are not labels, but collectives can be labels." },
|
|
13
|
-
{ role: "user", content: @description }
|
|
14
|
-
],
|
|
15
|
-
model: [
|
|
16
|
-
@model
|
|
17
|
-
],
|
|
18
|
-
extras: {
|
|
19
|
-
response_format: {
|
|
20
|
-
type: "json_object"
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
)
|
|
24
|
-
|
|
25
|
-
JSON.parse(response["choices"][0]["message"]["content"])["answer"]&.to_s&.downcase == "true"
|
|
26
|
-
else
|
|
27
|
-
@description.match? /label|platform|records/i
|
|
28
|
-
end
|
|
15
|
+
return ask(BandcampDiscover.configuration.label_prompt) if llm?
|
|
16
|
+
|
|
17
|
+
@description.to_s.match?(/label|platform|records/i)
|
|
29
18
|
end
|
|
30
19
|
|
|
31
20
|
def accepts_demos?
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
21
|
+
return false unless llm?
|
|
22
|
+
|
|
23
|
+
ask(BandcampDiscover.configuration.demos_prompt)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
def llm?
|
|
29
|
+
defined?(OpenRouter) && !!OpenRouter.configuration.access_token
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def ask(prompt)
|
|
33
|
+
response = OpenRouter::Client.new.complete(
|
|
34
|
+
[
|
|
35
|
+
{role: "system", content: prompt},
|
|
36
|
+
{role: "user", content: message}
|
|
37
|
+
],
|
|
38
|
+
model: [@model],
|
|
39
|
+
extras: {response_format: {type: "json_object"}}
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
JSON.parse(response["choices"][0]["message"]["content"])["answer"]&.to_s&.downcase == "true"
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# The bio alone cannot tell one person releasing under aliases from a
|
|
46
|
+
# roster; who the releases are credited to is the other half of the answer.
|
|
47
|
+
def message
|
|
48
|
+
return @description.to_s if @credits.empty?
|
|
49
|
+
|
|
50
|
+
"#{@description}\n\nReleases on this page are credited to #{@credits.size} " \
|
|
51
|
+
"#{(@credits.size == 1) ? "artist" : "artists"} other than the page owner: #{@credits.join(", ")}."
|
|
52
52
|
end
|
|
53
53
|
end
|
|
54
54
|
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
module BandcampDiscover
|
|
2
|
+
# The prompts decide what enters a catalogue, and tuning them used to mean a
|
|
3
|
+
# gem release. Hosting apps set them once; the defaults keep every existing
|
|
4
|
+
# caller behaving as before.
|
|
5
|
+
class Configuration
|
|
6
|
+
DEFAULT_MODEL = "openrouter/auto"
|
|
7
|
+
|
|
8
|
+
DEFAULT_LABEL_PROMPT = "You are given a description that could or could not be that of a record label. " \
|
|
9
|
+
"Analyze and answer with a JSON object {\"answer\": true|false}. " \
|
|
10
|
+
"Be critical: Individuals and bands are not labels, but collectives can be labels."
|
|
11
|
+
|
|
12
|
+
DEFAULT_DEMOS_PROMPT = "You are given a description of a record label or music platform. " \
|
|
13
|
+
"Analyze if they accept demo submissions from artists. Look for mentions of 'demos', 'demo submissions', " \
|
|
14
|
+
"'send demos', 'submit music', 'accepting submissions', contact information for demos, or similar language. " \
|
|
15
|
+
"Be critical, and default to false. Only answer with true when you are certain that the label accepts demos. " \
|
|
16
|
+
"Answer with a JSON object {\"answer\": true|false}."
|
|
17
|
+
|
|
18
|
+
attr_accessor :model, :label_prompt, :demos_prompt
|
|
19
|
+
|
|
20
|
+
def initialize
|
|
21
|
+
@model = DEFAULT_MODEL
|
|
22
|
+
@label_prompt = DEFAULT_LABEL_PROMPT
|
|
23
|
+
@demos_prompt = DEFAULT_DEMOS_PROMPT
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
class << self
|
|
28
|
+
def configuration
|
|
29
|
+
@configuration ||= Configuration.new
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def configure
|
|
33
|
+
yield configuration
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def reset_configuration!
|
|
37
|
+
@configuration = nil
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
module BandcampDiscover
|
|
2
|
+
# A label's /music grid credits each release to its artist; an artist's own
|
|
3
|
+
# page credits nothing, because the artist is the page. That is the one
|
|
4
|
+
# structural difference between the two, and it costs no API call.
|
|
5
|
+
#
|
|
6
|
+
# It only ever says "no": one person releasing under aliases looks like a
|
|
7
|
+
# roster, and a bio can say "not a label" over a grid full of credits, so a
|
|
8
|
+
# positive still has to come from the bio.
|
|
9
|
+
class Roster
|
|
10
|
+
attr_reader :band_name, :releases, :credits
|
|
11
|
+
|
|
12
|
+
def initialize(band_name:, credits:, releases: credits.size)
|
|
13
|
+
@band_name = band_name.to_s.strip
|
|
14
|
+
@releases = releases
|
|
15
|
+
@credits = credits.compact.map(&:strip).reject { |credit| credit.empty? || own?(credit) }.uniq
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# A single self-released record is not evidence either way.
|
|
19
|
+
def solo?
|
|
20
|
+
credits.empty? && releases >= 2
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
# "Sean Woosley" and "Woosley Band" on woosley.bandcamp.com are the owner,
|
|
26
|
+
# not a roster.
|
|
27
|
+
def own?(credit)
|
|
28
|
+
a = credit.downcase
|
|
29
|
+
b = band_name.downcase
|
|
30
|
+
return false if b.empty?
|
|
31
|
+
|
|
32
|
+
a.include?(b) || b.include?(a)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -15,7 +15,13 @@ module BandcampDiscover
|
|
|
15
15
|
end
|
|
16
16
|
|
|
17
17
|
def scrape(force: false)
|
|
18
|
-
yield @page if block_given?
|
|
18
|
+
guarded { yield @page if block_given? }
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
def guarded
|
|
24
|
+
yield
|
|
19
25
|
rescue Playwright::TimeoutError => e
|
|
20
26
|
raise ScrapeError, "Timed out waiting for an element on #{@url}: #{e.message}"
|
|
21
27
|
end
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
require_relative "
|
|
2
|
-
require_relative "
|
|
1
|
+
require_relative "base"
|
|
2
|
+
require_relative "music"
|
|
3
3
|
require_relative "../analyzer"
|
|
4
4
|
|
|
5
5
|
module BandcampDiscover
|
|
@@ -16,9 +16,13 @@ module BandcampDiscover
|
|
|
16
16
|
name = band_name_location_container.query_selector(".title").inner_text
|
|
17
17
|
location = band_name_location_container.query_selector(".location").inner_text
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
music = Scrapers::Music.new(url: "#{@url}/music", browser: @browser, max_tasks: @max_tasks)
|
|
20
|
+
grid = music.grid
|
|
21
|
+
roster = music.roster(grid, band_name: name)
|
|
22
|
+
|
|
23
|
+
if force || label?(roster, bio_text&.inner_html)
|
|
20
24
|
return Sync do
|
|
21
|
-
albums, music_tags =
|
|
25
|
+
albums, music_tags = music.albums(grid)
|
|
22
26
|
|
|
23
27
|
puts "done scraping #{@url}"
|
|
24
28
|
|
|
@@ -27,6 +31,7 @@ module BandcampDiscover
|
|
|
27
31
|
name: name,
|
|
28
32
|
location: location,
|
|
29
33
|
bio: bio_text.inner_text,
|
|
34
|
+
artists: roster.credits,
|
|
30
35
|
tags_with_weights: music_tags&.compact,
|
|
31
36
|
albums: albums
|
|
32
37
|
}
|
|
@@ -37,6 +42,15 @@ module BandcampDiscover
|
|
|
37
42
|
end
|
|
38
43
|
end
|
|
39
44
|
end
|
|
45
|
+
|
|
46
|
+
private
|
|
47
|
+
|
|
48
|
+
# The grid can rule a page out for free; only the rest costs a model call.
|
|
49
|
+
def label?(roster, bio)
|
|
50
|
+
return false if roster.solo?
|
|
51
|
+
|
|
52
|
+
Analyzer.new(bio, credits: roster.credits).label?
|
|
53
|
+
end
|
|
40
54
|
end
|
|
41
55
|
end
|
|
42
56
|
end
|
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
require_relative "base"
|
|
2
2
|
require_relative "album"
|
|
3
|
+
require_relative "../roster"
|
|
3
4
|
require "async"
|
|
4
5
|
require "async/semaphore"
|
|
5
6
|
|
|
6
7
|
module BandcampDiscover
|
|
7
8
|
module Scrapers
|
|
8
9
|
class Music < Base
|
|
10
|
+
MAX_ALBUMS = 20
|
|
11
|
+
|
|
9
12
|
def initialize(url:, browser:, max_tasks:)
|
|
10
13
|
super
|
|
11
14
|
|
|
@@ -14,28 +17,48 @@ module BandcampDiscover
|
|
|
14
17
|
end
|
|
15
18
|
|
|
16
19
|
def scrape(force: false)
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
20
|
+
albums(grid)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# The grid alone tells a label from an artist (see Roster), and it is one
|
|
24
|
+
# page load against the twenty behind albums, so callers can look at it
|
|
25
|
+
# before paying for the rest.
|
|
26
|
+
def grid
|
|
27
|
+
guarded do
|
|
28
|
+
@page.goto(@url)
|
|
29
|
+
items = @page.wait_for_selector("#music-grid").query_selector_all("li.music-grid-item")
|
|
30
|
+
|
|
31
|
+
items.map do |item|
|
|
32
|
+
{
|
|
33
|
+
url: absolute(item.query_selector("a")[:href]),
|
|
34
|
+
credit: item.query_selector(".artist-override")&.inner_text
|
|
35
|
+
}
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def roster(grid, band_name:)
|
|
41
|
+
Roster.new(band_name: band_name, credits: grid.map { _1[:credit] }, releases: grid.size)
|
|
42
|
+
end
|
|
21
43
|
|
|
44
|
+
def albums(grid)
|
|
45
|
+
guarded do
|
|
22
46
|
semaphore = Async::Semaphore.new(@max_tasks)
|
|
23
47
|
|
|
24
|
-
albums =
|
|
48
|
+
albums = grid.take(MAX_ALBUMS).map do |item|
|
|
25
49
|
semaphore.async do
|
|
26
|
-
|
|
27
|
-
puts "starting to scrape #{url}"
|
|
50
|
+
puts "starting to scrape #{item[:url]}"
|
|
28
51
|
|
|
29
|
-
album = Scrapers::Album.new(url: url, browser: @browser).scrape
|
|
52
|
+
album = Scrapers::Album.new(url: item[:url], browser: @browser).scrape
|
|
30
53
|
|
|
31
|
-
puts "done scraping #{url}"
|
|
54
|
+
puts "done scraping #{item[:url]}"
|
|
32
55
|
|
|
33
56
|
album
|
|
34
57
|
end
|
|
35
58
|
end.map(&:wait)
|
|
36
59
|
|
|
37
60
|
albums.map! do |album_url, album_title, album_tags, album_player|
|
|
38
|
-
{
|
|
61
|
+
{url: album_url, title: album_title, tags: album_tags, player_url: album_player}
|
|
39
62
|
end
|
|
40
63
|
|
|
41
64
|
[albums, normalize_tally(albums.map { _1[:tags] }.flatten.tally)]
|
|
@@ -47,6 +70,12 @@ module BandcampDiscover
|
|
|
47
70
|
tally.transform_values! { |count| count / total }
|
|
48
71
|
tally.sort_by { |k, v| v }.reverse.to_h
|
|
49
72
|
end
|
|
73
|
+
|
|
74
|
+
private
|
|
75
|
+
|
|
76
|
+
def absolute(href)
|
|
77
|
+
href.start_with?("https://") ? href : "#{@base_url}#{href}"
|
|
78
|
+
end
|
|
50
79
|
end
|
|
51
80
|
end
|
|
52
81
|
end
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
require_relative "test_helper"
|
|
2
|
+
require "bandcamp-discover/analyzer"
|
|
3
|
+
|
|
4
|
+
# OpenRouter is the host app's dependency, not this gem's, so a stand-in that
|
|
5
|
+
# records the request is enough to prove what the Analyzer sends.
|
|
6
|
+
module OpenRouter
|
|
7
|
+
Configuration = Struct.new(:access_token)
|
|
8
|
+
|
|
9
|
+
def self.configuration
|
|
10
|
+
@configuration ||= Configuration.new
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
class Client
|
|
14
|
+
class << self
|
|
15
|
+
attr_accessor :requests, :answer
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def complete(messages, model:, extras:)
|
|
19
|
+
self.class.requests << {messages: messages, model: model, extras: extras}
|
|
20
|
+
{"choices" => [{"message" => {"content" => JSON.generate("answer" => self.class.answer)}}]}
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
class AnalyzerTest < Minitest::Test
|
|
26
|
+
Analyzer = BandcampDiscover::Analyzer
|
|
27
|
+
|
|
28
|
+
def setup
|
|
29
|
+
BandcampDiscover.reset_configuration!
|
|
30
|
+
OpenRouter.configuration.access_token = "token"
|
|
31
|
+
OpenRouter::Client.requests = []
|
|
32
|
+
OpenRouter::Client.answer = true
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def teardown
|
|
36
|
+
BandcampDiscover.reset_configuration!
|
|
37
|
+
OpenRouter.configuration.access_token = nil
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def test_defaults_match_the_prompts_and_model_shipped_so_far
|
|
41
|
+
Analyzer.new("We release tapes.").label?
|
|
42
|
+
|
|
43
|
+
request = OpenRouter::Client.requests.last
|
|
44
|
+
assert_equal ["openrouter/auto"], request[:model]
|
|
45
|
+
assert_includes request[:messages].first[:content], "Individuals and bands are not labels"
|
|
46
|
+
assert_equal "We release tapes.", request[:messages].last[:content]
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def test_configured_prompt_and_model_are_sent
|
|
50
|
+
BandcampDiscover.configure do |config|
|
|
51
|
+
config.model = "anthropic/claude-3.5-haiku"
|
|
52
|
+
config.label_prompt = "Is this a label? Answer {\"answer\": true|false}."
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
Analyzer.new("bio").label?
|
|
56
|
+
|
|
57
|
+
request = OpenRouter::Client.requests.last
|
|
58
|
+
assert_equal ["anthropic/claude-3.5-haiku"], request[:model]
|
|
59
|
+
assert_equal "Is this a label? Answer {\"answer\": true|false}.", request[:messages].first[:content]
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def test_per_call_model_wins_over_the_configured_one
|
|
63
|
+
BandcampDiscover.configure { |config| config.model = "configured/model" }
|
|
64
|
+
|
|
65
|
+
Analyzer.new("bio", "explicit/model").label?
|
|
66
|
+
|
|
67
|
+
assert_equal ["explicit/model"], OpenRouter::Client.requests.last[:model]
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def test_demos_prompt_is_configurable_too
|
|
71
|
+
BandcampDiscover.configure { |config| config.demos_prompt = "Demos?" }
|
|
72
|
+
|
|
73
|
+
Analyzer.new("bio").accepts_demos?
|
|
74
|
+
|
|
75
|
+
assert_equal "Demos?", OpenRouter::Client.requests.last[:messages].first[:content]
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def test_credits_are_appended_to_the_bio
|
|
79
|
+
Analyzer.new("We run a tape label.", credits: ["Helen", "Cate Kennan"]).label?
|
|
80
|
+
|
|
81
|
+
message = OpenRouter::Client.requests.last[:messages].last[:content]
|
|
82
|
+
assert_equal "We run a tape label.\n\nReleases on this page are credited to 2 artists other than the page owner: Helen, Cate Kennan.", message
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def test_no_credits_sends_the_bio_alone
|
|
86
|
+
Analyzer.new(nil, credits: []).label?
|
|
87
|
+
|
|
88
|
+
assert_equal "", OpenRouter::Client.requests.last[:messages].last[:content]
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def test_parses_the_answer
|
|
92
|
+
OpenRouter::Client.answer = false
|
|
93
|
+
|
|
94
|
+
refute Analyzer.new("bio").label?
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def test_falls_back_to_the_regex_without_a_token
|
|
98
|
+
OpenRouter.configuration.access_token = nil
|
|
99
|
+
|
|
100
|
+
assert Analyzer.new("An independent label from Graz").label?
|
|
101
|
+
refute Analyzer.new("Solo musician").label?
|
|
102
|
+
refute Analyzer.new(nil).label?
|
|
103
|
+
refute Analyzer.new("Send demos to ...").accepts_demos?
|
|
104
|
+
assert_empty OpenRouter::Client.requests
|
|
105
|
+
end
|
|
106
|
+
end
|
data/test/roster_test.rb
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
require_relative "test_helper"
|
|
2
|
+
require "bandcamp-discover/roster"
|
|
3
|
+
|
|
4
|
+
# Cases are real pages from the discover.bandcamp.labels similarity audit.
|
|
5
|
+
class RosterTest < Minitest::Test
|
|
6
|
+
Roster = BandcampDiscover::Roster
|
|
7
|
+
|
|
8
|
+
def test_a_label_credits_other_artists
|
|
9
|
+
roster = Roster.new(band_name: "kranky", credits: ["Helen", "Cate Kennan", "Ana Roxanne", "Helen"])
|
|
10
|
+
|
|
11
|
+
refute roster.solo?
|
|
12
|
+
assert_equal ["Helen", "Cate Kennan", "Ana Roxanne"], roster.credits
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def test_an_artist_page_credits_nobody
|
|
16
|
+
roster = Roster.new(band_name: "Radiohead", credits: [nil] * 15)
|
|
17
|
+
|
|
18
|
+
assert roster.solo?
|
|
19
|
+
assert_empty roster.credits
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def test_credits_naming_the_owner_are_not_a_roster
|
|
23
|
+
roster = Roster.new(band_name: "Woosley", credits: ["Sean Woosley", "Woosley Band", nil, nil, nil])
|
|
24
|
+
|
|
25
|
+
assert roster.solo?
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def test_owner_match_is_case_insensitive_and_either_direction
|
|
29
|
+
roster = Roster.new(band_name: "Soul Juice", credits: ["J Sand (Soul Juice)", "Geechie Dan", "soul juice"])
|
|
30
|
+
|
|
31
|
+
assert_equal ["Geechie Dan"], roster.credits
|
|
32
|
+
refute roster.solo?
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def test_a_single_self_release_is_not_evidence
|
|
36
|
+
roster = Roster.new(band_name: "New Label", credits: [nil])
|
|
37
|
+
|
|
38
|
+
refute roster.solo?
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def test_releases_can_outnumber_rendered_credits
|
|
42
|
+
roster = Roster.new(band_name: "Whoever", credits: [], releases: 12)
|
|
43
|
+
|
|
44
|
+
assert roster.solo?
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def test_blank_band_name_keeps_every_credit
|
|
48
|
+
roster = Roster.new(band_name: nil, credits: [" A ", "", "B"])
|
|
49
|
+
|
|
50
|
+
assert_equal ["A", "B"], roster.credits
|
|
51
|
+
end
|
|
52
|
+
end
|
metadata
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: bandcamp-discover
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.6.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Julian RUbisch
|
|
8
8
|
bindir: bin
|
|
9
9
|
cert_chain: []
|
|
10
|
-
date: 2026-
|
|
10
|
+
date: 2026-09-03 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
13
|
name: rake
|
|
@@ -195,14 +195,18 @@ files:
|
|
|
195
195
|
- bin/bandcamp-discover
|
|
196
196
|
- lib/bandcamp-discover.rb
|
|
197
197
|
- lib/bandcamp-discover/analyzer.rb
|
|
198
|
+
- lib/bandcamp-discover/configuration.rb
|
|
199
|
+
- lib/bandcamp-discover/roster.rb
|
|
198
200
|
- lib/bandcamp-discover/scrapers/album.rb
|
|
199
201
|
- lib/bandcamp-discover/scrapers/base.rb
|
|
200
202
|
- lib/bandcamp-discover/scrapers/discover.rb
|
|
201
203
|
- lib/bandcamp-discover/scrapers/label.rb
|
|
202
204
|
- lib/bandcamp-discover/scrapers/music.rb
|
|
203
205
|
- lib/bandcamp-discover/version.rb
|
|
206
|
+
- test/analyzer_test.rb
|
|
204
207
|
- test/default_test.rb
|
|
205
208
|
- test/discover_test.rb
|
|
209
|
+
- test/roster_test.rb
|
|
206
210
|
- test/test_helper.rb
|
|
207
211
|
homepage: https://julianrubisch.at
|
|
208
212
|
licenses: []
|