mp3info 0.6.18 → 0.8.5
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +15 -0
- metadata +38 -56
- data/.gitignore +0 -12
- data/History.txt +0 -153
- data/README.rdoc +0 -68
- data/lib/mp3info.rb +0 -714
- data/lib/mp3info/extension_modules.rb +0 -46
- data/lib/mp3info/id3v2.rb +0 -437
- data/mp3info.gemspec +0 -17
- data/test/fixtures.yml +0 -86
- data/test/test_ruby-mp3info.rb +0 -572
@@ -1,46 +0,0 @@
|
|
1
|
-
# coding:utf-8
|
2
|
-
# License:: Ruby
|
3
|
-
# Author:: Guillaume Pierronnet (mailto:moumar_AT__rubyforge_DOT_org)
|
4
|
-
# Website:: http://ruby-mp3info.rubyforge.org/
|
5
|
-
|
6
|
-
class Mp3Info
|
7
|
-
module HashKeys #:nodoc:
|
8
|
-
### lets you specify hash["key"] as hash.key
|
9
|
-
### this came from CodingInRuby on RubyGarden
|
10
|
-
### http://www.rubygarden.org/ruby?CodingInRuby
|
11
|
-
def method_missing(meth,*args)
|
12
|
-
m = meth.id2name
|
13
|
-
if /=$/ =~ m
|
14
|
-
self[m.chop] = (args.length<2 ? args[0] : args)
|
15
|
-
else
|
16
|
-
self[m]
|
17
|
-
end
|
18
|
-
end
|
19
|
-
end
|
20
|
-
|
21
|
-
class ::String
|
22
|
-
if RUBY_VERSION < "1.9.0"
|
23
|
-
alias getbyte []
|
24
|
-
else
|
25
|
-
def getbyte(i)
|
26
|
-
self[i].ord unless self[i].nil?
|
27
|
-
end
|
28
|
-
end
|
29
|
-
end
|
30
|
-
|
31
|
-
module Mp3FileMethods #:nodoc:
|
32
|
-
if RUBY_VERSION < "1.9.0"
|
33
|
-
def getbyte
|
34
|
-
getc
|
35
|
-
end
|
36
|
-
end
|
37
|
-
|
38
|
-
def get32bits
|
39
|
-
(getbyte << 24) + (getbyte << 16) + (getbyte << 8) + getbyte
|
40
|
-
end
|
41
|
-
|
42
|
-
def get_syncsafe
|
43
|
-
(getbyte << 21) + (getbyte << 14) + (getbyte << 7) + getbyte
|
44
|
-
end
|
45
|
-
end
|
46
|
-
end
|
data/lib/mp3info/id3v2.rb
DELETED
@@ -1,437 +0,0 @@
|
|
1
|
-
# coding:utf-8
|
2
|
-
# License:: Ruby
|
3
|
-
# Author:: Guillaume Pierronnet (mailto:moumar_AT__rubyforge_DOT_org)
|
4
|
-
# Website:: http://ruby-mp3info.rubyforge.org/
|
5
|
-
|
6
|
-
require "delegate"
|
7
|
-
require "iconv"
|
8
|
-
|
9
|
-
require "mp3info/extension_modules"
|
10
|
-
|
11
|
-
class ID3v2Error < StandardError ; end
|
12
|
-
|
13
|
-
# This class can be used to decode id3v2 tags from files, like .mp3 or .ape for example.
|
14
|
-
# It works like a hash, where key represents the tag name as 3 or 4 upper case letters
|
15
|
-
# (respectively related to 2.2 and 2.3+ tag) and value represented as array or raw value.
|
16
|
-
# Written version is always 2.3.
|
17
|
-
class ID3v2 < DelegateClass(Hash)
|
18
|
-
|
19
|
-
# Major version used when writing tags
|
20
|
-
WRITE_VERSION = 3
|
21
|
-
|
22
|
-
TAGS = {
|
23
|
-
"AENC" => "Audio encryption",
|
24
|
-
"APIC" => "Attached picture",
|
25
|
-
"COMM" => "Comments",
|
26
|
-
"COMR" => "Commercial frame",
|
27
|
-
"ENCR" => "Encryption method registration",
|
28
|
-
"EQUA" => "Equalization",
|
29
|
-
"ETCO" => "Event timing codes",
|
30
|
-
"GEOB" => "General encapsulated object",
|
31
|
-
"GRID" => "Group identification registration",
|
32
|
-
"IPLS" => "Involved people list",
|
33
|
-
"LINK" => "Linked information",
|
34
|
-
"MCDI" => "Music CD identifier",
|
35
|
-
"MLLT" => "MPEG location lookup table",
|
36
|
-
"OWNE" => "Ownership frame",
|
37
|
-
"PRIV" => "Private frame",
|
38
|
-
"PCNT" => "Play counter",
|
39
|
-
"POPM" => "Popularimeter",
|
40
|
-
"POSS" => "Position synchronisation frame",
|
41
|
-
"RBUF" => "Recommended buffer size",
|
42
|
-
"RVAD" => "Relative volume adjustment",
|
43
|
-
"RVRB" => "Reverb",
|
44
|
-
"SYLT" => "Synchronized lyric/text",
|
45
|
-
"SYTC" => "Synchronized tempo codes",
|
46
|
-
"TALB" => "Album/Movie/Show title",
|
47
|
-
"TBPM" => "BPM (beats per minute)",
|
48
|
-
"TCOM" => "Composer",
|
49
|
-
"TCON" => "Content type",
|
50
|
-
"TCOP" => "Copyright message",
|
51
|
-
"TDAT" => "Date",
|
52
|
-
"TDLY" => "Playlist delay",
|
53
|
-
"TENC" => "Encoded by",
|
54
|
-
"TEXT" => "Lyricist/Text writer",
|
55
|
-
"TFLT" => "File type",
|
56
|
-
"TIME" => "Time",
|
57
|
-
"TIT1" => "Content group description",
|
58
|
-
"TIT2" => "Title/songname/content description",
|
59
|
-
"TIT3" => "Subtitle/Description refinement",
|
60
|
-
"TKEY" => "Initial key",
|
61
|
-
"TLAN" => "Language(s)",
|
62
|
-
"TLEN" => "Length",
|
63
|
-
"TMED" => "Media type",
|
64
|
-
"TOAL" => "Original album/movie/show title",
|
65
|
-
"TOFN" => "Original filename",
|
66
|
-
"TOLY" => "Original lyricist(s)/text writer(s)",
|
67
|
-
"TOPE" => "Original artist(s)/performer(s)",
|
68
|
-
"TORY" => "Original release year",
|
69
|
-
"TOWN" => "File owner/licensee",
|
70
|
-
"TPE1" => "Lead performer(s)/Soloist(s)",
|
71
|
-
"TPE2" => "Band/orchestra/accompaniment",
|
72
|
-
"TPE3" => "Conductor/performer refinement",
|
73
|
-
"TPE4" => "Interpreted, remixed, or otherwise modified by",
|
74
|
-
"TPOS" => "Part of a set",
|
75
|
-
"TPUB" => "Publisher",
|
76
|
-
"TRCK" => "Track number/Position in set",
|
77
|
-
"TRDA" => "Recording dates",
|
78
|
-
"TRSN" => "Internet radio station name",
|
79
|
-
"TRSO" => "Internet radio station owner",
|
80
|
-
"TSIZ" => "Size",
|
81
|
-
"TSRC" => "ISRC (international standard recording code)",
|
82
|
-
"TSSE" => "Software/Hardware and settings used for encoding",
|
83
|
-
"TYER" => "Year",
|
84
|
-
"TXXX" => "User defined text information frame",
|
85
|
-
"UFID" => "Unique file identifier",
|
86
|
-
"USER" => "Terms of use",
|
87
|
-
"USLT" => "Unsychronized lyric/text transcription",
|
88
|
-
"WCOM" => "Commercial information",
|
89
|
-
"WCOP" => "Copyright/Legal information",
|
90
|
-
"WOAF" => "Official audio file webpage",
|
91
|
-
"WOAR" => "Official artist/performer webpage",
|
92
|
-
"WOAS" => "Official audio source webpage",
|
93
|
-
"WORS" => "Official internet radio station homepage",
|
94
|
-
"WPAY" => "Payment",
|
95
|
-
"WPUB" => "Publishers official webpage",
|
96
|
-
"WXXX" => "User defined URL link frame"
|
97
|
-
}
|
98
|
-
|
99
|
-
# Translate V2 to V3 tags
|
100
|
-
TAG_MAPPING_2_2_to_2_3 = {
|
101
|
-
"BUF" => "RBUF",
|
102
|
-
"COM" => "COMM",
|
103
|
-
"CRA" => "AENC",
|
104
|
-
"EQU" => "EQUA",
|
105
|
-
"ETC" => "ETCO",
|
106
|
-
"GEO" => "GEOB",
|
107
|
-
"MCI" => "MCDI",
|
108
|
-
"MLL" => "MLLT",
|
109
|
-
"PIC" => "APIC",
|
110
|
-
"POP" => "POPM",
|
111
|
-
"REV" => "RVRB",
|
112
|
-
"RVA" => "RVAD",
|
113
|
-
"SLT" => "SYLT",
|
114
|
-
"STC" => "SYTC",
|
115
|
-
"TAL" => "TALB",
|
116
|
-
"TBP" => "TBPM",
|
117
|
-
"TCM" => "TCOM",
|
118
|
-
"TCO" => "TCON",
|
119
|
-
"TCR" => "TCOP",
|
120
|
-
"TDA" => "TDAT",
|
121
|
-
"TDY" => "TDLY",
|
122
|
-
"TEN" => "TENC",
|
123
|
-
"TFT" => "TFLT",
|
124
|
-
"TIM" => "TIME",
|
125
|
-
"TKE" => "TKEY",
|
126
|
-
"TLA" => "TLAN",
|
127
|
-
"TLE" => "TLEN",
|
128
|
-
"TMT" => "TMED",
|
129
|
-
"TOA" => "TOPE",
|
130
|
-
"TOF" => "TOFN",
|
131
|
-
"TOL" => "TOLY",
|
132
|
-
"TOR" => "TORY",
|
133
|
-
"TOT" => "TOAL",
|
134
|
-
"TP1" => "TPE1",
|
135
|
-
"TP2" => "TPE2",
|
136
|
-
"TP3" => "TPE3",
|
137
|
-
"TP4" => "TPE4",
|
138
|
-
"TPA" => "TPOS",
|
139
|
-
"TPB" => "TPUB",
|
140
|
-
"TRC" => "TSRC",
|
141
|
-
"TRD" => "TRDA",
|
142
|
-
"TRK" => "TRCK",
|
143
|
-
"TSI" => "TSIZ",
|
144
|
-
"TSS" => "TSSE",
|
145
|
-
"TT1" => "TIT1",
|
146
|
-
"TT2" => "TIT2",
|
147
|
-
"TT3" => "TIT3",
|
148
|
-
"TXT" => "TEXT",
|
149
|
-
"TXX" => "TXXX",
|
150
|
-
"TYE" => "TYER",
|
151
|
-
"UFI" => "UFID",
|
152
|
-
"ULT" => "USLT",
|
153
|
-
"WAF" => "WOAF",
|
154
|
-
"WAR" => "WOAR",
|
155
|
-
"WAS" => "WOAS",
|
156
|
-
"WCM" => "WCOM",
|
157
|
-
"WCP" => "WCOP",
|
158
|
-
"WPB" => "WPB",
|
159
|
-
"WXX" => "WXXX",
|
160
|
-
}
|
161
|
-
|
162
|
-
# See id3v2.4.0-structure document, at section 4.
|
163
|
-
TEXT_ENCODINGS = ["iso-8859-1", "utf-16", "utf-16be", "utf-8"]
|
164
|
-
|
165
|
-
include Mp3Info::HashKeys
|
166
|
-
|
167
|
-
# this is the position in the file where the tag really ends
|
168
|
-
attr_reader :io_position
|
169
|
-
|
170
|
-
# :+lang+: for writing comments
|
171
|
-
#
|
172
|
-
# :+encoding+: one of the string of +TEXT_ENCODINGS+,
|
173
|
-
# used as a source and destination encoding respectively
|
174
|
-
# for read and write tag2 values.
|
175
|
-
attr_reader :options
|
176
|
-
|
177
|
-
# possible options are described above ('options' attribute)
|
178
|
-
# you can access this object like an hash, with [] and []= methods
|
179
|
-
# special cases are ["disc_number"] and ["disc_total"] mirroring TPOS attribute
|
180
|
-
def initialize(options = {})
|
181
|
-
@options = {
|
182
|
-
:lang => "ENG",
|
183
|
-
:encoding => 'utf-8'
|
184
|
-
}
|
185
|
-
|
186
|
-
@options.update(options)
|
187
|
-
@text_encoding_index = TEXT_ENCODINGS.index(@options[:encoding])
|
188
|
-
|
189
|
-
unless @text_encoding_index
|
190
|
-
raise(ArgumentError, "bad id3v2 text encoding specified")
|
191
|
-
end
|
192
|
-
|
193
|
-
@hash = {}
|
194
|
-
#TAGS.keys.each { |k| @hash[k] = nil }
|
195
|
-
@hash_orig = {}
|
196
|
-
super(@hash)
|
197
|
-
@parsed = false
|
198
|
-
@version_maj = @version_min = nil
|
199
|
-
end
|
200
|
-
|
201
|
-
# does this tag has been correctly read ?
|
202
|
-
def parsed?
|
203
|
-
@parsed
|
204
|
-
end
|
205
|
-
|
206
|
-
# does this tag has been changed ?
|
207
|
-
def changed?
|
208
|
-
@hash_orig != @hash
|
209
|
-
end
|
210
|
-
|
211
|
-
# full version of this tag (like "2.3.0") or nil
|
212
|
-
# if tag was not correctly read
|
213
|
-
def version
|
214
|
-
if @version_maj && @version_min
|
215
|
-
"2.#{@version_maj}.#{@version_min}"
|
216
|
-
else
|
217
|
-
nil
|
218
|
-
end
|
219
|
-
end
|
220
|
-
|
221
|
-
### gets id3v2 tag information from io object (must support #seek() method)
|
222
|
-
def from_io(io)
|
223
|
-
@io = io
|
224
|
-
original_pos = @io.pos
|
225
|
-
@io.extend(Mp3Info::Mp3FileMethods)
|
226
|
-
version_maj, version_min, flags = @io.read(3).unpack("CCB4")
|
227
|
-
@unsync, ext_header, experimental, footer = (0..3).collect { |i| flags[i].chr == '1' }
|
228
|
-
raise(ID3v2Error, "can't find version_maj ('#{version_maj}')") unless [2, 3, 4].include?(version_maj)
|
229
|
-
@version_maj, @version_min = version_maj, version_min
|
230
|
-
@tag_length = @io.get_syncsafe
|
231
|
-
|
232
|
-
@parsed = true
|
233
|
-
begin
|
234
|
-
case @version_maj
|
235
|
-
when 2
|
236
|
-
read_id3v2_2_frames
|
237
|
-
when 3, 4
|
238
|
-
# seek past extended header if present
|
239
|
-
@io.seek(@io.get_syncsafe - 4, IO::SEEK_CUR) if ext_header
|
240
|
-
read_id3v2_3_frames
|
241
|
-
end
|
242
|
-
rescue ID3v2Error => e
|
243
|
-
warn("warning: id3v2 tag not fully parsed: #{e.message}")
|
244
|
-
end
|
245
|
-
@io_position = @io.pos
|
246
|
-
@tag_length = @io_position - original_pos
|
247
|
-
|
248
|
-
@hash_orig = @hash.dup
|
249
|
-
#no more reading
|
250
|
-
@io = nil
|
251
|
-
end
|
252
|
-
|
253
|
-
# dump tag for writing. Version is always 2.#{WRITE_VERSION}.0.
|
254
|
-
def to_bin
|
255
|
-
#TODO handle of @tag2[TLEN"]
|
256
|
-
#TODO add of crc
|
257
|
-
#TODO add restrictions tag
|
258
|
-
|
259
|
-
tag = ""
|
260
|
-
@hash.each do |k, v|
|
261
|
-
next unless v
|
262
|
-
next if v.respond_to?("empty?") and v.empty?
|
263
|
-
|
264
|
-
# Automagically translate V2 to V3 tags
|
265
|
-
k = TAG_MAPPING_2_2_to_2_3[k] if TAG_MAPPING_2_2_to_2_3.has_key?(k)
|
266
|
-
|
267
|
-
# doesn't encode id3v2.2 tags, which have 3 characters
|
268
|
-
next if k.size != 4
|
269
|
-
|
270
|
-
# Output one flag for each array element, or one only if it's not an array
|
271
|
-
[v].flatten.each do |value|
|
272
|
-
data = encode_tag(k, value.to_s, WRITE_VERSION)
|
273
|
-
#data << "\x00"*2 #End of tag
|
274
|
-
|
275
|
-
tag << k[0,4] #4 characte max for a tag's key
|
276
|
-
#tag << to_syncsafe(data.size) #+1 because of the language encoding byte
|
277
|
-
size = data.size
|
278
|
-
if RUBY_VERSION >= "1.9.0"
|
279
|
-
size = data.dup.force_encoding("binary").size
|
280
|
-
end
|
281
|
-
tag << [size].pack("N") #+1 because of the language encoding byte
|
282
|
-
tag << "\x00"*2 #flags
|
283
|
-
tag << data
|
284
|
-
end
|
285
|
-
end
|
286
|
-
|
287
|
-
tag_str = "ID3"
|
288
|
-
#version_maj, version_min, unsync, ext_header, experimental, footer
|
289
|
-
tag_str << [ WRITE_VERSION, 0, "0000" ].pack("CCB4")
|
290
|
-
tag_str << [to_syncsafe(tag.size)].pack("N")
|
291
|
-
tag_str << tag
|
292
|
-
puts "tag in binary format: #{tag_str.inspect}" if $DEBUG
|
293
|
-
tag_str
|
294
|
-
end
|
295
|
-
|
296
|
-
private
|
297
|
-
|
298
|
-
def encode_tag(name, value, version)
|
299
|
-
puts "encode_tag(#{name.inspect}, #{value.inspect}, #{version})" if $DEBUG
|
300
|
-
|
301
|
-
text_encoding_index = @text_encoding_index
|
302
|
-
if (name.index("T") == 0 || name == "COMM" ) && version == 3
|
303
|
-
# in id3v2.3 tags, there is only 2 encodings possible
|
304
|
-
transcoded_value = value
|
305
|
-
if text_encoding_index >= 2
|
306
|
-
begin
|
307
|
-
transcoded_value = Iconv.iconv(TEXT_ENCODINGS[1],
|
308
|
-
TEXT_ENCODINGS[text_encoding_index],
|
309
|
-
value).first
|
310
|
-
rescue Iconv::Failure
|
311
|
-
transcoded_value = value
|
312
|
-
end
|
313
|
-
text_encoding_index = 1
|
314
|
-
end
|
315
|
-
end
|
316
|
-
|
317
|
-
case name
|
318
|
-
when "COMM"
|
319
|
-
puts "encode COMM: enc: #{text_encoding_index}, lang: #{@options[:lang]}, str: #{transcoded_value.dump}" if $DEBUG
|
320
|
-
[ text_encoding_index , @options[:lang], 0, transcoded_value ].pack("ca3ca*")
|
321
|
-
when /^T/
|
322
|
-
text_encoding_index.chr + transcoded_value
|
323
|
-
else
|
324
|
-
value
|
325
|
-
end
|
326
|
-
end
|
327
|
-
|
328
|
-
### Read a tag from file and perform UNICODE translation if needed
|
329
|
-
def decode_tag(name, raw_value)
|
330
|
-
puts("decode_tag(#{name.inspect}, #{raw_value.inspect})") if $DEBUG
|
331
|
-
case name
|
332
|
-
when /^COM/
|
333
|
-
#FIXME improve this
|
334
|
-
encoding, lang, str = raw_value.unpack("ca3a*")
|
335
|
-
out = raw_value.split(0.chr).last
|
336
|
-
when /^T/
|
337
|
-
encoding = raw_value.getbyte(0) # language encoding (see TEXT_ENCODINGS constant)
|
338
|
-
out = raw_value[1..-1]
|
339
|
-
# we need to convert the string in order to match
|
340
|
-
# the requested encoding
|
341
|
-
if encoding && TEXT_ENCODINGS[encoding] && out && encoding != @text_encoding_index
|
342
|
-
begin
|
343
|
-
out = Iconv.iconv("#{@options[:encoding]}//IGNORE", TEXT_ENCODINGS[encoding], out).first
|
344
|
-
rescue Iconv::Failure
|
345
|
-
end
|
346
|
-
end
|
347
|
-
# remove padding zeros for textual tags
|
348
|
-
out.sub!(/\0*$/, '') unless out.nil?
|
349
|
-
return out
|
350
|
-
else
|
351
|
-
return raw_value
|
352
|
-
end
|
353
|
-
end
|
354
|
-
|
355
|
-
### reads id3 ver 2.3.x/2.4.x frames and adds the contents to @tag2 hash
|
356
|
-
### NOTE: the id3v2 header does not take padding zero's into consideration
|
357
|
-
def read_id3v2_3_frames
|
358
|
-
loop do # there are 2 ways to end the loop
|
359
|
-
name = @io.read(4)
|
360
|
-
if name.nil? || name.getbyte(0) == 0 || name == "MP3e" #bug caused by old tagging application "mp3ext" ( http://www.mutschler.de/mp3ext/ )
|
361
|
-
@io.seek(-4, IO::SEEK_CUR) # 1. find a padding zero,
|
362
|
-
seek_to_v2_end
|
363
|
-
break
|
364
|
-
else
|
365
|
-
if @version_maj == 4
|
366
|
-
size = @io.get_syncsafe
|
367
|
-
else
|
368
|
-
size = @io.get32bits
|
369
|
-
end
|
370
|
-
@io.seek(2, IO::SEEK_CUR) # skip flags
|
371
|
-
puts "name '#{name}' size #{size}" if $DEBUG
|
372
|
-
add_value_to_tag2(name, size)
|
373
|
-
end
|
374
|
-
break if @io.pos >= @tag_length # 2. reach length from header
|
375
|
-
end
|
376
|
-
end
|
377
|
-
|
378
|
-
### reads id3 ver 2.2.x frames and adds the contents to @tag2 hash
|
379
|
-
### NOTE: the id3v2 header does not take padding zero's into consideration
|
380
|
-
def read_id3v2_2_frames
|
381
|
-
loop do
|
382
|
-
name = @io.read(3)
|
383
|
-
if name.nil? || name.getbyte(0) == 0
|
384
|
-
@io.seek(-3, IO::SEEK_CUR)
|
385
|
-
seek_to_v2_end
|
386
|
-
break
|
387
|
-
else
|
388
|
-
size = (@io.getbyte << 16) + (@io.getbyte << 8) + @io.getbyte
|
389
|
-
add_value_to_tag2(name, size)
|
390
|
-
break if @io.pos >= @tag_length
|
391
|
-
end
|
392
|
-
end
|
393
|
-
end
|
394
|
-
|
395
|
-
### Add data to tag2["name"]
|
396
|
-
### read lang_encoding, decode data if unicode and
|
397
|
-
### create an array if the key already exists in the tag
|
398
|
-
def add_value_to_tag2(name, size)
|
399
|
-
puts "add_value_to_tag2" if $DEBUG
|
400
|
-
|
401
|
-
if size > 50_000_000
|
402
|
-
raise ID3v2Error, "tag size is > 50_000_000"
|
403
|
-
end
|
404
|
-
|
405
|
-
data_io = @io.read(size)
|
406
|
-
data = decode_tag(name, data_io)
|
407
|
-
|
408
|
-
if self["TPOS"] =~ /(\d+)\s*\/\s*(\d+)/
|
409
|
-
self["disc_number"] = $1.to_i
|
410
|
-
self["disc_total"] = $2.to_i
|
411
|
-
end
|
412
|
-
|
413
|
-
if self.keys.include?(name)
|
414
|
-
unless self[name].is_a?(Array)
|
415
|
-
self[name] = [ self[name] ]
|
416
|
-
end
|
417
|
-
self[name] << data
|
418
|
-
else
|
419
|
-
self[name] = data
|
420
|
-
end
|
421
|
-
p data if $DEBUG
|
422
|
-
end
|
423
|
-
|
424
|
-
### runs thru @file one char at a time looking for best guess of first MPEG
|
425
|
-
### frame, which should be first 0xff byte after id3v2 padding zero's
|
426
|
-
def seek_to_v2_end
|
427
|
-
until @io.getbyte == 0xff
|
428
|
-
raise ID3v2Error, "got EOF before finding id3v2 end" if @io.eof?
|
429
|
-
end
|
430
|
-
@io.seek(-1, IO::SEEK_CUR)
|
431
|
-
end
|
432
|
-
|
433
|
-
### convert an 32 integer to a syncsafe string
|
434
|
-
def to_syncsafe(num)
|
435
|
-
( (num<<3) & 0x7f000000 ) + ( (num<<2) & 0x7f0000 ) + ( (num<<1) & 0x7f00 ) + ( num & 0x7f )
|
436
|
-
end
|
437
|
-
end
|