Add downloaders

Downloaders take Granblue IDs and download images for those items from the Granblue Fantasy server in all relevant sizes.

Downloaders can download to disk or stream the file directly to S3.
This commit is contained in:
Justin Edmund 2025-01-12 16:02:51 -08:00
parent 9099849ee3
commit 551c2609b0
5 changed files with 267 additions and 0 deletions

View file

@ -0,0 +1,145 @@
# frozen_string_literal: true
module Granblue
module Downloader
class BaseDownloader
SIZES = %w[main grid square].freeze
def initialize(id, test_mode: false, verbose: false, storage: :both)
@id = id
@base_url = base_url
@test_mode = test_mode
@verbose = verbose
@storage = storage
@aws_service = AwsService.new
ensure_directories_exist unless @test_mode
end
def download
log_info "#{object_type.capitalize} #{@id}"
return if @test_mode
SIZES.each do |size|
path = download_path(size)
url = build_url(size)
process_download(url, size, path)
end
end
private
def process_download(url, size, path)
filename = File.basename(url)
s3_key = build_s3_key(size, filename)
download_uri = "#{path}/#{filename}"
should_process = should_download?(download_uri, s3_key)
return unless should_process
log_info "\tDownloading #{size}\t#{url}..."
case @storage
when :local
download_to_local(url, download_uri)
when :s3
stream_to_s3(url, s3_key)
when :both
download_to_both(url, download_uri, s3_key)
end
rescue OpenURI::HTTPError
log_info "\t404 returned\t#{url}"
end
def download_to_local(url, download_uri)
download = URI.parse(url).open
IO.copy_stream(download, download_uri)
end
def stream_to_s3(url, s3_key)
return if @aws_service.file_exists?(s3_key)
URI.parse(url).open do |file|
@aws_service.upload_stream(file, s3_key)
end
end
def download_to_both(url, download_uri, s3_key)
download = URI.parse(url).open
# Write to local file
IO.copy_stream(download, download_uri)
# Reset file pointer for S3 upload
download.rewind
# Upload to S3 if it doesn't exist
unless @aws_service.file_exists?(s3_key)
@aws_service.upload_stream(download, s3_key)
end
end
def should_download?(local_path, s3_key)
case @storage
when :local
!File.exist?(local_path)
when :s3
!@aws_service.file_exists?(s3_key)
when :both
!File.exist?(local_path) || !@aws_service.file_exists?(s3_key)
end
end
def ensure_directories_exist
return unless store_locally?
SIZES.each do |size|
FileUtils.mkdir_p(download_path(size))
end
end
def store_locally?
%i[local both].include?(@storage)
end
def download_path(size)
"#{Rails.root}/download/#{object_type}-#{size}"
end
def build_s3_key(size, filename)
"#{object_type}-#{size}/#{filename}"
end
def log_info(message)
puts message if @verbose
end
def download_elemental_image(url, size, path, filename)
return if @test_mode
filepath = "#{path}/#{filename}"
download = URI.parse(url).open
log_info "\tDownloading #{size}\t#{url}..."
IO.copy_stream(download, filepath)
rescue OpenURI::HTTPError
log_info "\t404 returned\t#{url}"
end
def object_type
raise NotImplementedError, 'Subclasses must define object_type'
end
def base_url
raise NotImplementedError, 'Subclasses must define base_url'
end
def directory_for_size(size)
raise NotImplementedError, 'Subclasses must define directory_for_size'
end
def build_url(size)
directory = directory_for_size(size)
"#{@base_url}/#{directory}/#{@id}.jpg"
end
end
end
end

View file

@ -0,0 +1,25 @@
# frozen_string_literal: true
module Granblue
module Downloader
class CharacterDownloader < BaseDownloader
private
def object_type
'character'
end
def base_url
'http://gbf.game-a.mbga.jp/assets/img/sp/assets/npc'
end
def directory_for_size(size)
case size.to_s
when 'main' then 'f'
when 'grid' then 'm'
when 'square' then 's'
end
end
end
end
end

View file

@ -0,0 +1,47 @@
# frozen_string_literal: true
require_relative 'weapon_downloader'
module Granblue
module Downloader
class ElementalWeaponDownloader < WeaponDownloader
SUFFIXES = [2, 3, 4, 1, 6, 5].freeze
def initialize(id_base)
@id_base = id_base.to_i
end
def download
(1..6).each do |i|
id = @id_base + (i - 1) * 100
suffix = SUFFIXES[i - 1]
puts "Elemental Weapon #{id}_#{suffix}"
SIZES.each do |size|
path = download_path(size)
url = build_url_for_id(id, size)
filename = "#{id}_#{suffix}.jpg"
download_elemental_image(url, size, path, filename)
end
progress_reporter(count: i, total: 6, result: "Elemental Weapon #{id}_#{suffix}")
end
end
private
def build_url_for_id(id, size)
directory = directory_for_size(size)
"#{base_url}/#{directory}/#{id}.jpg"
end
def progress_reporter(count:, total:, result:, bar_len: 40)
filled_len = (bar_len * count / total).round
status = result
percents = (100.0 * count / total).round(1)
bar = '=' * filled_len + '-' * (bar_len - filled_len)
print("\n[#{bar}] #{percents}% ...#{' ' * 14}#{status}\n")
end
end
end
end

View file

@ -0,0 +1,25 @@
# frozen_string_literal: true
module Granblue
module Downloader
class SummonDownloader < BaseDownloader
private
def object_type
'summon'
end
def base_url
'http://gbf.game-a.mbga.jp/assets/img/sp/assets/summon'
end
def directory_for_size(size)
case size.to_s
when 'main' then 'party_main'
when 'grid' then 'party_sub'
when 'square' then 's'
end
end
end
end
end

View file

@ -0,0 +1,25 @@
# frozen_string_literal: true
module Granblue
module Downloader
class WeaponDownloader < BaseDownloader
private
def object_type
'weapon'
end
def base_url
'http://gbf.game-a.mbga.jp/assets/img/sp/assets/weapon'
end
def directory_for_size(size)
case size.to_s
when 'main' then 'ls'
when 'grid' then 'm'
when 'square' then 's'
end
end
end
end
end