ipod_db 0.2.4

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,2 @@
1
+ pkg/
2
+ Gemfile.lock
data/.travis.yml ADDED
@@ -0,0 +1,8 @@
1
+ language: ruby
2
+ rvm:
3
+ - "1.9.3"
4
+ - rbx-19mode
5
+
6
+ before_install:
7
+ - sudo apt-get update -qq
8
+ - sudo apt-get install -qq libtag1-dev
data/Gemfile ADDED
@@ -0,0 +1,2 @@
1
+ source "https://rubygems.org"
2
+ gemspec
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
@@ -0,0 +1,3 @@
1
+ - allow grouping some podcasts so their episodes are spread out (via config field)
2
+ - display totals in ls output: total time and bytes; bytes left on device etc
3
+
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
+ }