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/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
+
@@ -0,0 +1,3 @@
1
+ class IpodDB
2
+ VERSION = '0.2.4'
3
+ end
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
@@ -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
@@ -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
+
@@ -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