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/.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
|
+
[![Build Status](https://travis-ci.org/artm/ipod_db.png)](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
|
+
[![Build Status](https://travis-ci.org/artm/ipod_db.png)](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
|
+
}
|