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/.gitignore
ADDED
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/Guardfile
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
# A sample Guardfile
|
2
|
+
# More info at https://github.com/guard/guard#readme
|
3
|
+
|
4
|
+
guard 'bundler' do
|
5
|
+
watch('Gemfile')
|
6
|
+
end
|
7
|
+
|
8
|
+
guard 'minitest' do
|
9
|
+
# with Minitest::Spec
|
10
|
+
watch(%r|^spec/(.*)_spec\.rb|)
|
11
|
+
watch(%r|^lib/(.*)([^/]+)\.rb|) { |m| "spec/#{m[1]}#{m[2]}_spec.rb" }
|
12
|
+
watch(%r|^spec/spec_helper\.rb|) { "spec" }
|
13
|
+
end
|
data/HISTORY
ADDED
@@ -0,0 +1,9 @@
|
|
1
|
+
I used to do the same sort of thing and more with a bunch of python scripts.
|
2
|
+
My cheapo mass-storage mp3 player has died recently and my son has donated
|
3
|
+
(voluntarily) his Shuffle to me, but python scripts were all bit-rot and
|
4
|
+
refused to work and I haven't touched python for several years already, so
|
5
|
+
I rewrote the main script in ruby. The reading part of python version still
|
6
|
+
worked so I made it dump what it read in ruby-esque format for testing
|
7
|
+
(that's test_data.rb). The test_data isn't very exciting though (all
|
8
|
+
tracks are in default state without bookmarks).
|
9
|
+
|
data/LICENSE
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
This is free and unencumbered software released into the public domain.
|
2
|
+
|
3
|
+
Anyone is free to copy, modify, publish, use, compile, sell, or
|
4
|
+
distribute this software, either in source code form or as a compiled
|
5
|
+
binary, for any purpose, commercial or non-commercial, and by any
|
6
|
+
means.
|
7
|
+
|
8
|
+
In jurisdictions that recognize copyright laws, the author or authors
|
9
|
+
of this software dedicate any and all copyright interest in the
|
10
|
+
software to the public domain. We make this dedication for the benefit
|
11
|
+
of the public at large and to the detriment of our heirs and
|
12
|
+
successors. We intend this dedication to be an overt act of
|
13
|
+
relinquishment in perpetuity of all present and future rights to this
|
14
|
+
software under copyright law.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
19
|
+
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
|
20
|
+
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
|
21
|
+
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
22
|
+
OTHER DEALINGS IN THE SOFTWARE.
|
23
|
+
|
24
|
+
For more information, please refer to <http://unlicense.org/>
|
data/README.md
ADDED
@@ -0,0 +1,132 @@
|
|
1
|
+
NAME
|
2
|
+
====
|
3
|
+
|
4
|
+
ipod_db v0.2.4
|
5
|
+
|
6
|
+
SYNOPSIS
|
7
|
+
========
|
8
|
+
|
9
|
+
ipod_db (sync|ls|rm) [options]+
|
10
|
+
|
11
|
+
DESCRIPTION
|
12
|
+
===========
|
13
|
+
|
14
|
+
A couple of tools for working with iPod Shuffle (2nd gen) database from
|
15
|
+
command line. Each subcommand understands -h/--help flag.
|
16
|
+
|
17
|
+
PARAMETERS
|
18
|
+
==========
|
19
|
+
|
20
|
+
--version, -v
|
21
|
+
show package version and exit
|
22
|
+
--help, -h
|
23
|
+
|
24
|
+
AUTHOR
|
25
|
+
======
|
26
|
+
|
27
|
+
artm <femistofel@gmail.com>
|
28
|
+
|
29
|
+
|
30
|
+
[](https://travis-ci.org/artm/ipod_db)
|
31
|
+
|
32
|
+
|
33
|
+
SUBCOMMAND: sync
|
34
|
+
================
|
35
|
+
|
36
|
+
SYNOPSIS
|
37
|
+
========
|
38
|
+
|
39
|
+
ipod_db sync [options]+
|
40
|
+
|
41
|
+
DESCRIPTION
|
42
|
+
===========
|
43
|
+
|
44
|
+
Update the iPod database. Given directories of bookmarkable and non-bookmarkable
|
45
|
+
media 'ipod' will find all supported tracks add them to the iPod database so
|
46
|
+
the device is aware of their existance.
|
47
|
+
|
48
|
+
The tracks under 'books' folder will get "riffled" - tracks from the same folder
|
49
|
+
are spread out in the list so they don't follow each other if possible. IN THE
|
50
|
+
FUTURE it is planned to allow configuring of track groups which are treated like
|
51
|
+
single folders - e.g. to spread out all SciAm's "60 second somthing" podcasts along
|
52
|
+
the playlist.
|
53
|
+
|
54
|
+
It is perfectly possible to have other directories full of tracks in device's
|
55
|
+
subconscious - e.g. when time-sharing the device among members of a poor
|
56
|
+
family. Just make sure you update the database using your directories when
|
57
|
+
receiving it from a relation.
|
58
|
+
|
59
|
+
iPod remembers playback position on bookmarkable media and the 'ipod' goes
|
60
|
+
out of its way to preserve the bookmarks. It also removes bookmarkable files
|
61
|
+
from shuffle list.
|
62
|
+
|
63
|
+
I configure gpodder to place podcast files inside IPOD/books directory and delete
|
64
|
+
them after syncing. Having copied podcasts I run 'ipod sync' to update the
|
65
|
+
database on the device and it's ready for consumption.
|
66
|
+
|
67
|
+
PARAMETERS
|
68
|
+
==========
|
69
|
+
|
70
|
+
--version, -v
|
71
|
+
show package version and exit
|
72
|
+
--books=books, -b (0 ~> books=books)
|
73
|
+
subdirectory of ipod with bookmarkable media
|
74
|
+
--songs=songs, -s (0 ~> songs=songs)
|
75
|
+
subdirectory of ipod with non-bookmarkable media
|
76
|
+
--help, -h
|
77
|
+
|
78
|
+
SUBCOMMAND: ls
|
79
|
+
==============
|
80
|
+
|
81
|
+
SYNOPSIS
|
82
|
+
========
|
83
|
+
|
84
|
+
ipod_db ls [options]+
|
85
|
+
|
86
|
+
DESCRIPTION
|
87
|
+
===========
|
88
|
+
|
89
|
+
produce a colorful listing of the tracks in the ipod database
|
90
|
+
|
91
|
+
PARAMETERS
|
92
|
+
==========
|
93
|
+
|
94
|
+
--version, -v
|
95
|
+
show package version and exit
|
96
|
+
--help, -h
|
97
|
+
|
98
|
+
SUBCOMMAND: rm
|
99
|
+
==============
|
100
|
+
|
101
|
+
SYNOPSIS
|
102
|
+
========
|
103
|
+
|
104
|
+
ipod_db rm track track* [options]+
|
105
|
+
|
106
|
+
DESCRIPTION
|
107
|
+
===========
|
108
|
+
|
109
|
+
Remove tracks from the device by their numbers (that's why ls
|
110
|
+
displays numbers: so it's easier to select them for rm).
|
111
|
+
|
112
|
+
PARAMETERS
|
113
|
+
==========
|
114
|
+
|
115
|
+
track (-2 -> track)
|
116
|
+
track numbers to delete from device (ranges like 2-5 are accepted
|
117
|
+
too).
|
118
|
+
--version, -v
|
119
|
+
show package version and exit
|
120
|
+
--help, -h
|
121
|
+
|
122
|
+
HISTORY
|
123
|
+
=======
|
124
|
+
|
125
|
+
I used to do the same sort of thing and more with a bunch of python scripts.
|
126
|
+
My cheapo mass-storage mp3 player has died recently and my son has donated
|
127
|
+
(voluntarily) his Shuffle to me, but python scripts were all bit-rot and
|
128
|
+
refused to work and I haven't touched python for several years already, so
|
129
|
+
I rewrote the main script in ruby. The reading part of python version still
|
130
|
+
worked so I made it dump what it read in ruby-esque format for testing
|
131
|
+
(that's test_data.rb). The test_data isn't very exciting though (all
|
132
|
+
tracks are in default state without bookmarks).
|
data/Rakefile
ADDED
@@ -0,0 +1,41 @@
|
|
1
|
+
task :default => [:test, :build]
|
2
|
+
task :readme do
|
3
|
+
readme = `bin/ipod help`
|
4
|
+
|
5
|
+
readme += <<-__
|
6
|
+
|
7
|
+
[](https://travis-ci.org/artm/ipod_db)
|
8
|
+
|
9
|
+
__
|
10
|
+
|
11
|
+
%w(sync ls rm).each do |subcommand|
|
12
|
+
readme += "\nSUBCOMMAND: #{subcommand}\n"
|
13
|
+
|
14
|
+
rejecting = false
|
15
|
+
rejects = %w(name author)
|
16
|
+
readme += `bin/ipod #{subcommand} -h`.split("\n").reject do |line|
|
17
|
+
if line =~ /^\w/
|
18
|
+
rejecting = rejects.include? line.downcase
|
19
|
+
else
|
20
|
+
rejecting
|
21
|
+
end
|
22
|
+
end.join "\n"
|
23
|
+
end
|
24
|
+
|
25
|
+
readme += "\nHISTORY\n"
|
26
|
+
readme += IO.read('HISTORY').split("\n").map{|s| " #{s}"}.join("\n")
|
27
|
+
|
28
|
+
File.open('README.md','w') do |io|
|
29
|
+
io.puts readme.gsub(/^\w.*$/) {|m| "#{$&}\n#{'=' * $&.length}\n" }.gsub(/^ /,' ')
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
Rake::TestTask.new { |t|
|
34
|
+
t.libs << 'spec'
|
35
|
+
t.pattern = 'spec/*_spec.rb'
|
36
|
+
}
|
37
|
+
|
38
|
+
BEGIN {
|
39
|
+
require 'bundler/gem_tasks'
|
40
|
+
require 'rake/testtask'
|
41
|
+
}
|
data/TODO
ADDED
data/bin/ipod
ADDED
@@ -0,0 +1,245 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
Main {
|
4
|
+
version IpodDB::VERSION
|
5
|
+
|
6
|
+
description <<-__
|
7
|
+
A couple of tools for working with iPod Shuffle (2nd gen) database from
|
8
|
+
command line. Each subcommand understands -h/--help flag.
|
9
|
+
__
|
10
|
+
|
11
|
+
author 'artm <femistofel@gmail.com>'
|
12
|
+
|
13
|
+
name 'ipod_db'
|
14
|
+
config ipod_root: "/media/#{ENV['USER']}/IPOD"
|
15
|
+
option('version','v') {
|
16
|
+
description 'show package version and exit'
|
17
|
+
}
|
18
|
+
IgnorePlaybackPosUnder = 10
|
19
|
+
IgnoreProgressUnder = 0.01
|
20
|
+
|
21
|
+
def run
|
22
|
+
if param['version'].given?
|
23
|
+
puts IpodDB::VERSION
|
24
|
+
else
|
25
|
+
help!
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
mode('sync') {
|
30
|
+
description <<-__
|
31
|
+
Update the iPod database. Given directories of bookmarkable and non-bookmarkable
|
32
|
+
media '#{program}' will find all supported tracks add them to the iPod database so
|
33
|
+
the device is aware of their existance.
|
34
|
+
|
35
|
+
The tracks under 'books' folder will get "riffled" - tracks from the same folder
|
36
|
+
are spread out in the list so they don't follow each other if possible. IN THE
|
37
|
+
FUTURE it is planned to allow configuring of track groups which are treated like
|
38
|
+
single folders - e.g. to spread out all SciAm's "60 second somthing" podcasts along
|
39
|
+
the playlist.
|
40
|
+
|
41
|
+
It is perfectly possible to have other directories full of tracks in device's
|
42
|
+
subconscious - e.g. when time-sharing the device among members of a poor
|
43
|
+
family. Just make sure you update the database using your directories when
|
44
|
+
receiving it from a relation.
|
45
|
+
|
46
|
+
iPod remembers playback position on bookmarkable media and the '#{program}' goes
|
47
|
+
out of its way to preserve the bookmarks. It also removes bookmarkable files
|
48
|
+
from shuffle list.
|
49
|
+
|
50
|
+
I configure gpodder to place podcast files inside IPOD/books directory and delete
|
51
|
+
them after syncing. Having copied podcasts I run 'ipod sync' to update the
|
52
|
+
database on the device and it's ready for consumption.
|
53
|
+
__
|
54
|
+
|
55
|
+
option('books','b') {
|
56
|
+
argument_required
|
57
|
+
default 'books'
|
58
|
+
description 'subdirectory of ipod with bookmarkable media'
|
59
|
+
}
|
60
|
+
option('songs','s') {
|
61
|
+
argument_required
|
62
|
+
default 'songs'
|
63
|
+
description 'subdirectory of ipod with non-bookmarkable media'
|
64
|
+
}
|
65
|
+
def run
|
66
|
+
load_ipod_db
|
67
|
+
sync
|
68
|
+
end
|
69
|
+
}
|
70
|
+
|
71
|
+
mode('ls') {
|
72
|
+
description 'produce a colorful listing of the tracks in the ipod database'
|
73
|
+
|
74
|
+
def run
|
75
|
+
load_ipod_db
|
76
|
+
ls
|
77
|
+
end
|
78
|
+
|
79
|
+
}
|
80
|
+
|
81
|
+
mode('rm') {
|
82
|
+
description <<-__
|
83
|
+
Remove tracks from the device by their numbers (that's why ls
|
84
|
+
displays numbers: so it's easier to select them for rm).
|
85
|
+
__
|
86
|
+
argument('track') {
|
87
|
+
arity -2
|
88
|
+
description <<-__
|
89
|
+
track numbers to delete from device (ranges like
|
90
|
+
2-5 are accepted too).
|
91
|
+
__
|
92
|
+
}
|
93
|
+
def run
|
94
|
+
load_ipod_db
|
95
|
+
rm
|
96
|
+
end
|
97
|
+
}
|
98
|
+
|
99
|
+
def sync
|
100
|
+
books_path = params['books'].value
|
101
|
+
songs_path = params['songs'].value
|
102
|
+
books = collect_tracks books_path
|
103
|
+
books = riffle(books)
|
104
|
+
songs = collect_tracks songs_path
|
105
|
+
@ipod_db.update books: books, songs: songs
|
106
|
+
@ipod_db.save
|
107
|
+
ls
|
108
|
+
end
|
109
|
+
|
110
|
+
def ls
|
111
|
+
@ipod_db.each_track_with_index {|track,i| list_track i, track}
|
112
|
+
end
|
113
|
+
|
114
|
+
def rm
|
115
|
+
tracks = parse_track_list(params['track'].values).map{|i,t| [i,t.filename]}
|
116
|
+
puts "The following tracks are selected for removal:"
|
117
|
+
tracks.each { |i,path| puts " %2d. %s" % [i,path.green] }
|
118
|
+
if agree "Are you sure you want them gone (Y/n)?", true
|
119
|
+
FileUtils.rm tracks.map{|i,path|resolve_ipod_path path}
|
120
|
+
end
|
121
|
+
sync
|
122
|
+
end
|
123
|
+
|
124
|
+
def ipod_path path
|
125
|
+
'/' + Pathname.new(path).relative_path_from(Pathname.new @ipod_root).to_s
|
126
|
+
end
|
127
|
+
|
128
|
+
def resolve_ipod_path ipath
|
129
|
+
File.join @ipod_root, ipath
|
130
|
+
end
|
131
|
+
|
132
|
+
def track_length( path )
|
133
|
+
TagLib::FileRef.open(path){|file| file.audio_properties.length}
|
134
|
+
end
|
135
|
+
|
136
|
+
def load_ipod_db
|
137
|
+
@ipod_root = config['ipod_root']
|
138
|
+
unless IpodDB.looks_like_ipod? @ipod_root
|
139
|
+
fatal { "#{@ipod_root} does not appear to be a mounted ipod" }
|
140
|
+
exit exit_failure
|
141
|
+
end
|
142
|
+
@ipod_db = IpodDB.new @ipod_root
|
143
|
+
end
|
144
|
+
|
145
|
+
def collect_tracks path
|
146
|
+
begin
|
147
|
+
tracks = []
|
148
|
+
Find.find(resolve_ipod_path path) do |filepath|
|
149
|
+
tracks << ipod_path(filepath) if track? filepath
|
150
|
+
end
|
151
|
+
tracks
|
152
|
+
rescue Errno::ENOENT
|
153
|
+
[]
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
def track? path
|
158
|
+
IpodDB::ExtToFileType.include? File.extname(path)
|
159
|
+
end
|
160
|
+
|
161
|
+
def track_info track
|
162
|
+
info = Map.new
|
163
|
+
info['playcount'] = track.playcount if track.playcount > 0
|
164
|
+
info['skipcount'] = track.skippedcount if track.skippedcount > 0
|
165
|
+
if track.bookmarkflag && track.bookmarktime > 0
|
166
|
+
pos = track.bookmarktime * 0.256 # ipod keeps time in 256 ms increments
|
167
|
+
abs_path = resolve_ipod_path track.filename
|
168
|
+
total_time = track_length(abs_path)
|
169
|
+
if pos > IgnorePlaybackPosUnder && pos / total_time >= IgnoreProgressUnder
|
170
|
+
info['pos'] = Pretty.seconds pos
|
171
|
+
info['total'] = Pretty.seconds total_time
|
172
|
+
# and cache seconds
|
173
|
+
track['pos'] = pos
|
174
|
+
track['total_time'] = total_time
|
175
|
+
end
|
176
|
+
end
|
177
|
+
info
|
178
|
+
end
|
179
|
+
|
180
|
+
def list_track i, track
|
181
|
+
abs_path = resolve_ipod_path track.filename
|
182
|
+
exists = File.exists?(abs_path)
|
183
|
+
track_color = exists ? :green : :red
|
184
|
+
|
185
|
+
listing_entry = "%2d: %s" % [i,track.filename.apply_format( color: track_color )]
|
186
|
+
listing_entry = listing_entry.bold if @ipod_db.playback_state.trackno == i
|
187
|
+
puts listing_entry
|
188
|
+
|
189
|
+
return unless exists
|
190
|
+
|
191
|
+
info = track_info(track)
|
192
|
+
if track.include? 'pos'
|
193
|
+
progress = ProgressBar.create(
|
194
|
+
format: " [%b #{info.pos.yellow} %P%%%i] #{info.total.yellow}",
|
195
|
+
starting_at: track.pos,
|
196
|
+
total: track.total_time,
|
197
|
+
)
|
198
|
+
puts
|
199
|
+
info.delete :pos
|
200
|
+
info.delete :total
|
201
|
+
end
|
202
|
+
if info.count > 0
|
203
|
+
puts " " + info.map{|label,value| "#{label}: #{value.to_s.yellow}"}.join(" ")
|
204
|
+
end
|
205
|
+
end
|
206
|
+
|
207
|
+
def parse_track_list args
|
208
|
+
tracks = Map.new
|
209
|
+
args.each do |arg|
|
210
|
+
case arg
|
211
|
+
when /^\d+$/
|
212
|
+
n = arg.to_i
|
213
|
+
tracks[n] = @ipod_db[n]
|
214
|
+
when /^(\d+)-(\d+)$/
|
215
|
+
(Regexp.last_match(1)..Regexp.last_match(2)).each do |n_str|
|
216
|
+
n = n_str.to_i
|
217
|
+
tracks[n] = @ipod_db[n]
|
218
|
+
end
|
219
|
+
end
|
220
|
+
end
|
221
|
+
tracks
|
222
|
+
end
|
223
|
+
|
224
|
+
def riffle paths
|
225
|
+
bins = paths.group_by{|path| File.dirname(path)}.values
|
226
|
+
Spread.spread *bins
|
227
|
+
end
|
228
|
+
|
229
|
+
}
|
230
|
+
|
231
|
+
BEGIN {
|
232
|
+
require 'main'
|
233
|
+
require 'find'
|
234
|
+
require 'pathname'
|
235
|
+
require 'smart_colored/extend'
|
236
|
+
require 'map'
|
237
|
+
require 'taglib'
|
238
|
+
require 'ruby-progressbar'
|
239
|
+
require 'highline/import'
|
240
|
+
|
241
|
+
# our own
|
242
|
+
require 'ipod_db'
|
243
|
+
require 'pretty'
|
244
|
+
require 'spread'
|
245
|
+
}
|