Add sigs and docs to downloaders

This commit is contained in:
Justin Edmund 2025-01-18 02:43:22 -08:00
parent 144d860408
commit 2d41b24896
12 changed files with 439 additions and 10 deletions

View file

@ -2,9 +2,39 @@
module Granblue module Granblue
module Downloaders module Downloaders
# Abstract base class for downloading game asset images in various sizes.
# Handles local and S3 storage, with support for test mode and verbose logging.
#
# @abstract Subclass must implement {#object_type}, {#base_url}, and {#directory_for_size}
#
# @example Downloading assets for a specific ID
# class MyDownloader < BaseDownloader
# def object_type; "weapon"; end
# def base_url; "http://example.com/assets"; end
# def directory_for_size(size)
# case size
# when "main" then "large"
# when "grid" then "medium"
# when "square" then "small"
# end
# end
# end
#
# downloader = MyDownloader.new("1234", storage: :both)
# downloader.download
#
# @note Supports three image sizes: main, grid, and square
# @note Can store images locally, in S3, or both
class BaseDownloader class BaseDownloader
# @return [Array<String>] Available image size variants
SIZES = %w[main grid square].freeze SIZES = %w[main grid square].freeze
# Initialize a new downloader instance
# @param id [String] ID of the object to download images for
# @param test_mode [Boolean] When true, only logs actions without downloading
# @param verbose [Boolean] When true, enables detailed logging
# @param storage [Symbol] Storage mode (:local, :s3, or :both)
# @return [void]
def initialize(id, test_mode: false, verbose: false, storage: :both) def initialize(id, test_mode: false, verbose: false, storage: :both)
@id = id @id = id
@base_url = base_url @base_url = base_url
@ -15,6 +45,8 @@ module Granblue
ensure_directories_exist unless @test_mode ensure_directories_exist unless @test_mode
end end
# Download images for all sizes
# @return [void]
def download def download
log_info "-> #{@id}" log_info "-> #{@id}"
return if @test_mode return if @test_mode
@ -28,6 +60,12 @@ module Granblue
private private
# Process download for a specific size variant
# @param url [String] URL to download from
# @param size [String] Size variant being processed
# @param path [String] Local path for download
# @param last [Boolean] Whether this is the last size being processed
# @return [void]
def process_download(url, size, path, last: false) def process_download(url, size, path, last: false)
filename = File.basename(url) filename = File.basename(url)
s3_key = build_s3_key(size, filename) s3_key = build_s3_key(size, filename)
@ -54,11 +92,19 @@ module Granblue
log_info "\t404 returned\t#{url}" log_info "\t404 returned\t#{url}"
end end
# Download file to local storage
# @param url [String] Source URL
# @param download_uri [String] Local destination path
# @return [void]
def download_to_local(url, download_uri) def download_to_local(url, download_uri)
download = URI.parse(url).open download = URI.parse(url).open
IO.copy_stream(download, download_uri) IO.copy_stream(download, download_uri)
end end
# Stream file directly to S3
# @param url [String] Source URL
# @param s3_key [String] S3 object key
# @return [void]
def stream_to_s3(url, s3_key) def stream_to_s3(url, s3_key)
return if @aws_service.file_exists?(s3_key) return if @aws_service.file_exists?(s3_key)
@ -67,6 +113,11 @@ module Granblue
end end
end end
# Download file to both local storage and S3
# @param url [String] Source URL
# @param download_uri [String] Local destination path
# @param s3_key [String] S3 object key
# @return [void]
def download_to_both(url, download_uri, s3_key) def download_to_both(url, download_uri, s3_key)
download = URI.parse(url).open download = URI.parse(url).open
@ -82,17 +133,23 @@ module Granblue
end end
end end
# Check if file should be downloaded based on storage mode
# @param local_path [String] Local file path
# @param s3_key [String] S3 object key
# @return [Boolean] true if file should be downloaded
def should_download?(local_path, s3_key) def should_download?(local_path, s3_key)
case @storage if @storage == :local
when :local
!File.exist?(local_path) !File.exist?(local_path)
when :s3 elsif @storage == :s3
!@aws_service.file_exists?(s3_key) !@aws_service.file_exists?(s3_key)
when :both else
# :both
!File.exist?(local_path) || !@aws_service.file_exists?(s3_key) !File.exist?(local_path) || !@aws_service.file_exists?(s3_key)
end end
end end
# Ensure local directories exist for each size
# @return [void]
def ensure_directories_exist def ensure_directories_exist
return unless store_locally? return unless store_locally?
@ -101,45 +158,85 @@ module Granblue
end end
end end
# Check if local storage is being used
# @return [Boolean] true if storing locally
def store_locally? def store_locally?
%i[local both].include?(@storage) %i[local both].include?(@storage)
end end
# Get local download path for a size
# @param size [String] Image size variant
# @return [String] Local directory path
def download_path(size) def download_path(size)
"#{Rails.root}/download/#{object_type}-#{size}" "#{Rails.root}/download/#{object_type}-#{size}"
end end
# Build S3 key for an image
# @param size [String] Image size variant
# @param filename [String] Image filename
# @return [String] Complete S3 key
def build_s3_key(size, filename) def build_s3_key(size, filename)
"#{object_type}-#{size}/#{filename}" "#{object_type}-#{size}/#{filename}"
end end
# Log informational message if verbose
# @param message [String] Message
def log_info(message) def log_info(message)
puts message if @verbose puts message if @verbose
end end
# Download elemental variant image
# @param url [String] Source URL
# @param size [String] Image size variant
# @param path [String] Destination path
# @param filename [String] Image filename
# @return [void]
def download_elemental_image(url, size, path, filename) def download_elemental_image(url, size, path, filename)
return if @test_mode return if @test_mode
filepath = "#{path}/#{filename}" filepath = "#{path}/#{filename}"
download = URI.parse(url).open URI.open(url) do |file|
content = file.read
if content
File.open(filepath, 'wb') do |output|
output.write(content)
end
else
raise "Failed to read content from #{url}"
end
end
log_info "-> #{size}:\t#{url}..." log_info "-> #{size}:\t#{url}..."
IO.copy_stream(download, filepath)
rescue OpenURI::HTTPError rescue OpenURI::HTTPError
log_info "\t404 returned\t#{url}" log_info "\t404 returned\t#{url}"
rescue StandardError => e
log_info "\tError downloading #{url}: #{e.message}"
end end
# Get asset type (e.g., "weapon", "character")
# @abstract
# @return [String] Asset type name
def object_type def object_type
raise NotImplementedError, 'Subclasses must define object_type' raise NotImplementedError, 'Subclasses must define object_type'
end end
# Get base URL for assets
# @abstract
# @return [String] Base URL
def base_url def base_url
raise NotImplementedError, 'Subclasses must define base_url' raise NotImplementedError, 'Subclasses must define base_url'
end end
# Get directory name for a size variant
# @abstract
# @param size [String] Image size variant
# @return [String] Directory name
def directory_for_size(size) def directory_for_size(size)
raise NotImplementedError, 'Subclasses must define directory_for_size' raise NotImplementedError, 'Subclasses must define directory_for_size'
end end
# Build complete URL for a size variant
# @param size [String] Image size variant
# @return [String] Complete download URL
def build_url(size) def build_url(size)
directory = directory_for_size(size) directory = directory_for_size(size)
"#{@base_url}/#{directory}/#{@id}.jpg" "#{@base_url}/#{directory}/#{@id}.jpg"

View file

@ -2,7 +2,23 @@
module Granblue module Granblue
module Downloaders module Downloaders
# Downloads character image assets from the game server in different sizes and variants.
# Handles character-specific variants like base art, uncap art, and transcendence art.
#
# @example Download images for a specific character
# downloader = CharacterDownloader.new("3040001000", storage: :both)
# downloader.download
#
# @note Character images come in multiple variants (_01, _02, etc.) based on uncap status
# @note Supports FLB (5★) and ULB (6★) art variants when available
class CharacterDownloader < BaseDownloader class CharacterDownloader < BaseDownloader
# Downloads images for all variants of a character based on their uncap status.
# Overrides {BaseDownloader#download} to handle character-specific variants.
#
# @return [void]
# @note Skips download if character is not found in database
# @note Downloads FLB/ULB variants only if character has those uncaps
# @see #download_variants
def download def download
character = Character.find_by(granblue_id: @id) character = Character.find_by(granblue_id: @id)
return unless character return unless character
@ -12,9 +28,13 @@ module Granblue
private private
# Downloads all variants of a character's images
# @param character [Character] Character model instance to download images for
# @return [void]
# @note Only downloads variants that should exist based on character uncap status
def download_variants(character) def download_variants(character)
# All characters have 01 and 02 variants # All characters have 01 and 02 variants
variants = ["#{@id}_01", "#{@id}_02"] variants = %W[#{@id}_01 #{@id}_02]
# Add FLB variant if available # Add FLB variant if available
variants << "#{@id}_03" if character.flb variants << "#{@id}_03" if character.flb
@ -29,6 +49,9 @@ module Granblue
end end
end end
# Downloads a specific variant's images in all sizes
# @param variant_id [String] Character variant ID (e.g., "3040001000_01")
# @return [void]
def download_variant(variant_id) def download_variant(variant_id)
log_info "-> #{variant_id}" if @verbose log_info "-> #{variant_id}" if @verbose
return if @test_mode return if @test_mode
@ -40,19 +63,31 @@ module Granblue
end end
end end
# Builds URL for a specific variant and size
# @param variant_id [String] Character variant ID
# @param size [String] Image size variant ("main", "grid", or "square")
# @return [String] Complete URL for downloading the image
def build_variant_url(variant_id, size) def build_variant_url(variant_id, size)
directory = directory_for_size(size) directory = directory_for_size(size)
"#{@base_url}/#{directory}/#{variant_id}.jpg" "#{@base_url}/#{directory}/#{variant_id}.jpg"
end end
# Gets object type for file paths and storage keys
# @return [String] Returns "character"
def object_type def object_type
'character' 'character'
end end
# Gets base URL for character assets
# @return [String] Base URL for character images
def base_url def base_url
'http://gbf.game-a.mbga.jp/assets/img/sp/assets/npc' 'http://gbf.game-a.mbga.jp/assets/img/sp/assets/npc'
end end
# Gets directory name for a size variant
# @param size [String] Image size variant
# @return [String] Directory name in game asset URL structure
# @note Maps "main" -> "f", "grid" -> "m", "square" -> "s"
def directory_for_size(size) def directory_for_size(size)
case size.to_s case size.to_s
when 'main' then 'f' when 'main' then 'f'

View file

@ -2,8 +2,41 @@
module Granblue module Granblue
module Downloaders module Downloaders
# Manages downloading of game assets by coordinating different downloader types.
# Provides a single interface for downloading any type of game asset.
#
# @example Download character images
# DownloadManager.download_for_object('character', '3040001000', storage: :s3)
#
# @example Download weapon images in test mode
# DownloadManager.download_for_object('weapon', '1040001000', test_mode: true, verbose: true)
#
# @note Automatically selects the appropriate downloader based on object type
# @note Handles configuration of downloader options consistently across types
class DownloadManager class DownloadManager
class << self class << self
# Downloads assets for a specific game object using the appropriate downloader
#
# @param type [String] Type of game object ('character', 'weapon', or 'summon')
# @param granblue_id [String] Game ID of the object to download assets for
# @param test_mode [Boolean] When true, simulates downloads without actually downloading
# @param verbose [Boolean] When true, enables detailed logging
# @param storage [Symbol] Storage mode to use (:local, :s3, or :both)
# @return [void]
#
# @example Download character images to S3
# DownloadManager.download_for_object('character', '3040001000', storage: :s3)
#
# @example Test weapon downloads with verbose logging
# DownloadManager.download_for_object('weapon', '1040001000',
# test_mode: true,
# verbose: true
# )
#
# @note Logs warning if object type is unknown
# @see CharacterDownloader
# @see WeaponDownloader
# @see SummonDownloader
def download_for_object(type, granblue_id, test_mode: false, verbose: false, storage: :both) def download_for_object(type, granblue_id, test_mode: false, verbose: false, storage: :both)
downloader_options = { downloader_options = {
test_mode: test_mode, test_mode: test_mode,

View file

@ -4,13 +4,31 @@ require_relative 'weapon_downloader'
module Granblue module Granblue
module Downloaders module Downloaders
# Specialized downloader for handling elemental weapon variants.
# Some weapons have different art for each element, requiring multiple downloads.
#
# @example Download all elemental variants
# downloader = ElementalWeaponDownloader.new(1040001000)
# downloader.download
#
# @note Handles weapons that have variants for all six elements
# @note Uses specific suffix mappings for element art variants
class ElementalWeaponDownloader < WeaponDownloader class ElementalWeaponDownloader < WeaponDownloader
# Element variant suffix mapping
# @return [Array<Integer>] Ordered list of suffixes for element variants
SUFFIXES = [2, 3, 4, 1, 6, 5].freeze SUFFIXES = [2, 3, 4, 1, 6, 5].freeze
# Initialize downloader with base weapon ID
# @param id_base [Integer] Base ID for the elemental weapon series
# @return [void]
def initialize(id_base) def initialize(id_base)
@id_base = id_base.to_i @id_base = id_base.to_i
end end
# Downloads all elemental variants of the weapon
# @return [void]
# @note Downloads variants for all six elements
# @note Uses progress reporter to show download status
def download def download
(1..6).each do |i| (1..6).each do |i|
id = @id_base + (i - 1) * 100 id = @id_base + (i - 1) * 100

View file

@ -2,7 +2,23 @@
module Granblue module Granblue
module Downloaders module Downloaders
# Downloads summon image assets from the game server in different sizes and variants.
# Handles summon-specific variants like base art, ULB art, and transcendence art.
#
# @example Download images for a specific summon
# downloader = SummonDownloader.new("2040001000", storage: :both)
# downloader.download
#
# @note Summon images come in multiple variants based on uncap status
# @note Supports ULB (5★) and transcendence variants when available
class SummonDownloader < BaseDownloader class SummonDownloader < BaseDownloader
# Downloads images for all variants of a summon based on their uncap status.
# Overrides {BaseDownloader#download} to handle summon-specific variants.
#
# @return [void]
# @note Skips download if summon is not found in database
# @note Downloads ULB and transcendence variants only if summon has those uncaps
# @see #download_variants
def download def download
summon = Summon.find_by(granblue_id: @id) summon = Summon.find_by(granblue_id: @id)
return unless summon return unless summon
@ -12,6 +28,11 @@ module Granblue
private private
# Downloads all variants of a summon's images
# @param summon [Summon] Summon model instance to download images for
# @return [void]
# @note Only downloads variants that should exist based on summon uncap status
# @note Handles special transcendence art variants for 6★ summons
def download_variants(summon) def download_variants(summon)
# All summons have base variant # All summons have base variant
variants = [@id] variants = [@id]
@ -31,6 +52,10 @@ module Granblue
end end
end end
# Downloads a specific variant's images in all sizes
# @param variant_id [String] Summon variant ID (e.g., "2040001000_02")
# @return [void]
# @note Downloads all size variants (main/grid/square) for the given variant
def download_variant(variant_id) def download_variant(variant_id)
log_info "-> #{variant_id}" if @verbose log_info "-> #{variant_id}" if @verbose
return if @test_mode return if @test_mode
@ -42,19 +67,31 @@ module Granblue
end end
end end
# Builds URL for a specific variant and size
# @param variant_id [String] Summon variant ID
# @param size [String] Image size variant ("main", "grid", or "square")
# @return [String] Complete URL for downloading the image
def build_variant_url(variant_id, size) def build_variant_url(variant_id, size)
directory = directory_for_size(size) directory = directory_for_size(size)
"#{@base_url}/#{directory}/#{variant_id}.jpg" "#{@base_url}/#{directory}/#{variant_id}.jpg"
end end
# Gets object type for file paths and storage keys
# @return [String] Returns "summon"
def object_type def object_type
'summon' 'summon'
end end
# Gets base URL for summon assets
# @return [String] Base URL for summon images
def base_url def base_url
'http://gbf.game-a.mbga.jp/assets/img/sp/assets/summon' 'http://gbf.game-a.mbga.jp/assets/img/sp/assets/summon'
end end
# Gets directory name for a size variant
# @param size [String] Image size variant
# @return [String] Directory name in game asset URL structure
# @note Maps "main" -> "party_main", "grid" -> "party_sub", "square" -> "s"
def directory_for_size(size) def directory_for_size(size)
case size.to_s case size.to_s
when 'main' then 'party_main' when 'main' then 'party_main'

View file

@ -2,7 +2,24 @@
module Granblue module Granblue
module Downloaders module Downloaders
# Downloads weapon image assets from the game server in different sizes and variants.
# Handles weapon-specific variants like base art, transcendence art, and elemental variants.
#
# @example Download images for a specific weapon
# downloader = WeaponDownloader.new("1040001000", storage: :both)
# downloader.download
#
# @note Weapon images come in multiple variants based on uncap and element status
# @note Supports transcendence variants and element-specific variants
# @see ElementalWeaponDownloader for handling multi-element weapons
class WeaponDownloader < BaseDownloader class WeaponDownloader < BaseDownloader
# Downloads images for all variants of a weapon based on their uncap status.
# Overrides {BaseDownloader#download} to handle weapon-specific variants.
#
# @return [void]
# @note Skips download if weapon is not found in database
# @note Downloads transcendence variants only if weapon has those uncaps
# @see #download_variants
def download def download
weapon = Weapon.find_by(granblue_id: @id) weapon = Weapon.find_by(granblue_id: @id)
return unless weapon return unless weapon
@ -12,6 +29,11 @@ module Granblue
private private
# Downloads all variants of a weapon's images
# @param weapon [Weapon] Weapon model instance to download images for
# @return [void]
# @note Only downloads variants that should exist based on weapon uncap status
# @note Handles special transcendence art variants for transcendable weapons
def download_variants(weapon) def download_variants(weapon)
# All weapons have base variant # All weapons have base variant
variants = [@id] variants = [@id]
@ -28,6 +50,10 @@ module Granblue
end end
end end
# Downloads a specific variant's images in all sizes
# @param variant_id [String] Weapon variant ID (e.g., "1040001000_02")
# @return [void]
# @note Downloads all size variants (main/grid/square) for the given variant
def download_variant(variant_id) def download_variant(variant_id)
log_info "-> #{variant_id}" if @verbose log_info "-> #{variant_id}" if @verbose
return if @test_mode return if @test_mode
@ -39,19 +65,31 @@ module Granblue
end end
end end
# Builds URL for a specific variant and size
# @param variant_id [String] Weapon variant ID
# @param size [String] Image size variant ("main", "grid", or "square")
# @return [String] Complete URL for downloading the image
def build_variant_url(variant_id, size) def build_variant_url(variant_id, size)
directory = directory_for_size(size) directory = directory_for_size(size)
"#{@base_url}/#{directory}/#{variant_id}.jpg" "#{@base_url}/#{directory}/#{variant_id}.jpg"
end end
# Gets object type for file paths and storage keys
# @return [String] Returns "weapon"
def object_type def object_type
'weapon' 'weapon'
end end
# Gets base URL for weapon assets
# @return [String] Base URL for weapon images
def base_url def base_url
'http://gbf.game-a.mbga.jp/assets/img/sp/assets/weapon' 'http://gbf.game-a.mbga.jp/assets/img/sp/assets/weapon'
end end
# Gets directory name for a size variant
# @param size [String] Image size variant
# @return [String] Directory name in game asset URL structure
# @note Maps "main" -> "ls", "grid" -> "m", "square" -> "s"
def directory_for_size(size) def directory_for_size(size)
case size.to_s case size.to_s
when 'main' then 'ls' when 'main' then 'ls'

View file

@ -0,0 +1,53 @@
module Granblue
module Downloaders
class BaseDownloader
SIZES: Array[String]
# Define allowed storage types
type storage = :local | :s3 | :both
@id: String
@base_url: String
@test_mode: bool
@verbose: bool
@storage: storage
@aws_service: AwsService
def initialize: (String id, ?test_mode: bool, ?verbose: bool, ?storage: storage) -> void
def download: -> void
private
def process_download: (String url, String size, String path, ?last: bool) -> void
def download_to_local: (String url, String download_uri) -> void
def stream_to_s3: (String url, String s3_key) -> void
def download_to_both: (String url, String download_uri, String s3_key) -> void
def should_download?: (String local_path, String s3_key) -> bool
def ensure_directories_exist: -> void
def store_locally?: -> bool
def download_path: (String size) -> String
def build_s3_key: (String size, String filename) -> String
def log_info: (String message) -> void
def download_elemental_image: (String url, String size, String path, String filename) -> void
def object_type: -> String
def base_url: -> String
def directory_for_size: (String size) -> String
def build_url: (String size) -> String
end
end
end

View file

@ -0,0 +1,28 @@
module Granblue
module Downloaders
class CharacterDownloader < BaseDownloader
private
def download_variants: (Character character) -> void
def download_variant: (String variant_id) -> void
def build_variant_url: (String variant_id, String size) -> String
def object_type: -> String
def base_url: -> String
def directory_for_size: (String size) -> String
private
@id: String
@base_url: String
@test_mode: bool
@verbose: bool
@storage: Symbol
@aws_service: AwsService
end
end
end

View file

@ -0,0 +1,15 @@
module Granblue
module Downloaders
class DownloadManager
def self.download_for_object: (
String type,
String granblue_id,
?test_mode: bool,
?verbose: bool,
?storage: Symbol
) -> void
private
end
end
end

View file

@ -0,0 +1,30 @@
module Granblue
module Downloaders
class SummonDownloader < BaseDownloader
def download: -> void
private
def download_variants: (Summon summon) -> void
def download_variant: (String variant_id) -> void
def build_variant_url: (String variant_id, String size) -> String
def object_type: -> String
def base_url: -> String
def directory_for_size: (String size) -> String
private
@id: String
@base_url: String
@test_mode: bool
@verbose: bool
@storage: Symbol
@aws_service: AwsService
end
end
end

View file

@ -0,0 +1,48 @@
module Granblue
module Downloaders
class WeaponDownloader < BaseDownloader
def download: -> void
private
def download_variants: (Weapon weapon) -> void
def download_variant: (String variant_id) -> void
def build_variant_url: (String variant_id, String size) -> String
def object_type: -> String
def base_url: -> String
def directory_for_size: (String size) -> String
def build_url_for_id: (String id, String size) -> String
# Track progress of elemental weapon downloads
def progress_reporter: (count: Integer, total: Integer, result: String, ?bar_len: Integer) -> void
private
@id: String
@base_url: String
@test_mode: bool
@verbose: bool
@storage: Symbol
@aws_service: AwsService
end
# Special downloader for handling elemental weapon variants
class ElementalWeaponDownloader < WeaponDownloader
SUFFIXES: Array[Integer]
def initialize: (Integer id_base) -> void
def download: -> void
private
@id_base: Integer
end
end
end

View file

@ -1,15 +1,12 @@
module Granblue module Granblue
module Transformers module Transformers
class SummonTransformer < BaseTransformer class SummonTransformer < BaseTransformer
# Level thresholds for transcendence calculations
TRANSCENDENCE_LEVELS: Array[Integer] TRANSCENDENCE_LEVELS: Array[Integer]
# Quick summon ID for the current transformation
@quick_summon_id: String? @quick_summon_id: String?
def initialize: (untyped data, ?String? quick_summon_id, ?Hash[Symbol, untyped] options) -> void def initialize: (untyped data, ?String? quick_summon_id, ?Hash[Symbol, untyped] options) -> void
# Implements abstract method from BaseTransformer
def transform: -> Array[Hash[Symbol, untyped]] def transform: -> Array[Hash[Symbol, untyped]]
private private