ipod_db 0.2.4
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.
- 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
|