ipod_db 0.2.4
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +2 -0
- data/.travis.yml +8 -0
- data/Gemfile +2 -0
- data/Guardfile +13 -0
- data/HISTORY +9 -0
- data/LICENSE +24 -0
- data/README.md +132 -0
- data/Rakefile +41 -0
- data/TODO +3 -0
- data/bin/ipod +245 -0
- data/doc/ITunesDB - wikiPodLinux.html +3979 -0
- data/ipod_db.gemspec +38 -0
- data/lib/bindata/itypes.rb +25 -0
- data/lib/ipod_db/version.rb +3 -0
- data/lib/ipod_db.rb +203 -0
- data/lib/pretty.rb +17 -0
- data/lib/spread.rb +32 -0
- data/spec/ipod_db_spec.rb +159 -0
- data/spec/spec_helper.rb +53 -0
- data/spec/spread_spec.rb +31 -0
- data/test_data/iPod_Control/iTunes/iTunesDB +0 -0
- data/test_data/iPod_Control/iTunes/iTunesDB.ext +135 -0
- data/test_data/iPod_Control/iTunes/iTunesPState +0 -0
- data/test_data/iPod_Control/iTunes/iTunesSD +0 -0
- data/test_data/iPod_Control/iTunes/iTunesShuffle +0 -0
- data/test_data/iPod_Control/iTunes/iTunesStats +0 -0
- data/test_data.rb +14 -0
- metadata +332 -0
data/ipod_db.gemspec
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
lib = File.expand_path('../lib', __FILE__)
|
2
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
3
|
+
require 'ipod_db/version'
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = 'ipod_db'
|
7
|
+
s.version = IpodDB::VERSION
|
8
|
+
s.summary = 'ipod database access'
|
9
|
+
s.description = 'Access iPod Shuffle 2nd gen from ruby'
|
10
|
+
s.author = 'Artem Baguinski'
|
11
|
+
s.email = 'femistofel@gmail.com'
|
12
|
+
s.homepage = 'https://github.com/artm/ipod_db'
|
13
|
+
s.license = 'Public Domain'
|
14
|
+
|
15
|
+
s.files = `git ls-files`.split($/)
|
16
|
+
s.executables = s.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
17
|
+
s.test_files = s.files.grep(%r{^(test|spec|features)/})
|
18
|
+
s.require_paths = ["lib"]
|
19
|
+
|
20
|
+
s.add_runtime_dependency 'bindata'
|
21
|
+
s.add_runtime_dependency 'map'
|
22
|
+
s.add_runtime_dependency 'main'
|
23
|
+
s.add_runtime_dependency 'smart_colored'
|
24
|
+
s.add_runtime_dependency 'taglib-ruby'
|
25
|
+
s.add_runtime_dependency 'ruby-progressbar'
|
26
|
+
s.add_runtime_dependency 'highline'
|
27
|
+
|
28
|
+
s.add_development_dependency 'rake'
|
29
|
+
s.add_development_dependency 'bundler', '~> 1.3'
|
30
|
+
# not sure where this belongs, they are part of my development process
|
31
|
+
s.add_development_dependency 'purdytest'
|
32
|
+
s.add_development_dependency 'guard'
|
33
|
+
s.add_development_dependency 'guard-minitest'
|
34
|
+
s.add_development_dependency 'guard-bundler'
|
35
|
+
s.add_development_dependency 'rb-inotify'
|
36
|
+
s.add_development_dependency 'rb-fsevent'
|
37
|
+
s.add_development_dependency 'libnotify'
|
38
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
require 'bindata'
|
2
|
+
|
3
|
+
class Bool24 < BinData::Primitive
|
4
|
+
uint24le :int
|
5
|
+
def get; self.int==0 ? false : true ; end
|
6
|
+
def set(v) self.int = v ? 1 : 0 ; end
|
7
|
+
end
|
8
|
+
|
9
|
+
class Bool8 < BinData::Primitive
|
10
|
+
uint8 :int
|
11
|
+
def get; self.int==0 ? false : true ; end
|
12
|
+
def set(v) self.int = v ? 1 : 0 ; end
|
13
|
+
end
|
14
|
+
|
15
|
+
class EncodedString < BinData::Primitive
|
16
|
+
string :str, length: :length
|
17
|
+
def get
|
18
|
+
self.str.force_encoding('UTF-16LE').encode('UTF-8').sub(/\u0000*$/,'')
|
19
|
+
end
|
20
|
+
def set(v)
|
21
|
+
self.str = v.encode('UTF-16LE')
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
|
data/lib/ipod_db.rb
ADDED
@@ -0,0 +1,203 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
require 'bindata'
|
3
|
+
require 'bindata/itypes'
|
4
|
+
require 'map'
|
5
|
+
require 'pathname'
|
6
|
+
|
7
|
+
require 'ipod_db/version'
|
8
|
+
|
9
|
+
class Hash
|
10
|
+
def subset *args
|
11
|
+
subset = {}
|
12
|
+
args.each do |arg|
|
13
|
+
subset[arg] = self[arg] if self.include? arg
|
14
|
+
end
|
15
|
+
subset
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
class IpodDB
|
20
|
+
attr_reader :playback_state
|
21
|
+
|
22
|
+
ExtToFileType = {
|
23
|
+
'.mp3' => 1,
|
24
|
+
'.aa' => 1,
|
25
|
+
'.m4a' => 2,
|
26
|
+
'.m4b' => 2,
|
27
|
+
'.m4p' => 2,
|
28
|
+
'.wav' => 4
|
29
|
+
}
|
30
|
+
|
31
|
+
class NotAnIpod < RuntimeError
|
32
|
+
def initialize path
|
33
|
+
super "#{path} doesn't appear to be an iPod"
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def initialize root_dir
|
38
|
+
@root_dir = root_dir
|
39
|
+
begin
|
40
|
+
read
|
41
|
+
rescue Errno::ENOENT
|
42
|
+
raise NotAnIpod.new @root_dir
|
43
|
+
rescue IOError => error
|
44
|
+
puts "Corrupt database, creating a-new"
|
45
|
+
init_db
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def IpodDB.looks_like_ipod? path
|
50
|
+
Dir.exists? File.join(path,'iPod_Control','iTunes')
|
51
|
+
end
|
52
|
+
|
53
|
+
def read
|
54
|
+
@playback_state = read_records PState, 'PState'
|
55
|
+
stats = read_records Stats, 'Stats'
|
56
|
+
sd = read_records SD, 'SD'
|
57
|
+
@tracks = Map.new
|
58
|
+
stats.records.each_with_index do |stat,i|
|
59
|
+
h = stat.snapshot.merge( sd.records[i].snapshot )
|
60
|
+
h.delete :reclen
|
61
|
+
@tracks[ h[:filename].to_s ] = h
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def init_db
|
66
|
+
@playback_state = PState.new
|
67
|
+
@tracks = Map.new
|
68
|
+
end
|
69
|
+
|
70
|
+
def current_filename
|
71
|
+
@tracks.keys[ @playback_state.trackno ]
|
72
|
+
end
|
73
|
+
|
74
|
+
def read_records bindata, file_suffix
|
75
|
+
File.open make_filename(file_suffix) do |io|
|
76
|
+
bindata.read io
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def include? track ; @tracks.include? track ; end
|
81
|
+
|
82
|
+
def update *args
|
83
|
+
opts = Map.options(args)
|
84
|
+
new_books = opts.getopt :books, default: []
|
85
|
+
new_songs = opts.getopt :songs, default: []
|
86
|
+
new_tracks = new_books + new_songs
|
87
|
+
prev_current_filename = current_filename
|
88
|
+
|
89
|
+
old_tracks = @tracks.keys.clone # clone because otherwise it'll change during iteration
|
90
|
+
old_tracks.each do |filename|
|
91
|
+
@tracks.delete filename unless new_tracks.include? filename
|
92
|
+
end
|
93
|
+
old_tracks = @tracks
|
94
|
+
@tracks = Map.new
|
95
|
+
new_books.each do |filename|
|
96
|
+
@tracks[filename] = old_tracks[filename] || {:filename => filename}
|
97
|
+
@tracks[filename].merge! shuffleflag: false, bookmarkflag: true
|
98
|
+
end
|
99
|
+
new_songs.each do |filename|
|
100
|
+
@tracks[filename] = old_tracks[filename] || {:filename => filename}
|
101
|
+
@tracks[filename].merge! shuffleflag: true, bookmarkflag: false
|
102
|
+
end
|
103
|
+
if @tracks.include? prev_current_filename
|
104
|
+
@playback_state.trackno = @tracks.find_index{|filename,t|filename == prev_current_filename}
|
105
|
+
else
|
106
|
+
@playback_state.trackno = -1
|
107
|
+
@playback_state.trackpos = -1
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
def save
|
112
|
+
stats = Stats.new
|
113
|
+
sd = SD.new
|
114
|
+
@tracks.each_value do |track|
|
115
|
+
stats.records << track.subset(:bookmarktime, :playcount, :skippedcount)
|
116
|
+
sd.records << track.subset(:starttime, :stoptime, :volume, :file_type, :filename,
|
117
|
+
:shuffleflag, :bookmarkflag)
|
118
|
+
end
|
119
|
+
write_records @playback_state, 'PState'
|
120
|
+
write_records stats, 'Stats'
|
121
|
+
write_records sd, 'SD'
|
122
|
+
|
123
|
+
shuffle = make_filename('Shuffle')
|
124
|
+
File.delete shuffle if File.exists? shuffle
|
125
|
+
end
|
126
|
+
|
127
|
+
def write_records bindata, file_suffix
|
128
|
+
File.open( make_filename(file_suffix), 'w' ) do |io|
|
129
|
+
bindata.write io
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
def each_track
|
134
|
+
@tracks.each_value {|track| yield track}
|
135
|
+
end
|
136
|
+
|
137
|
+
def each_track_with_index
|
138
|
+
@tracks.each_with_index {|path_track,i| yield path_track[1], i}
|
139
|
+
end
|
140
|
+
|
141
|
+
def [] filename_or_index
|
142
|
+
case filename_or_index
|
143
|
+
when Integer
|
144
|
+
@tracks.values[filename_or_index]
|
145
|
+
else
|
146
|
+
@tracks[filename_or_index]
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
def inspect
|
151
|
+
"<IpodDB>"
|
152
|
+
end
|
153
|
+
|
154
|
+
def make_filename suffix
|
155
|
+
"#{@root_dir}/iPod_Control/iTunes/iTunes#{suffix}"
|
156
|
+
end
|
157
|
+
|
158
|
+
class PState < BinData::Record
|
159
|
+
endian :little
|
160
|
+
uint8 :volume, initial_value: 29
|
161
|
+
uint24 :shufflepos
|
162
|
+
uint24 :trackno, default: -1
|
163
|
+
bool24 :shuffleflag
|
164
|
+
uint24 :trackpos, default: -1
|
165
|
+
string length: 19
|
166
|
+
end
|
167
|
+
|
168
|
+
class Stats < BinData::Record
|
169
|
+
endian :little
|
170
|
+
uint24 :record_count, value: lambda { records.count }
|
171
|
+
uint24
|
172
|
+
array :records, initial_length: :record_count do
|
173
|
+
uint24 :reclen, value: lambda { num_bytes }
|
174
|
+
int24 :bookmarktime, initial_value: -1
|
175
|
+
string length: 6
|
176
|
+
uint24 :playcount
|
177
|
+
uint24 :skippedcount
|
178
|
+
end
|
179
|
+
end
|
180
|
+
|
181
|
+
class SD < BinData::Record
|
182
|
+
endian :big
|
183
|
+
uint24 :record_count, value: lambda { records.count }
|
184
|
+
uint24 :const, value: 0x10800
|
185
|
+
uint24 :reclen, value: lambda { num_bytes - records.num_bytes }
|
186
|
+
string length: 9
|
187
|
+
array :records, initial_length: :record_count do
|
188
|
+
uint24 :reclen, value: lambda { num_bytes }
|
189
|
+
string length: 3
|
190
|
+
uint24 :starttime
|
191
|
+
string length: 6
|
192
|
+
uint24 :stoptime
|
193
|
+
string length: 6
|
194
|
+
uint24 :volume, initial_value: 100
|
195
|
+
uint24 :file_type, value: lambda { ExtToFileType[File.extname(filename)] }
|
196
|
+
string length: 3
|
197
|
+
encoded_string :filename, length: 522
|
198
|
+
bool8 :shuffleflag
|
199
|
+
bool8 :bookmarkflag
|
200
|
+
string length: 1
|
201
|
+
end
|
202
|
+
end
|
203
|
+
end
|
data/lib/pretty.rb
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
module Pretty
|
2
|
+
def Pretty.seconds seconds
|
3
|
+
if seconds < 60
|
4
|
+
"#{seconds} sec"
|
5
|
+
else
|
6
|
+
minutes = (seconds / 60).floor
|
7
|
+
seconds -= 60*minutes
|
8
|
+
if minutes < 60
|
9
|
+
"%02d:%02d" % [minutes, seconds]
|
10
|
+
else
|
11
|
+
hours = (minutes / 60).floor
|
12
|
+
minutes -= 60*hours
|
13
|
+
"%d:%02d:%02d" % [ hours, minutes, seconds ]
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
data/lib/spread.rb
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
module Spread
|
2
|
+
def self.spread_two a, b
|
3
|
+
a,b = b,a if a.count < b.count
|
4
|
+
avg_d = a.count.to_f / (b.count+1)
|
5
|
+
offs = 0
|
6
|
+
|
7
|
+
result = []
|
8
|
+
b.each do |b_elem|
|
9
|
+
from = offs.floor
|
10
|
+
offs += avg_d
|
11
|
+
to = offs.floor
|
12
|
+
result << a[from...to]
|
13
|
+
result << b_elem
|
14
|
+
end
|
15
|
+
|
16
|
+
result << a[offs.to_i..-1]
|
17
|
+
result.flatten
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.spread *args
|
21
|
+
return [] if args.empty?
|
22
|
+
return args[0] if args.count == 0
|
23
|
+
|
24
|
+
args = args.sort_by{|array|array.count}
|
25
|
+
|
26
|
+
mix = args.shift
|
27
|
+
while enum = args.shift
|
28
|
+
mix = spread_two mix, enum
|
29
|
+
end
|
30
|
+
mix
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,159 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
require 'spec_helper'
|
3
|
+
require 'ipod_db'
|
4
|
+
require 'fileutils'
|
5
|
+
|
6
|
+
describe IpodDB do
|
7
|
+
before do
|
8
|
+
@expected = Map.new eval( File.open( "test_data.rb" ).read )
|
9
|
+
@ipod_root = 'mock_root'
|
10
|
+
@itunes_prefix = "#{@ipod_root}/iPod_Control/iTunes/iTunes"
|
11
|
+
# just in case
|
12
|
+
FileUtils::rm_rf(@ipod_root)
|
13
|
+
FileUtils::cp_r 'test_data', @ipod_root, remove_destination: true
|
14
|
+
FileUtils::chmod_R "u=rwX", @ipod_root
|
15
|
+
end
|
16
|
+
|
17
|
+
after { FileUtils::rm_rf(@ipod_root) }
|
18
|
+
|
19
|
+
def read_struct struct_name, suffix=''
|
20
|
+
File.open "#{@itunes_prefix}#{struct_name}#{suffix}" do |io|
|
21
|
+
IpodDB.const_get(struct_name).read(io)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
def write_struct struct_name, struct, suffix=''
|
25
|
+
File.open "#{@itunes_prefix}#{struct_name}#{suffix}", 'w' do |io|
|
26
|
+
struct.write(io)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
describe IpodDB::PState do
|
31
|
+
it 'parses pstate file' do
|
32
|
+
read_struct(:PState).snapshot.must_be :==, @expected[:pstate]
|
33
|
+
end
|
34
|
+
it 'writes pstate file' do
|
35
|
+
pstate = read_struct :PState
|
36
|
+
write_struct :PState, pstate, '_test'
|
37
|
+
pstate_test = read_struct :PState, '_test'
|
38
|
+
pstate_test.must_equal pstate
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
describe IpodDB::Stats do
|
43
|
+
it 'parses stats file' do
|
44
|
+
stats = read_struct :Stats
|
45
|
+
stats.record_count.must_equal @expected[:tracks].count
|
46
|
+
stats.records.count.must_equal @expected[:tracks].count
|
47
|
+
stats.records.must_have_records_like @expected[:tracks]
|
48
|
+
end
|
49
|
+
it 'writes stats file' do
|
50
|
+
stats = read_struct :Stats
|
51
|
+
write_struct :Stats, stats, '_test'
|
52
|
+
stats_test = read_struct :Stats, '_test'
|
53
|
+
stats_test.must_equal stats
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
describe IpodDB::SD do
|
58
|
+
it 'parses tracks file' do
|
59
|
+
sd = read_struct :SD
|
60
|
+
sd.record_count.must_equal @expected[:tracks].count
|
61
|
+
sd.records.count.must_equal @expected[:tracks].count
|
62
|
+
sd.records.must_have_records_like @expected[:tracks]
|
63
|
+
end
|
64
|
+
it 'writes sd file' do
|
65
|
+
sd = read_struct :SD
|
66
|
+
write_struct :SD, sd, '_test'
|
67
|
+
sd_test = read_struct :SD, '_test'
|
68
|
+
sd_test.must_equal sd
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
it 'loads the whole structure' do
|
73
|
+
ipod_db = IpodDB.new @ipod_root
|
74
|
+
current_index = @expected[:pstate][:trackno]
|
75
|
+
current_filename = @expected[:tracks][ current_index ][:filename]
|
76
|
+
|
77
|
+
ipod_db.must_include_each_of @expected[:tracks].map{|t|t[:filename]}
|
78
|
+
ipod_db.current_filename.must_equal current_filename
|
79
|
+
end
|
80
|
+
|
81
|
+
describe 'update' do
|
82
|
+
before do
|
83
|
+
# Given ...
|
84
|
+
old_filenames = @expected[:tracks].map{|t| t[:filename]}
|
85
|
+
@expected_hash = Hash[old_filenames.zip @expected[:tracks]]
|
86
|
+
one_third = old_filenames.count / 3
|
87
|
+
@new_books = old_filenames[0...one_third]
|
88
|
+
@removed = old_filenames[one_third...2*one_third]
|
89
|
+
@new_songs = old_filenames[2*one_third..-1]
|
90
|
+
@new_books << '/another_book.mp3'
|
91
|
+
@new_songs << '/another_song.mp3'
|
92
|
+
end
|
93
|
+
it 'updates tracklist in memory given new tracks' do
|
94
|
+
# When I ...
|
95
|
+
ipod_db = IpodDB.new @ipod_root
|
96
|
+
ipod_db.update books: @new_books, songs: @new_songs
|
97
|
+
|
98
|
+
# Then ...
|
99
|
+
ipod_db.must_include_none_of @removed
|
100
|
+
@new_books.each do |filename|
|
101
|
+
actual = ipod_db[filename]
|
102
|
+
assert !actual[:shuffleflag]
|
103
|
+
assert actual[:bookmarkflag]
|
104
|
+
if @expected_hash.include? filename
|
105
|
+
rest = @expected_hash[filename].clone
|
106
|
+
rest.delete :shuffleflag
|
107
|
+
rest.delete :bookmarkflag
|
108
|
+
rest.must_be_subset_of actual
|
109
|
+
end
|
110
|
+
end
|
111
|
+
@new_songs.each do |filename|
|
112
|
+
actual = ipod_db[filename]
|
113
|
+
assert actual[:shuffleflag]
|
114
|
+
assert !actual[:bookmarkflag]
|
115
|
+
if @expected_hash.include? filename
|
116
|
+
rest = @expected_hash[filename].clone
|
117
|
+
rest.delete :shuffleflag
|
118
|
+
rest.delete :bookmarkflag
|
119
|
+
rest.must_be_subset_of actual
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
123
|
+
it 'keeps the current track if still present' do
|
124
|
+
current_filename = @expected[:tracks][ @expected[:pstate][:trackno] ][:filename]
|
125
|
+
ipod_db = IpodDB.new @ipod_root
|
126
|
+
ipod_db.update books: @new_books, songs: @new_songs
|
127
|
+
ipod_db.must_include current_filename
|
128
|
+
ipod_db.current_filename.must_equal current_filename
|
129
|
+
end
|
130
|
+
it 'writes the whole db' do
|
131
|
+
# When I ...
|
132
|
+
ipod_db = IpodDB.new @ipod_root
|
133
|
+
ipod_db.update books: @new_books, songs: @new_songs
|
134
|
+
ipod_db.save
|
135
|
+
|
136
|
+
test_db = IpodDB.new @ipod_root
|
137
|
+
test_db.each_track do |track|
|
138
|
+
ipod_db[track[:filename]].must_be_subset_of track
|
139
|
+
end
|
140
|
+
ipod_db.each_track do |track|
|
141
|
+
track.must_be_subset_of test_db[track[:filename]]
|
142
|
+
end
|
143
|
+
ipod_db.playback_state.must_equal test_db.playback_state
|
144
|
+
end
|
145
|
+
it 'updates the track order' do
|
146
|
+
# When I ...
|
147
|
+
ipod_db = IpodDB.new @ipod_root
|
148
|
+
@new_books.shuffle!
|
149
|
+
ipod_db.update books: @new_books, songs: @new_songs
|
150
|
+
|
151
|
+
# Then ...
|
152
|
+
book_order = []
|
153
|
+
ipod_db.each_track do |t|
|
154
|
+
book_order << t[:filename] if @new_books.include? t[:filename]
|
155
|
+
end
|
156
|
+
book_order.must_equal @new_books
|
157
|
+
end
|
158
|
+
end
|
159
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,53 @@
|
|
1
|
+
require 'minitest/autorun'
|
2
|
+
require 'purdytest'
|
3
|
+
$LOAD_PATH.push "File.dirname(__FILE__)/../lib"
|
4
|
+
require 'bindata'
|
5
|
+
|
6
|
+
class Hash
|
7
|
+
def is_subset_of? other
|
8
|
+
all? { |key,value| other[key] == value }
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
module MiniTest::Assertions
|
13
|
+
def assert_includes_each_of expected, sequence
|
14
|
+
expected.each {|x| assert_includes sequence, x}
|
15
|
+
end
|
16
|
+
def assert_includes_none_of expected, sequence
|
17
|
+
expected.each {|x| refute_includes sequence, x}
|
18
|
+
end
|
19
|
+
def assert_is_subset_of superset, subset
|
20
|
+
assert subset.is_subset_of?(superset), "Expected\n #{subset}\n\nto be subset of\n #{superset}"
|
21
|
+
end
|
22
|
+
def assert_records_are_like expected, actual
|
23
|
+
actual.each_with_index do |record,i|
|
24
|
+
h = Map.new record.snapshot
|
25
|
+
h.delete :reclen
|
26
|
+
h.must_be_subset_of expected[i]
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
Object.infect_an_assertion :assert_includes_each_of, :must_include_each_of
|
32
|
+
Object.infect_an_assertion :assert_includes_none_of, :must_include_none_of
|
33
|
+
Object.infect_an_assertion :assert_is_subset_of, :must_be_subset_of
|
34
|
+
Object.infect_an_assertion :assert_records_are_like, :must_have_records_like
|
35
|
+
|
36
|
+
module Enumerable
|
37
|
+
def distances_between elem
|
38
|
+
d = 0
|
39
|
+
cnt = 0
|
40
|
+
ds = []
|
41
|
+
each do |e|
|
42
|
+
if e == elem
|
43
|
+
cnt += 1
|
44
|
+
ds << d
|
45
|
+
d = 0
|
46
|
+
else
|
47
|
+
d += 1
|
48
|
+
end
|
49
|
+
end
|
50
|
+
ds << d
|
51
|
+
ds
|
52
|
+
end
|
53
|
+
end
|
data/spec/spread_spec.rb
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'spread'
|
3
|
+
|
4
|
+
describe Spread do
|
5
|
+
|
6
|
+
{
|
7
|
+
'large difference in length' => [ 9.times.map{1}, 3.times.map{2} ],
|
8
|
+
'small difference in length' => [ 9.times.map{1}, 8.times.map{2} ],
|
9
|
+
'same length' => [ 9.times.map{1}, 9.times.map{2} ],
|
10
|
+
'more than two collections' => [ 5.times.map{1}, 9.times.map{2}, 13.times.map{3} ],
|
11
|
+
'degenerates upfront' => (10.times.map{|i| [i]} + [ 3.times.map{100} ]),
|
12
|
+
'degenerates behind' => ([ 3.times.map{100} ] + 10.times.map{|i| [i]}),
|
13
|
+
'degenerates around' => (10.times.map{|i| [i]} + [ 3.times.map{100} ] + 10.times.map{|i| [i]}),
|
14
|
+
}.each do |title, data|
|
15
|
+
describe title do
|
16
|
+
before do
|
17
|
+
@collections = data
|
18
|
+
@mix = Spread.spread *@collections
|
19
|
+
end
|
20
|
+
it 'uses all elements' do
|
21
|
+
@mix.count.must_equal @collections.reduce(0){|sum,enum|sum+enum.count}
|
22
|
+
end
|
23
|
+
it 'keeps elements of the same collection apart' do
|
24
|
+
@collections.each do |collection|
|
25
|
+
@mix.distances_between(collection[0]).max.must_be :<=, @mix.count / collection.count
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
Binary file
|
@@ -0,0 +1,135 @@
|
|
1
|
+
itunesdb_hash=5bd44041b25f8f501f7ccc026750dcb44281f34b
|
2
|
+
version=2.1.2
|
3
|
+
id=52
|
4
|
+
hostname=parurak
|
5
|
+
filename_locale=/home/artm/Music/Podcasts/Scott Sigler Audiobooks/20_THE_MVP_Episode_20.mp3
|
6
|
+
filename_utf8=/home/artm/Music/Podcasts/Scott Sigler Audiobooks/20_THE_MVP_Episode_20.mp3
|
7
|
+
filename_ipod=:iPod_Control:Music:F01:libgpod805604.mp3
|
8
|
+
sha1_hash=aa0f559ffa04663be150f3f259834f80a397ce9a
|
9
|
+
charset=UTF-8
|
10
|
+
pc_mtime=1362263347
|
11
|
+
transferred=1
|
12
|
+
id=53
|
13
|
+
hostname=parurak
|
14
|
+
filename_locale=/home/artm/Music/Podcasts/The Future And You/TFAY_2013_2_27.mp3
|
15
|
+
filename_utf8=/home/artm/Music/Podcasts/The Future And You/TFAY_2013_2_27.mp3
|
16
|
+
filename_ipod=:iPod_Control:Music:F00:libgpod007430.mp3
|
17
|
+
sha1_hash=f34ffe73504f8193c6c8ea1e28a737d3711771a0
|
18
|
+
charset=UTF-8
|
19
|
+
pc_mtime=1362263375
|
20
|
+
transferred=1
|
21
|
+
id=54
|
22
|
+
hostname=parurak
|
23
|
+
filename_locale=/home/artm/Music/Podcasts/Tales To Terrify/Tales_to_Terrify_Show_No_60_Nicole_Cushing_Ray_Banks.mp3
|
24
|
+
filename_utf8=/home/artm/Music/Podcasts/Tales To Terrify/Tales_to_Terrify_Show_No_60_Nicole_Cushing_Ray_Banks.mp3
|
25
|
+
filename_ipod=:iPod_Control:Music:F02:libgpod176172.mp3
|
26
|
+
sha1_hash=4f9e674a7385760dd4e79e1db6535df34a70c076
|
27
|
+
charset=UTF-8
|
28
|
+
pc_mtime=1362263014
|
29
|
+
transferred=1
|
30
|
+
id=55
|
31
|
+
hostname=parurak
|
32
|
+
filename_locale=/home/artm/Music/Podcasts/StarShipSofa/StarShipSofa_No_278_Theodora_Goss.mp3
|
33
|
+
filename_utf8=/home/artm/Music/Podcasts/StarShipSofa/StarShipSofa_No_278_Theodora_Goss.mp3
|
34
|
+
filename_ipod=:iPod_Control:Music:F02:libgpod879833.mp3
|
35
|
+
sha1_hash=5dc24d6ed94feba2cbbfd852f2b6666bccb97da4
|
36
|
+
charset=UTF-8
|
37
|
+
pc_mtime=1362263311
|
38
|
+
transferred=1
|
39
|
+
id=56
|
40
|
+
hostname=parurak
|
41
|
+
filename_locale=/home/artm/Music/Podcasts/The Ruby Show/rubyshow-222.mp3
|
42
|
+
filename_utf8=/home/artm/Music/Podcasts/The Ruby Show/rubyshow-222.mp3
|
43
|
+
filename_ipod=:iPod_Control:Music:F01:libgpod751619.mp3
|
44
|
+
sha1_hash=e8c2692db7798242e809d6297327dceabe189917
|
45
|
+
charset=UTF-8
|
46
|
+
pc_mtime=1362262814
|
47
|
+
transferred=1
|
48
|
+
id=57
|
49
|
+
hostname=parurak
|
50
|
+
filename_locale=/home/artm/Music/Podcasts/The Javascript Show/javascriptshow-54.mp3
|
51
|
+
filename_utf8=/home/artm/Music/Podcasts/The Javascript Show/javascriptshow-54.mp3
|
52
|
+
filename_ipod=:iPod_Control:Music:F02:libgpod129565.mp3
|
53
|
+
sha1_hash=122f8ea7a3e59614f25a28b8455bc5472a5e1e2a
|
54
|
+
charset=UTF-8
|
55
|
+
pc_mtime=1362262833
|
56
|
+
transferred=1
|
57
|
+
id=58
|
58
|
+
hostname=parurak
|
59
|
+
filename_locale=/home/artm/Music/Podcasts/This Week in Science - The Kickass Science Podcast/TWIS_2013_02_14.mp3
|
60
|
+
filename_utf8=/home/artm/Music/Podcasts/This Week in Science - The Kickass Science Podcast/TWIS_2013_02_14.mp3
|
61
|
+
filename_ipod=:iPod_Control:Music:F00:libgpod773026.mp3
|
62
|
+
sha1_hash=5236e14dc0d108ab47b5b26ef05e6887757e30d0
|
63
|
+
charset=UTF-8
|
64
|
+
pc_mtime=1362263416
|
65
|
+
transferred=1
|
66
|
+
id=59
|
67
|
+
hostname=parurak
|
68
|
+
filename_locale=/home/artm/Music/Podcasts/Corrupting the Kids/ctk-0024.mp3
|
69
|
+
filename_utf8=/home/artm/Music/Podcasts/Corrupting the Kids/ctk-0024.mp3
|
70
|
+
filename_ipod=:iPod_Control:Music:F02:libgpod588508.mp3
|
71
|
+
sha1_hash=5602a2068c03623d22ed84938cb98356140b3844
|
72
|
+
charset=UTF-8
|
73
|
+
pc_mtime=1362262986
|
74
|
+
transferred=1
|
75
|
+
id=60
|
76
|
+
filename_ipod=:iPod_Control:Music:F02:libgpod725768.mp3
|
77
|
+
sha1_hash=20a242536ef65dfbb4ef67d45adf19106150ac25
|
78
|
+
transferred=1
|
79
|
+
id=61
|
80
|
+
filename_ipod=:iPod_Control:Music:F00:libgpod191213.mp3
|
81
|
+
sha1_hash=c88ace50201042ea0b6499c13b3edac5eb147599
|
82
|
+
transferred=1
|
83
|
+
id=62
|
84
|
+
filename_ipod=:iPod_Control:Music:F02:libgpod700862.mp3
|
85
|
+
sha1_hash=6d33c58164582da261c5b466df1e6a0f60d95dd4
|
86
|
+
transferred=1
|
87
|
+
id=63
|
88
|
+
filename_ipod=:iPod_Control:Music:F01:libgpod392504.mp3
|
89
|
+
sha1_hash=6ad25a02f120a48324e7255ebc98ea8b26168296
|
90
|
+
transferred=1
|
91
|
+
id=64
|
92
|
+
filename_ipod=:iPod_Control:Music:F02:libgpod827455.mp3
|
93
|
+
sha1_hash=1dca015a808aa6ebb1619020f58934fd3005b999
|
94
|
+
transferred=1
|
95
|
+
id=65
|
96
|
+
filename_ipod=:iPod_Control:Music:F01:libgpod807107.mp3
|
97
|
+
sha1_hash=91a225e2c3619d9d43dddb58ea827975fb996822
|
98
|
+
transferred=1
|
99
|
+
id=66
|
100
|
+
filename_ipod=:iPod_Control:Music:F01:libgpod320923.mp3
|
101
|
+
sha1_hash=5d7858be13a4548290c5769c8d5f7e53648e7db5
|
102
|
+
transferred=1
|
103
|
+
id=67
|
104
|
+
filename_ipod=:iPod_Control:Music:F02:libgpod148225.mp3
|
105
|
+
sha1_hash=fad447a5e144bbdb3d13f5153ab954864ab1f5be
|
106
|
+
transferred=1
|
107
|
+
id=68
|
108
|
+
filename_ipod=:iPod_Control:Music:F01:libgpod442574.mp3
|
109
|
+
sha1_hash=636ff6688f27cfdc81ccbf76a0e4cbf10916deff
|
110
|
+
transferred=1
|
111
|
+
id=69
|
112
|
+
filename_ipod=:iPod_Control:Music:F01:libgpod496196.mp3
|
113
|
+
sha1_hash=65d9fb5c8ce324f887858d50e69885bdc81719d0
|
114
|
+
transferred=1
|
115
|
+
id=70
|
116
|
+
filename_ipod=:iPod_Control:Music:F02:libgpod032874.mp3
|
117
|
+
sha1_hash=efd6f29c915ea33fef1d87a6d1571f6d9f444831
|
118
|
+
transferred=1
|
119
|
+
id=71
|
120
|
+
filename_ipod=:iPod_Control:Music:F00:libgpod461353.mp3
|
121
|
+
sha1_hash=27a0222eb67c986ff2260c2d9b7983d9b271da12
|
122
|
+
transferred=1
|
123
|
+
id=72
|
124
|
+
filename_ipod=:iPod_Control:Music:F01:libgpod438233.mp3
|
125
|
+
sha1_hash=01b57daee12d8068789c667dc56f225035ae0ee4
|
126
|
+
transferred=1
|
127
|
+
id=73
|
128
|
+
filename_ipod=:iPod_Control:Music:F01:libgpod484105.mp3
|
129
|
+
sha1_hash=85cae99bff08ac7de178139404606744059194ee
|
130
|
+
transferred=1
|
131
|
+
id=74
|
132
|
+
filename_ipod=:iPod_Control:Music:F00:libgpod863057.mp3
|
133
|
+
sha1_hash=416e1edf0b77d3e226c44c5e04000cb3abb3f6c2
|
134
|
+
transferred=1
|
135
|
+
id=xxx
|