sources: add artist profile links to fetch source data box.
Add site icons linking to all the artist's sites in the fetch source data box. Some artist entries have a large number of URLs. Various heuristics are applied to try to present the most useful URLs first. Dead URLs and redundant URLs (Pixiv stacc and Twitter intent URLs) are filtered out. Remaining URLs are sorted first by site (to put sites like Pixiv and Twitter first), then by URL (to break ties when an artist has multiple accounts on the same site). Some sites have shitty hard-to-read icons. It can't be helped. The icons are the official favicons of each site.
@@ -2,9 +2,15 @@
|
|||||||
|
|
||||||
class SourceDataComponent < ApplicationComponent
|
class SourceDataComponent < ApplicationComponent
|
||||||
attr_reader :source
|
attr_reader :source
|
||||||
delegate :spinner_icon, to: :helpers
|
delegate :spinner_icon, :external_site_icon, to: :helpers
|
||||||
|
|
||||||
def initialize(source:)
|
def initialize(source:)
|
||||||
@source = source
|
@source = source
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def profile_urls(artist)
|
||||||
|
artist.urls.active.reject(&:secondary_url?).sort_by do |artist_url|
|
||||||
|
[artist_url.priority, artist_url.domain, artist_url.url]
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -3,46 +3,55 @@
|
|||||||
<%= spinner_icon class: "source-data-loading" %>
|
<%= spinner_icon class: "source-data-loading" %>
|
||||||
|
|
||||||
<% if @source.present? %>
|
<% if @source.present? %>
|
||||||
<dl class="source-data-content">
|
<table class="source-data-content mt-2">
|
||||||
<div class="source-data-artist">
|
<tbody>
|
||||||
<dt>Artist</dt>
|
<% if @source.artist_name.blank? %>
|
||||||
<dd>
|
<tr>
|
||||||
<% if @source.artist_name.blank? %>
|
<th>Artist</th>
|
||||||
<em>None</em>
|
<td><em>None</em></td>
|
||||||
<% else %>
|
</tr>
|
||||||
<%= external_link_to @source.profile_url, @source.artist_name, class: "source-data-artist-profile" %>
|
<% elsif @source.artists.empty? %>
|
||||||
|
<tr>
|
||||||
|
<th>Artist</th>
|
||||||
|
<td>
|
||||||
|
<%= external_link_to @source.profile_url, @source.artist_name %>
|
||||||
|
(<%= link_to "Create new artist", new_artist_path(artist: { source: @source.canonical_url }) %>)
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<% else %>
|
||||||
|
<% @source.artists.each do |artist| %>
|
||||||
|
<tr>
|
||||||
|
<th>Artist</th>
|
||||||
|
<td>
|
||||||
|
<%= link_to artist.name, artist_path(artist), class: tag_class(artist.tag) %>
|
||||||
|
|
||||||
<% if @source.artists.empty? %>
|
<ul class="list-inline">
|
||||||
(<%= link_to "Create new artist", new_artist_path(artist: { source: @source.canonical_url }), class: "source-data-create-new-artist" %>)
|
<% profile_urls(artist).each do |artist_url| %>
|
||||||
|
<%= external_link_to artist_url.url, external_site_icon(artist_url.site_name), title: artist_url.url %>
|
||||||
|
<% end %>
|
||||||
|
</ul>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<% end %>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
<tr class="source-data-tags">
|
||||||
|
<th>Tags</th>
|
||||||
|
<td>
|
||||||
|
<% if @source.tags.empty? %>
|
||||||
|
<em>None</em>
|
||||||
<% else %>
|
<% else %>
|
||||||
(<ul class="source-data-translated-artists">
|
<ul class="list-inline">
|
||||||
<% @source.artists.each do |artist| %>
|
<% @source.tags.each do |tag, href| %>
|
||||||
<li><%= link_to artist.name, artist_path(artist), class: tag_class(artist.tag) %></li>
|
<li>
|
||||||
|
<%= external_link_to href, tag, class: "source-data-tag" %>
|
||||||
|
</li>
|
||||||
<% end %>
|
<% end %>
|
||||||
</ul>)
|
</ul>
|
||||||
<% end %>
|
<% end %>
|
||||||
<% end %>
|
</td>
|
||||||
</dd>
|
</tr>
|
||||||
</div>
|
</tbody>
|
||||||
|
</table>
|
||||||
<div class="source-data-tags">
|
|
||||||
<dt>Tags</dt>
|
|
||||||
<dd>
|
|
||||||
<% if @source.tags.empty? %>
|
|
||||||
<em>None</em>
|
|
||||||
<% else %>
|
|
||||||
<ul>
|
|
||||||
<% @source.tags.each do |tag, href| %>
|
|
||||||
<li><%= external_link_to href, tag %></li>
|
|
||||||
<% end %>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<% if @source.image_urls.length > 1 %>
|
|
||||||
<p class="source-data-gallery-warning">Gallery. Tags may not apply to all images.</p>
|
|
||||||
<% end %>
|
|
||||||
<% end %>
|
|
||||||
</dd>
|
|
||||||
</li>
|
|
||||||
</dl>
|
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -6,15 +6,27 @@ div.source-data {
|
|||||||
&.loading .source-data-content { display: none; }
|
&.loading .source-data-content { display: none; }
|
||||||
&.loading .source-data-fetch { display: none; }
|
&.loading .source-data-fetch { display: none; }
|
||||||
|
|
||||||
ul {
|
th {
|
||||||
|
padding-right: 1rem;
|
||||||
|
padding-bottom: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
td {
|
||||||
|
vertical-align: top;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
height: 1rem;
|
||||||
|
font-size: 1rem;
|
||||||
|
vertical-align: bottom;
|
||||||
|
}
|
||||||
|
|
||||||
|
.source-data-tag {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
}
|
background-color: var(--wiki-page-other-name-background-color);
|
||||||
|
padding: 0 0.25rem;
|
||||||
dt, dd, li {
|
margin-right: 0.25rem;
|
||||||
display: inline;
|
margin-bottom: 0.25rem;
|
||||||
}
|
border-radius: 0.25rem;
|
||||||
|
|
||||||
dt, .source-data-tags li {
|
|
||||||
margin-right: 1em;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,11 @@ module IconHelper
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def image_icon_tag(filename, class: nil, **options)
|
||||||
|
klass = binding.local_variable_get(:class)
|
||||||
|
tag.img(src: "/images/#{filename}", class: "icon #{klass}", **options)
|
||||||
|
end
|
||||||
|
|
||||||
# fontawesome.com/icons/arrow-alt-up
|
# fontawesome.com/icons/arrow-alt-up
|
||||||
def upvote_icon(**options)
|
def upvote_icon(**options)
|
||||||
svg_icon_tag("upvote-icon", "M272 480h-96c-13.3 0-24-10.7-24-24V256H48.2c-21.4 0-32.1-25.8-17-41L207 39c9.4-9.4 24.6-9.4 34 0l175.8 176c15.1 15.1 4.4 41-17 41H296v200c0 13.3-10.7 24-24 24z", **options)
|
svg_icon_tag("upvote-icon", "M272 480h-96c-13.3 0-24-10.7-24-24V256H48.2c-21.4 0-32.1-25.8-17-41L207 39c9.4-9.4 24.6-9.4 34 0l175.8 176c15.1 15.1 4.4 41-17 41H296v200c0 13.3-10.7 24-24 24z", **options)
|
||||||
@@ -148,4 +153,83 @@ module IconHelper
|
|||||||
def plus_icon(**options)
|
def plus_icon(**options)
|
||||||
icon_tag("fas fa-plus", **options)
|
icon_tag("fas fa-plus", **options)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def globe_icon(**options)
|
||||||
|
icon_tag("fas fa-globe", **options)
|
||||||
|
end
|
||||||
|
|
||||||
|
def discord_icon(**options)
|
||||||
|
image_icon_tag("discord-logo.png", **options)
|
||||||
|
end
|
||||||
|
|
||||||
|
def github_icon(**options)
|
||||||
|
image_icon_tag("github-logo.png", **options)
|
||||||
|
end
|
||||||
|
|
||||||
|
def twitter_icon(**options)
|
||||||
|
image_icon_tag("twitter-logo.png", **options)
|
||||||
|
end
|
||||||
|
|
||||||
|
def external_site_icon(site_name, **options)
|
||||||
|
case site_name
|
||||||
|
when "ArtStation"
|
||||||
|
image_icon_tag("artstation-logo.png", **options)
|
||||||
|
when "BCY"
|
||||||
|
image_icon_tag("bcy-logo.png", **options)
|
||||||
|
when "Booth.pm"
|
||||||
|
image_icon_tag("booth-pm-logo.png", **options)
|
||||||
|
when "Circle.ms"
|
||||||
|
image_icon_tag("circle-ms-logo.png", **options)
|
||||||
|
when "DLSite"
|
||||||
|
image_icon_tag("dlsite-logo.png", **options)
|
||||||
|
when "Deviant Art"
|
||||||
|
image_icon_tag("deviantart-logo.png", **options)
|
||||||
|
when "Facebook"
|
||||||
|
image_icon_tag("facebook-logo.png", **options)
|
||||||
|
when "Fantia"
|
||||||
|
image_icon_tag("fantia-logo.png", **options)
|
||||||
|
when "FC2"
|
||||||
|
image_icon_tag("fc2-logo.png", **options)
|
||||||
|
when "Gumroad"
|
||||||
|
image_icon_tag("gumroad-logo.png", **options)
|
||||||
|
when "Instagram"
|
||||||
|
image_icon_tag("instagram-logo.png", **options)
|
||||||
|
when "Lofter"
|
||||||
|
image_icon_tag("lofter-logo.png", **options)
|
||||||
|
when "Melonbooks"
|
||||||
|
image_icon_tag("melonbooks-logo.png", **options)
|
||||||
|
when "Nico Seiga"
|
||||||
|
image_icon_tag("nicoseiga-logo.png", **options)
|
||||||
|
when "Nijie"
|
||||||
|
image_icon_tag("nijie-logo.png", **options)
|
||||||
|
when "Patreon"
|
||||||
|
image_icon_tag("patreon-logo.png", **options)
|
||||||
|
when "pawoo.net"
|
||||||
|
image_icon_tag("pawoo-logo.png", **options)
|
||||||
|
when "Pixiv"
|
||||||
|
image_icon_tag("pixiv-logo.png", **options)
|
||||||
|
when "Pixiv Fanbox"
|
||||||
|
image_icon_tag("pixiv-fanbox-logo.png", **options)
|
||||||
|
when "Pixiv Sketch"
|
||||||
|
image_icon_tag("pixiv-sketch-logo.png", **options)
|
||||||
|
when "Privatter"
|
||||||
|
image_icon_tag("privatter-logo.png", **options)
|
||||||
|
when "Skeb"
|
||||||
|
image_icon_tag("skeb-logo.png", **options)
|
||||||
|
when "Tinami"
|
||||||
|
image_icon_tag("tinami-logo.png", **options)
|
||||||
|
when "Tumblr"
|
||||||
|
image_icon_tag("tumblr-logo.png", **options)
|
||||||
|
when "Twitter"
|
||||||
|
image_icon_tag("twitter-logo.png", **options)
|
||||||
|
when "Toranoana"
|
||||||
|
image_icon_tag("toranoana-logo.png", **options)
|
||||||
|
when "Weibo"
|
||||||
|
image_icon_tag("weibo-logo.png", **options)
|
||||||
|
when "Youtube"
|
||||||
|
image_icon_tag("youtube-logo.png", **options)
|
||||||
|
else
|
||||||
|
globe_icon(**options)
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -60,7 +60,47 @@ module Sources
|
|||||||
end
|
end
|
||||||
|
|
||||||
def site_name
|
def site_name
|
||||||
Addressable::URI.heuristic_parse(url)&.host
|
host = Addressable::URI.heuristic_parse(url)&.host
|
||||||
|
|
||||||
|
# XXX should go in dedicated strategies.
|
||||||
|
case host
|
||||||
|
when /bcy\.net\z/i
|
||||||
|
"BCY"
|
||||||
|
when /booth\.pm\z/i
|
||||||
|
"Booth.pm"
|
||||||
|
when /circle\.ms\z/i
|
||||||
|
"Circle.ms"
|
||||||
|
when /dlsite\.(com|net)\z/i
|
||||||
|
"DLSite"
|
||||||
|
when /facebook\.com\z/i
|
||||||
|
"Facebook"
|
||||||
|
when /fantia\.jp\z/i
|
||||||
|
"Fantia"
|
||||||
|
when /fc2\.com\z/i
|
||||||
|
"FC2"
|
||||||
|
when /gumroad\.com\z/i
|
||||||
|
"Gumroad"
|
||||||
|
when /instagram\.com\z/i
|
||||||
|
"Instagram"
|
||||||
|
when /lofter\.com\z/i
|
||||||
|
"Lofter"
|
||||||
|
when /melonbooks\.co\.jp\z/i
|
||||||
|
"Melonbooks"
|
||||||
|
when /patreon\.com\z/i
|
||||||
|
"Patreon"
|
||||||
|
when /privatter\.net\z/i
|
||||||
|
"Privatter"
|
||||||
|
when /skeb\.jp\z/i
|
||||||
|
"Skeb"
|
||||||
|
when /tinami\.com\z/i
|
||||||
|
"Tinami"
|
||||||
|
when /toranoana\.(jp|shop)\z/i
|
||||||
|
"Toranoana"
|
||||||
|
when /youtube\.com\z/i
|
||||||
|
"Youtube"
|
||||||
|
else
|
||||||
|
host
|
||||||
|
end
|
||||||
rescue Addressable::URI::InvalidURIError
|
rescue Addressable::URI::InvalidURIError
|
||||||
nil
|
nil
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -90,7 +90,12 @@ module Sources
|
|||||||
end
|
end
|
||||||
|
|
||||||
def site_name
|
def site_name
|
||||||
"Pixiv"
|
# XXX pixiv sketch should be in a separate strategy
|
||||||
|
if parsed_url.host.in?(%w[sketch.pixiv.net img-sketch.pixiv.net img-sketch.pximg.net])
|
||||||
|
"Pixiv Sketch"
|
||||||
|
else
|
||||||
|
"Pixiv"
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def image_urls
|
def image_urls
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ class ArtistUrl < ApplicationRecord
|
|||||||
|
|
||||||
scope :url_matches, ->(url) { url_attribute_matches(:url, url) }
|
scope :url_matches, ->(url) { url_attribute_matches(:url, url) }
|
||||||
scope :normalized_url_matches, ->(url) { url_attribute_matches(:normalized_url, url) }
|
scope :normalized_url_matches, ->(url) { url_attribute_matches(:normalized_url, url) }
|
||||||
|
scope :active, -> { where(is_active: true) }
|
||||||
|
|
||||||
def self.parse_prefix(url)
|
def self.parse_prefix(url)
|
||||||
prefix, url = url.match(/\A(-)?(.*)/)[1, 2]
|
prefix, url = url.match(/\A(-)?(.*)/)[1, 2]
|
||||||
@@ -68,24 +69,51 @@ class ArtistUrl < ApplicationRecord
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def priority
|
def domain
|
||||||
if normalized_url =~ /pixiv\.net\/member\.php/
|
uri = Addressable::URI.parse(normalized_url)
|
||||||
10
|
uri.domain
|
||||||
|
end
|
||||||
|
|
||||||
elsif normalized_url =~ /seiga\.nicovideo\.jp\/user\/illust/
|
def site_name
|
||||||
10
|
source = Sources::Strategies.find(normalized_url)
|
||||||
|
source.site_name
|
||||||
elsif normalized_url =~ /twitter\.com/ && normalized_url !~ /status/
|
end
|
||||||
15
|
|
||||||
|
|
||||||
elsif normalized_url =~ /tumblr|patreon|deviantart|artstation/
|
|
||||||
20
|
|
||||||
|
|
||||||
|
# A secondary URL is an artist URL that we don't normally want to display,
|
||||||
|
# usually because it's redundant with the primary profile URL.
|
||||||
|
def secondary_url?
|
||||||
|
case url
|
||||||
|
when %r!pixiv\.net/stacc!i
|
||||||
|
true
|
||||||
|
when %r!pixiv\.net/fanbox!i
|
||||||
|
true
|
||||||
|
when %r!twitter\.com/intent!i
|
||||||
|
true
|
||||||
|
when %r!lohas\.nicoseiga\.jp!i
|
||||||
|
true
|
||||||
|
when %r!(?:www|com|dic)\.nicovideo\.jp!i
|
||||||
|
true
|
||||||
|
when %r!pawoo\.net/web/accounts!i
|
||||||
|
true
|
||||||
|
when %r!www\.artstation\.com!i
|
||||||
|
true
|
||||||
else
|
else
|
||||||
100
|
false
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# The sort order of sites in artist URL lists.
|
||||||
|
def priority
|
||||||
|
sites = %w[
|
||||||
|
Pixiv Twitter
|
||||||
|
ArtStation Deviant\ Art Nico\ Seiga Nijie pawoo.net Pixiv\ Fanbox Pixiv\ Sketch Tinami Tumblr
|
||||||
|
Booth.pm Facebook Fantia FC2 Gumroad Instagram Lofter Patreon Privatter Skeb Weibo Youtube
|
||||||
|
Circle.ms DLSite Melonbooks Toranoana
|
||||||
|
]
|
||||||
|
|
||||||
|
sites.index(site_name) || 1000
|
||||||
|
end
|
||||||
|
|
||||||
def normalize
|
def normalize
|
||||||
# Perform some normalization with Addressable on the URL itself
|
# Perform some normalization with Addressable on the URL itself
|
||||||
# - Converts scheme and hostname to downcase
|
# - Converts scheme and hostname to downcase
|
||||||
|
|||||||
@@ -6,17 +6,17 @@
|
|||||||
<span class="social-icons">
|
<span class="social-icons">
|
||||||
<% if Danbooru.config.source_code_url.present? %>
|
<% if Danbooru.config.source_code_url.present? %>
|
||||||
<%= link_to Danbooru.config.source_code_url, title: "Running commit: #{Rails.application.config.x.git_hash&.first(9)}", class: "social-icon" do %>
|
<%= link_to Danbooru.config.source_code_url, title: "Running commit: #{Rails.application.config.x.git_hash&.first(9)}", class: "social-icon" do %>
|
||||||
<img src="/images/github-logo.png">
|
<%= github_icon %>
|
||||||
<% end %>
|
<% end %>
|
||||||
<% end %>
|
<% end %>
|
||||||
<% if Danbooru.config.twitter_username.present? %>
|
<% if Danbooru.config.twitter_username.present? %>
|
||||||
<%= link_to "https://twitter.com/#{Danbooru.config.twitter_username}", class: "social-icon" do %>
|
<%= link_to "https://twitter.com/#{Danbooru.config.twitter_username}", class: "social-icon" do %>
|
||||||
<img src="/images/twitter-logo.png">
|
<%= twitter_icon %>
|
||||||
<% end %>
|
<% end %>
|
||||||
<% end %>
|
<% end %>
|
||||||
<% if Danbooru.config.discord_server_url.present? %>
|
<% if Danbooru.config.discord_server_url.present? %>
|
||||||
<%= link_to Danbooru.config.discord_server_url, class: "social-icon" do %>
|
<%= link_to Danbooru.config.discord_server_url, class: "social-icon" do %>
|
||||||
<img src="/images/discord-logo.png">
|
<%= discord_icon %>
|
||||||
<% end %>
|
<% end %>
|
||||||
<% end %>
|
<% end %>
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
BIN
public/images/artstation-logo.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
public/images/bcy-logo.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
public/images/booth-pm-logo.png
Normal file
|
After Width: | Height: | Size: 463 B |
BIN
public/images/circle-ms-logo.png
Normal file
|
After Width: | Height: | Size: 4.4 KiB |
BIN
public/images/deviantart-logo.png
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
public/images/dlsite-logo.png
Normal file
|
After Width: | Height: | Size: 597 B |
BIN
public/images/facebook-logo.png
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
BIN
public/images/fantia-logo.png
Executable file
|
After Width: | Height: | Size: 7.7 KiB |
BIN
public/images/fc2-logo.png
Normal file
|
After Width: | Height: | Size: 435 B |
BIN
public/images/gumroad-logo.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
public/images/instagram-logo.png
Normal file
|
After Width: | Height: | Size: 7.6 KiB |
BIN
public/images/lofter-logo.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
public/images/melonbooks-logo.png
Normal file
|
After Width: | Height: | Size: 6.1 KiB |
BIN
public/images/nicoseiga-logo.png
Normal file
|
After Width: | Height: | Size: 412 B |
BIN
public/images/nijie-logo.png
Normal file
|
After Width: | Height: | Size: 542 B |
BIN
public/images/patreon-logo.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
public/images/pawoo-logo.png
Normal file
|
After Width: | Height: | Size: 942 B |
BIN
public/images/pixiv-fanbox-logo.png
Normal file
|
After Width: | Height: | Size: 2.7 KiB |
BIN
public/images/pixiv-logo.png
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
BIN
public/images/pixiv-sketch-logo.png
Executable file
|
After Width: | Height: | Size: 2.3 KiB |
BIN
public/images/privatter-logo.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
public/images/skeb-logo.png
Normal file
|
After Width: | Height: | Size: 7.7 KiB |
BIN
public/images/tinami-logo.png
Normal file
|
After Width: | Height: | Size: 6.0 KiB |
BIN
public/images/toranoana-logo.png
Normal file
|
After Width: | Height: | Size: 666 B |
BIN
public/images/tumblr-logo.png
Normal file
|
After Width: | Height: | Size: 5.6 KiB |
BIN
public/images/weibo-logo.png
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
BIN
public/images/youtube-logo.png
Normal file
|
After Width: | Height: | Size: 16 KiB |