artbase 0.1.0 → 0.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/CHANGELOG.md +3 -3
- data/Manifest.txt +3 -6
- data/README.md +274 -39
- data/Rakefile +38 -29
- data/bin/artbase +17 -17
- data/lib/artbase/attributes.rb +83 -83
- data/lib/artbase/collection/base.rb +268 -0
- data/lib/artbase/collection/image.rb +39 -39
- data/lib/artbase/collection/opensea.rb +297 -484
- data/lib/artbase/collection/token.rb +362 -72
- data/lib/artbase/collection.rb +12 -12
- data/lib/artbase/helper.rb +168 -149
- data/lib/artbase/image/sample.rb +31 -0
- data/lib/artbase/image.rb +31 -75
- data/lib/artbase/retry.rb +41 -0
- data/lib/artbase/tool.rb +169 -159
- data/lib/artbase/version.rb +22 -22
- data/lib/artbase.rb +103 -42
- metadata +22 -12
- data/LICENSE.md +0 -116
- data/lib/artbase/image/24x24.rb +0 -68
- data/lib/artbase/image/32x32.rb +0 -43
- data/lib/artbase/image/60x60.rb +0 -70
- data/lib/artbase/image/80x80.rb +0 -90
- data/lib/artbase/opensea.rb +0 -144
@@ -1,72 +1,362 @@
|
|
1
|
-
|
2
|
-
class TokenCollection
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
end
|
12
|
-
|
13
|
-
|
14
|
-
def
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
end
|
1
|
+
|
2
|
+
class TokenCollection < Artbase::Base
|
3
|
+
|
4
|
+
|
5
|
+
#############
|
6
|
+
# (nested) Meta classes
|
7
|
+
# read meta data into struct
|
8
|
+
class Meta
|
9
|
+
def self.read( path )
|
10
|
+
new( read_json( path ))
|
11
|
+
end
|
12
|
+
|
13
|
+
|
14
|
+
def initialize( data )
|
15
|
+
@data = data
|
16
|
+
end
|
17
|
+
|
18
|
+
|
19
|
+
def name
|
20
|
+
@name ||= _normalize( @data['name'] )
|
21
|
+
end
|
22
|
+
|
23
|
+
def description
|
24
|
+
@description ||= _normalize( @data['description'] )
|
25
|
+
end
|
26
|
+
|
27
|
+
## note: auto-convert "" (empty string) to nil
|
28
|
+
def image() _blank( @data['image'] ); end
|
29
|
+
alias_method :image_url, :image ## add image_url alias - why? why not?
|
30
|
+
|
31
|
+
|
32
|
+
def attributes
|
33
|
+
@attributes ||= begin
|
34
|
+
traits = []
|
35
|
+
## keep traits as (simple)
|
36
|
+
## ordered array of pairs for now
|
37
|
+
##
|
38
|
+
## in a step two make lookup via hash table
|
39
|
+
## or such easier / "automagic"
|
40
|
+
|
41
|
+
@data[ 'attributes' ].each do |t|
|
42
|
+
trait_type = t['trait_type'].strip
|
43
|
+
trait_value = t['value'].strip
|
44
|
+
traits << [trait_type, trait_value]
|
45
|
+
end
|
46
|
+
|
47
|
+
traits
|
48
|
+
end
|
49
|
+
end
|
50
|
+
alias_method :traits, :attributes ## keep traits alias - why? why not?
|
51
|
+
|
52
|
+
### "private" convenience / helper methods
|
53
|
+
def _normalize( str )
|
54
|
+
return if str.nil? ## check: check for nil - why? why not?
|
55
|
+
|
56
|
+
## normalize string
|
57
|
+
## remove leading and trailing spaces
|
58
|
+
## collapse two and more spaces into one
|
59
|
+
## change unicode space to ascii
|
60
|
+
str = str.gsub( "\u{00a0}", ' ' )
|
61
|
+
str = str.strip.gsub( /[ ]{2,}/, ' ' )
|
62
|
+
str
|
63
|
+
end
|
64
|
+
|
65
|
+
def _blank( o ) ## auto-convert "" (empty string) into nil
|
66
|
+
if o && o.strip.empty?
|
67
|
+
nil
|
68
|
+
else
|
69
|
+
o
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end # (nested) class Meta
|
73
|
+
|
74
|
+
|
75
|
+
|
76
|
+
|
77
|
+
attr_reader :slug, :count
|
78
|
+
|
79
|
+
def initialize( slug, count,
|
80
|
+
token_base:,
|
81
|
+
image_base: nil,
|
82
|
+
image_base_id_format: nil,
|
83
|
+
format:,
|
84
|
+
source:,
|
85
|
+
top_x: 0,
|
86
|
+
top_y: 0,
|
87
|
+
center_x: true,
|
88
|
+
center_y: true,
|
89
|
+
excludes: [],
|
90
|
+
offset: 0 ) # check: rename count to items or such - why? why not?
|
91
|
+
@slug = slug
|
92
|
+
@count = count
|
93
|
+
@offset = offset ## starting by default at 0 (NOT 1 or such)
|
94
|
+
|
95
|
+
@token_base = token_base
|
96
|
+
@image_base = image_base
|
97
|
+
@image_base_id_format = image_base_id_format
|
98
|
+
|
99
|
+
@width, @height = _parse_dimension( format )
|
100
|
+
|
101
|
+
|
102
|
+
## note: allow multiple source formats / dimensions
|
103
|
+
### e.g. convert 512x512 into [ [512,512] ]
|
104
|
+
##
|
105
|
+
source = [source] unless source.is_a?( Array )
|
106
|
+
@sources = source.map { |dimension| _parse_dimension( dimension ) }
|
107
|
+
|
108
|
+
@top_x = top_x ## more (down)sampling / pixelate options
|
109
|
+
@top_y = top_y
|
110
|
+
@center_x = center_x
|
111
|
+
@center_y = center_y
|
112
|
+
|
113
|
+
@excludes = excludes
|
114
|
+
end
|
115
|
+
|
116
|
+
|
117
|
+
## e.g. convert dimension (width x height) "24x24" or "24 x 24" to [24,24]
|
118
|
+
def _parse_dimension( str )
|
119
|
+
str.split( /x/i ).map { |str| str.strip.to_i }
|
120
|
+
end
|
121
|
+
|
122
|
+
|
123
|
+
def _range( offset: 0 ) ## return "default" range - make "private" helper public - why? why not?
|
124
|
+
## note: range uses three dots (...) exclusive (NOT inclusive) range
|
125
|
+
## e.g. 0...100 => [0,..,99]
|
126
|
+
## 1...101 => [1,..,100]
|
127
|
+
##
|
128
|
+
## note: allow offset argument
|
129
|
+
## (to start with different offset - note: in addition to builtin 0/1 offset)
|
130
|
+
|
131
|
+
(0+@offset+offset...@count+@offset)
|
132
|
+
end
|
133
|
+
|
134
|
+
|
135
|
+
|
136
|
+
def each_image( range=_range,
|
137
|
+
exclude: true, &blk )
|
138
|
+
range.each do |id|
|
139
|
+
####
|
140
|
+
# filter out/skip
|
141
|
+
# if exclude && @excludes.include?( id )
|
142
|
+
# puts " skipping / exclude #{id}..."
|
143
|
+
# next
|
144
|
+
# end
|
145
|
+
|
146
|
+
puts "==> #{id}"
|
147
|
+
img = Image.read( "./#{@slug}/#{@width}x#{@height}/#{id}.png" )
|
148
|
+
blk.call( img, id )
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
|
153
|
+
def each_meta( range=_range,
|
154
|
+
exclude: true, &blk )
|
155
|
+
range.each do |id| ## check: change/rename id to index - why? why not?
|
156
|
+
meta = Meta.read( "./#{@slug}/token/#{id}.json" )
|
157
|
+
|
158
|
+
####
|
159
|
+
# filter out/skip
|
160
|
+
# if exclude && @excludes.include?( meta.name )
|
161
|
+
# puts " skipping / exclude #{id} >#{meta.name}<..."
|
162
|
+
# next
|
163
|
+
# end
|
164
|
+
|
165
|
+
blk.call( meta, id )
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
|
170
|
+
|
171
|
+
|
172
|
+
|
173
|
+
def pixelate( range=_range, exclude: true,
|
174
|
+
force: false,
|
175
|
+
debug: false,
|
176
|
+
zoom: nil )
|
177
|
+
|
178
|
+
range.each do |id|
|
179
|
+
|
180
|
+
if exclude && @excludes.include?( id )
|
181
|
+
puts " skipping #{id}; listed in excludes #{@excludes.inspect}"
|
182
|
+
next
|
183
|
+
end
|
184
|
+
|
185
|
+
outpath = "./#{@slug}/#{@width}x#{@height}/#{id}.png"
|
186
|
+
if !force && File.exist?( outpath )
|
187
|
+
next ## note: skip if file already exists
|
188
|
+
end
|
189
|
+
|
190
|
+
center_x = if @center_x.is_a?( Proc ) then @center_x.call( id ); else @center_x; end
|
191
|
+
center_y = if @center_y.is_a?( Proc ) then @center_y.call( id ); else @center_y; end
|
192
|
+
|
193
|
+
|
194
|
+
|
195
|
+
puts "==> #{id} - reading / decoding #{id} ..."
|
196
|
+
start = Time.now
|
197
|
+
|
198
|
+
img = Image.read( "./#{@slug}/token-i/#{id}.png" )
|
199
|
+
|
200
|
+
stop = Time.now
|
201
|
+
diff = stop - start
|
202
|
+
|
203
|
+
puts " in #{diff} sec(s)\n"
|
204
|
+
|
205
|
+
|
206
|
+
source = nil
|
207
|
+
@sources.each do |source_width, source_height|
|
208
|
+
if img.width == source_width && img.height == source_height
|
209
|
+
source = [source_width, source_height]
|
210
|
+
break
|
211
|
+
end
|
212
|
+
end
|
213
|
+
|
214
|
+
|
215
|
+
if source
|
216
|
+
source_width = source[0]
|
217
|
+
source_height = source[1]
|
218
|
+
|
219
|
+
steps_x = Image.calc_sample_steps( source_width-@top_x, @width, center: center_x )
|
220
|
+
steps_y = Image.calc_sample_steps( source_height-@top_y, @height, center: center_y )
|
221
|
+
|
222
|
+
pix = if debug
|
223
|
+
img.pixelate_debug( steps_x, steps_y,
|
224
|
+
top_x: @top_x,
|
225
|
+
top_y: @top_y )
|
226
|
+
else
|
227
|
+
img.pixelate( steps_x, steps_y,
|
228
|
+
top_x: @top_x,
|
229
|
+
top_y: @top_y )
|
230
|
+
end
|
231
|
+
## todo/check: keep usingu slug e.g. 0001.png or "plain" 1.png - why? why not?
|
232
|
+
## slug = "%04d" % id
|
233
|
+
pix.save( outpath )
|
234
|
+
|
235
|
+
if zoom
|
236
|
+
outpath = "./#{@slug}/#{@width}x#{@height}/#{id}@#{zoom}x.png"
|
237
|
+
pix.zoom( zoom ).save( outpath )
|
238
|
+
end
|
239
|
+
else
|
240
|
+
puts "!! ERROR - unknown/unsupported dimension - #{img.width}x#{img.height}; sorry - tried:"
|
241
|
+
pp @sources
|
242
|
+
exit 1
|
243
|
+
end
|
244
|
+
end
|
245
|
+
end
|
246
|
+
|
247
|
+
|
248
|
+
|
249
|
+
def meta_url( id: )
|
250
|
+
src = @token_base.gsub( '{id}', id.to_s )
|
251
|
+
|
252
|
+
## quick ipfs (interplanetary file system) hack - make more reusabele!!!
|
253
|
+
src = handle_ipfs( src )
|
254
|
+
src
|
255
|
+
end
|
256
|
+
alias_method :token_url, :meta_url
|
257
|
+
|
258
|
+
|
259
|
+
def image_url( id:,
|
260
|
+
direct: @image_base ? true : false )
|
261
|
+
src = if direct && @image_base
|
262
|
+
###
|
263
|
+
## todo/fix:
|
264
|
+
## change image_base_id_format
|
265
|
+
## to image_base proc with para id and call proc!!!!
|
266
|
+
if @image_base_id_format
|
267
|
+
@image_base.gsub( '{id}', @image_base_id_format % id )
|
268
|
+
else
|
269
|
+
@image_base.gsub( '{id}', id.to_s )
|
270
|
+
end
|
271
|
+
else
|
272
|
+
## todo/check - change/rename data to meta - why? why not?
|
273
|
+
data = Meta.read( "./#{@slug}/token/#{id}.json" )
|
274
|
+
|
275
|
+
meta_name = data.name
|
276
|
+
meta_image = data.image
|
277
|
+
|
278
|
+
puts "==> #{id} - #{@slug}..."
|
279
|
+
puts " name: #{meta_name}"
|
280
|
+
puts " image: #{meta_image}"
|
281
|
+
meta_image
|
282
|
+
end
|
283
|
+
src
|
284
|
+
|
285
|
+
## quick ipfs (interplanetary file system) hack - make more reusabele!!!
|
286
|
+
src = handle_ipfs( src )
|
287
|
+
src
|
288
|
+
end
|
289
|
+
|
290
|
+
|
291
|
+
|
292
|
+
def download_meta( range=_range, force: false )
|
293
|
+
start = Time.now
|
294
|
+
delay_in_s = 0.3
|
295
|
+
|
296
|
+
range.each do |id|
|
297
|
+
outpath = "./#{@slug}/token/#{id}.json"
|
298
|
+
if !force && File.exist?( outpath )
|
299
|
+
next ## note: skip if file already exists
|
300
|
+
end
|
301
|
+
|
302
|
+
dirname = File.dirname( outpath )
|
303
|
+
FileUtils.mkdir_p( dirname ) unless Dir.exist?( dirname )
|
304
|
+
|
305
|
+
puts "==> #{id} - #{@slug}..."
|
306
|
+
|
307
|
+
token_src = meta_url( id: id )
|
308
|
+
copy_json( token_src, outpath )
|
309
|
+
|
310
|
+
stop = Time.now
|
311
|
+
diff = stop - start
|
312
|
+
puts " download token metadata in #{diff} sec(s)"
|
313
|
+
|
314
|
+
mins = diff / 60 ## todo - use floor or such?
|
315
|
+
secs = diff % 60
|
316
|
+
puts "up #{mins} mins #{secs} secs (total #{diff} secs)"
|
317
|
+
|
318
|
+
puts "sleeping #{delay_in_s}s..."
|
319
|
+
sleep( delay_in_s )
|
320
|
+
end
|
321
|
+
end
|
322
|
+
|
323
|
+
|
324
|
+
## note: default to direct true if image_base present/availabe
|
325
|
+
## otherwise to false
|
326
|
+
def download_images( range=_range, force: false,
|
327
|
+
direct: @image_base ? true : false )
|
328
|
+
start = Time.now
|
329
|
+
delay_in_s = 0.3
|
330
|
+
|
331
|
+
range.each do |id|
|
332
|
+
## note: for now assumes only .png format!!!
|
333
|
+
## todo - check for more format - why? why not?
|
334
|
+
outpath = "./#{@slug}/token-i/#{id}.png"
|
335
|
+
if !force && File.exist?( outpath )
|
336
|
+
next ## note: skip if file already exists
|
337
|
+
end
|
338
|
+
|
339
|
+
|
340
|
+
image_src = image_url( id: id, direct: direct )
|
341
|
+
|
342
|
+
## note: will auto-add format file extension (e.g. .png, .jpg)
|
343
|
+
## depending on http content type!!!!!
|
344
|
+
start_copy = Time.now
|
345
|
+
copy_image( image_src, "./#{@slug}/token-i/#{id}" )
|
346
|
+
|
347
|
+
stop = Time.now
|
348
|
+
|
349
|
+
diff = stop - start_copy
|
350
|
+
puts " download image in #{diff} sec(s)"
|
351
|
+
|
352
|
+
diff = stop - start
|
353
|
+
mins = diff / 60 ## todo - use floor or such?
|
354
|
+
secs = diff % 60
|
355
|
+
puts "up #{mins} mins #{secs} secs (total #{diff} secs)"
|
356
|
+
|
357
|
+
puts "sleeping #{delay_in_s}s..."
|
358
|
+
sleep( delay_in_s )
|
359
|
+
end
|
360
|
+
end
|
361
|
+
|
362
|
+
end # class TokenCollection
|
data/lib/artbase/collection.rb
CHANGED
@@ -1,12 +1,12 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
require_relative 'collection/token'
|
10
|
-
require_relative 'collection/image'
|
11
|
-
require_relative 'collection/opensea'
|
12
|
-
|
1
|
+
|
2
|
+
|
3
|
+
|
4
|
+
|
5
|
+
|
6
|
+
|
7
|
+
|
8
|
+
require_relative 'collection/base'
|
9
|
+
require_relative 'collection/token'
|
10
|
+
require_relative 'collection/image'
|
11
|
+
require_relative 'collection/opensea'
|
12
|
+
|