ipod_db 0.2.4

Sign up to get free protection for your applications and to get access to all the features.
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