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