sapis 0.1.0

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.
@@ -0,0 +1,281 @@
1
+ =begin
2
+
3
+ <multimedia_helper.rb> - Part of Sav's APIs.
4
+ Copyright (C) 2011 Saverio Miroddi
5
+
6
+ This program is free software: you can redistribute it and/or modify
7
+ it under the terms of the GNU General Public License as published by
8
+ the Free Software Foundation, either version 3 of the License, or
9
+ (at your option) any later version.
10
+
11
+ This program is distributed in the hope that it will be useful,
12
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
13
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
+ GNU General Public License for more details.
15
+
16
+ You should have received a copy of the GNU General Public License
17
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
18
+
19
+ =end
20
+
21
+ require_relative 'system_helper'
22
+ require_relative 'bash_helper'
23
+
24
+ module MultimediaHelper
25
+
26
+ include SystemHelper, BashHelper
27
+
28
+ RHYTHMBOX_PLAYLISTS_FILE = File.expand_path( '.local/share/rhythmbox/playlists.xml', '~' )
29
+ BANSHEE_DATA_FILE = File.expand_path( '.config/banshee-1/banshee.db', '~' )
30
+ AUDIO_FILES_EXTENSIONS = [ 'm4a', 'mp3' ]
31
+
32
+ def image_format( image_data )
33
+ if image_data.start_with?( 'GIF8' )
34
+ 'gif'
35
+ elsif image_data.start_with?( "\xFF\xD8\xFF\xE0" )
36
+ 'jpg'
37
+ else
38
+ raise "Unrecognized picture format."
39
+ end
40
+ end
41
+
42
+ def play_audio_file( filename )
43
+ filename = File.expand_path( filename )
44
+
45
+ if SystemHelper.mac?
46
+ simple_bash_execute 'afplay', filename
47
+ else
48
+ simple_bash_execute "gst-launch-1.0 playbin -q", "uri=file://#{ filename }"
49
+ end
50
+ end
51
+
52
+ def normalize_songs( *files )
53
+ files.flatten!
54
+
55
+ mp3_files = files.select { | file | file =~ /\.mp3$/i }
56
+ mp4_files = files.select { | file | file =~ /\.m4a$/i }
57
+
58
+ raise "xxx!" if mp3_files.size + mp4_files.size != files.size
59
+
60
+ if mp3_files.size != 0
61
+ simple_bash_execute "mp3gain -r", mp3_files
62
+ end
63
+
64
+ if mp4_files.size != 0
65
+ simple_bash_execute "aacgain -r", mp4_files
66
+ end
67
+ end
68
+
69
+ # Works on a single folder - no recursion.
70
+ #
71
+ def normalize_album( directory, extension )
72
+ case extension
73
+ when 'm4a'
74
+ program = 'aacgain'
75
+ when 'mp3'
76
+ program = 'mp3gain'
77
+ else
78
+ raise "Unsupported extension: #{ extension }"
79
+ end
80
+
81
+ files = Dir.glob( File.join( directory, "*.#{ extension }" ) )
82
+ encoded_files = encode_bash_filenames( *files )
83
+
84
+ # The m.f. popen3 hangs when normalizing, possibly because aacgain rewrites to screen because of the counter
85
+ # F**K F**K
86
+ #
87
+ # Regardless, aacgain appears to be broken, as if an error happens, it still exits successfully.
88
+ #
89
+ safe_execute "#{ program } -a -k " + encoded_files + " 2> /dev/null"
90
+ end
91
+
92
+ def encode_alac_to_m4a( file )
93
+ temp_file = file + ".wav"
94
+
95
+ simple_bash_execute "ffmpeg -i", file, temp_file
96
+
97
+ File.delete( file )
98
+
99
+ simple_bash_execute "neroAacEnc -q 0.5", "-if", temp_file, "-of", file
100
+
101
+ File.delete( temp_file )
102
+ end
103
+
104
+ # Sorts files by name
105
+ #
106
+ def create_m3u_playlist( files_or_pattern, basedir, output )
107
+ case files_or_pattern
108
+ when Array
109
+ files = files_or_pattern
110
+ # do nothing
111
+ when String
112
+ files = Dir.glob( files_or_pattern )
113
+ else
114
+ raise "ziokann!! #{ files_or_pattern }"
115
+ end
116
+
117
+ buffer = "#EXTM3U" << "\n"
118
+
119
+ files.sort.each do | file |
120
+ duration = get_audio_file_duration( file )
121
+ song_name = File.basename( file ).sub( /\.\w+$/, '' )
122
+
123
+ buffer << "#EXTINF:#{ duration },#{ song_name }" << "\n"
124
+
125
+ file_relative_path = File.join( basedir, File.basename( file ) )
126
+
127
+ buffer << file_relative_path << "\n"
128
+ end
129
+
130
+ IO.write( output, buffer )
131
+ end
132
+
133
+ def get_audio_file_duration( file )
134
+ case file
135
+ when /.mp3$/
136
+ duration = safe_execute( "mp3info -p '%S' " + encode_bash_filenames( file ) )
137
+ when /.m4a$/
138
+ # f#!$ing faad writes only to stderr
139
+ #
140
+ raw_result = safe_execute( 'faad -i ' + encode_bash_filenames( file ) + " 2>&1" )
141
+
142
+ duration = raw_result[ /(\d+)\.\d+ secs/, 1 ] || raise( "z.k.!!" )
143
+ else
144
+ raise "ziokann!!! #{ file }"
145
+ end
146
+
147
+ duration.to_i
148
+ end
149
+
150
+ # :directories single entry or array
151
+ #
152
+ def add_playlists_to_rhythmbox( directories, options={} )
153
+ require 'rexml/document'
154
+ require 'uri'
155
+
156
+ directories = [ directories ] if ! directories.is_a?( Array )
157
+
158
+ raw_xml = IO.read( RHYTHMBOX_PLAYLISTS_FILE )
159
+ xml_doc = REXML::Document.new( raw_xml )
160
+ xml_root = xml_doc.elements.first
161
+
162
+ directories.each do | directory |
163
+ puts "Adding #{ directory }..."
164
+
165
+ playlist_name = File.basename( directory )
166
+ filenames = AUDIO_FILES_EXTENSIONS.map { | extension | Dir.glob( File.join( directory, "*.#{ extension }" ) ) }.flatten.sort
167
+
168
+ if xml_root.elements.any? { | xml_element | xml_element.attributes[ 'name' ] == playlist_name }
169
+ puts ">>> playlist already existent!"
170
+ else
171
+ playlist_node = xml_root.add_element( 'playlist', 'name' => playlist_name, 'type' => 'static' )
172
+
173
+ filenames.sort!
174
+
175
+ filenames.each do | filename |
176
+ entry_node = playlist_node.add_element( 'location' )
177
+ encoded_filename = URI.encode( File.expand_path( filename ) )
178
+ entry_node.text = "file://" + encoded_filename
179
+ end
180
+ end
181
+ end
182
+
183
+ buffer = format_xml_playlist_for_rhythmbox( xml_doc )
184
+
185
+ IO.write( RHYTHMBOX_PLAYLISTS_FILE, buffer )
186
+ end
187
+
188
+ # :directories single entry or array
189
+ #
190
+ def add_playlists_to_banshee( directories, options={} )
191
+ require 'uri'
192
+
193
+ directories = [ directories ] if ! directories.is_a?( Array )
194
+
195
+ db_layer = SQLiteLayer.new( BANSHEE_DATA_FILE )
196
+
197
+ primary_source_id_music = 1
198
+
199
+ db_layer.transaction do
200
+ directories.each do | directory |
201
+ puts "Adding #{ directory }..."
202
+
203
+ playlist_name = File.basename( directory )
204
+
205
+ insertion_values = {
206
+ :PrimarySourceID => primary_source_id_music,
207
+ :Name => playlist_name,
208
+ }
209
+
210
+ playlist_id = db_layer.insert_values( 'CorePlaylists', insertion_values )
211
+
212
+ filenames = AUDIO_FILES_EXTENSIONS.map { | extension | Dir.glob( File.join( directory, "*.#{ extension }" ) ) }.flatten.sort
213
+
214
+ filenames.each do | filename |
215
+ puts " - #{ filename }"
216
+
217
+ track_id = banshee_find_track_id( filename, db_layer ) || banshee_add_playlist_entry( filename, db_layer, primary_source_id_music )
218
+
219
+ insertion_values = {
220
+ :PlaylistID => playlist_id,
221
+ :TrackID => track_id,
222
+ }
223
+
224
+ db_layer.insert_values( 'CorePlaylistEntries', insertion_values )
225
+ end
226
+ end
227
+ end
228
+ end
229
+
230
+ private
231
+
232
+ def banshee_find_track_id( filename, db_layer )
233
+ uri_filename = "file://" + URI.encode( File.expand_path( filename ) )
234
+
235
+ db_layer.select_value( "SELECT TrackID FROM CoreTracks WHERE Uri = ?", uri_filename )
236
+ end
237
+
238
+ def banshee_add_playlist_entry( filename, db_layer, primary_source_id_music )
239
+ uri_filename = "file://" + URI.encode( File.expand_path( filename ) )
240
+ title = File.basename( filename ).sub( /\.\w+$/, '' )
241
+ straight_title = "'" + title.gsub( "'", "''" ) + "'"
242
+ timestamp = Time.now.to_i
243
+
244
+ insertion_values = {
245
+ :PrimarySourceID => primary_source_id_music,
246
+ :ArtistID => 1,
247
+ :AlbumID => 1,
248
+ :TagSetID => 0,
249
+
250
+ :Uri => uri_filename,
251
+
252
+ :DateAddedStamp => timestamp,
253
+ :LastSyncedStamp => timestamp,
254
+ }
255
+
256
+ # when the titles are inserted with placeholders, they're inserted as BLOBs, because the columns are
257
+ # defined as TEXT. this causes problems with banshee, and is not noticeable when querying the db via
258
+ # cmdline client, or via ruby driver (translation: it's a m.f. pain in the back), but only via dump.
259
+ #
260
+ straight_insert_values = {
261
+ :Title => straight_title,
262
+ :TitleLowered => straight_title.downcase,
263
+ }
264
+
265
+ db_layer.insert_values( 'CoreTracks', insertion_values, :straight_insert => straight_insert_values )
266
+ end
267
+
268
+ def format_xml_playlist_for_rhythmbox( xml_doc )
269
+ buffer = ""
270
+ xml_formatter = REXML::Formatters::Pretty.new
271
+
272
+ xml_formatter.compact = true
273
+ xml_formatter.width = 16384 # avoid introducing f*ing spaces inside <location> elements, which are not compatible with RhythmBox
274
+
275
+ xml_formatter.write( xml_doc, buffer )
276
+
277
+ buffer
278
+ end
279
+
280
+ end
281
+
@@ -0,0 +1,166 @@
1
+ =begin
2
+
3
+ <sqlite_layer.rb> - Part of Sav's APIs.
4
+ Copyright (C) 2011 Saverio Miroddi
5
+
6
+ This program is free software: you can redistribute it and/or modify
7
+ it under the terms of the GNU General Public License as published by
8
+ the Free Software Foundation, either version 3 of the License, or
9
+ (at your option) any later version.
10
+
11
+ This program is distributed in the hope that it will be useful,
12
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
13
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
+ GNU General Public License for more details.
15
+
16
+ You should have received a copy of the GNU General Public License
17
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
18
+
19
+ =end
20
+
21
+ require 'sqlite3'
22
+
23
+ # In block form, transaction don't accept the :rollback call, so we need to use an exception.
24
+ #
25
+ class Rollback < StandardError; end
26
+
27
+ class SQLiteLayer
28
+ attr_reader :db
29
+
30
+ def initialize( filename, options={} )
31
+ absolute_db_filename = options[ :relative ] ? File.expand_path( filename, '~' ) : filename
32
+
33
+ @db = SQLite3::Database.new( absolute_db_filename )
34
+
35
+ @db.execute( 'PRAGMA foreign_keys = ON' )
36
+ end
37
+
38
+ # values: Can be either an array of values, or a hash field=>value.
39
+ # In order to insert BLOBs, pass a value enclosed in an array, e.g. ['mydata']
40
+ #
41
+ # Doesn't try to be clever.
42
+ #
43
+ # options:
44
+ # :straight_insert hash of values (position => value) to insert straight, without placeholders.
45
+ # values are not automatically escaped or quoted.
46
+ # this makes sense for some edge cases.
47
+ #
48
+ def insert_values( table, values, options={} )
49
+ straight_insert = options[ :straight_insert ] || {}
50
+
51
+ sql_fields = []
52
+ sql_placeholders = []
53
+ sql_values = []
54
+
55
+ case values
56
+ when Hash
57
+ values.each do | field, value |
58
+ sql_fields << field.to_s
59
+ sql_placeholders << '?'
60
+ sql_values << value
61
+ end
62
+ when Array
63
+ values.each do | value |
64
+ sql_placeholders << '?'
65
+ sql_values << value
66
+ end
67
+ else
68
+ raise "Invalid values class: #{ values.class }"
69
+ end
70
+
71
+ straight_insert.each do | field, value |
72
+ sql_fields << field.to_s
73
+ sql_placeholders << value
74
+ end
75
+
76
+ sql_values.each_with_index do | value, i |
77
+ sql_values[ i ] = SQLite3::Blob.new( value.first ) if value.is_a?( Array )
78
+ end
79
+
80
+ sql = "INSERT INTO #{ table }"
81
+ sql << "( #{ sql_fields.join(', ') } )" if sql_fields.size > 0
82
+ sql << " VALUES( #{ sql_placeholders.join(', ') } )"
83
+
84
+ @db.execute( sql, sql_values )
85
+
86
+ @db.last_insert_row_id
87
+ end
88
+
89
+ # values: the :where key is the where condition
90
+ #
91
+ def update_values( table, values )
92
+ where_sql = values.delete( :where ) || 'TRUE'
93
+
94
+ set_sql, set_values = values.inject( [ "", [] ] ) do | ( current_set_sql, current_set_values ), ( column, value ) |
95
+ current_set_sql << ', ' if current_set_sql != ''
96
+ current_set_sql << "#{ column } = ?"
97
+ current_set_values << value
98
+ [ current_set_sql, current_set_values ]
99
+ end
100
+
101
+ @db.execute( "UPDATE #{ table } SET #{ set_sql } WHERE #{ where_sql }", set_values )
102
+ end
103
+
104
+ def execute( sql, *params )
105
+ @db.execute( sql, params )
106
+ end
107
+
108
+ def select( sql, *params )
109
+ options = params.last.is_a?( Hash ) ? params.pop : {}
110
+ execute( sql, *params )
111
+ end
112
+
113
+ # params: the last can be :options
114
+ # options:
115
+ # :force: force finding a value
116
+ #
117
+ def select_value( sql, *params )
118
+ options = params.last.is_a?( Hash ) ? params.pop : {}
119
+
120
+ row = execute( sql, *params ).first
121
+
122
+ value = row && row.first
123
+
124
+ if value
125
+ value
126
+ elsif options[ :force ]
127
+ raise "Value not found!"
128
+ end
129
+ end
130
+
131
+ def select_all( sql, *params )
132
+ column_names, *raw_data = select_with_headers( sql, *params )
133
+
134
+ raw_data.map do | row |
135
+ Hash[ column_names.zip( row ) ]
136
+ end
137
+ end
138
+
139
+ # This is a :select_row with headers as first row.
140
+ #
141
+ def select_with_headers( sql, *params )
142
+ @db.execute2( sql, params )
143
+ end
144
+
145
+ # Can be nested - only the outer call with start a transaction.
146
+ #
147
+ def transaction( commit=true, &block )
148
+ if @db.transaction_active?
149
+ yield
150
+ else
151
+ begin
152
+ @db.transaction do
153
+ yield
154
+ raise Rollback.new if !commit
155
+ end
156
+ rescue Rollback
157
+ # do nothing
158
+ end
159
+ end
160
+ end
161
+
162
+ def close
163
+ @db.close
164
+ end
165
+
166
+ end
@@ -0,0 +1,141 @@
1
+ =begin
2
+
3
+ <system_helper.rb> - Part of Sav's APIs.
4
+ Copyright (C) 2011 Saverio Miroddi
5
+
6
+ This program is free software: you can redistribute it and/or modify
7
+ it under the terms of the GNU General Public License as published by
8
+ the Free Software Foundation, either version 3 of the License, or
9
+ (at your option) any later version.
10
+
11
+ This program is distributed in the hope that it will be useful,
12
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
13
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
+ GNU General Public License for more details.
15
+
16
+ You should have received a copy of the GNU General Public License
17
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
18
+
19
+ =end
20
+
21
+ require File.expand_path( '../bash_helper.rb', __FILE__ )
22
+
23
+ module SystemHelper
24
+
25
+ require 'shellwords'
26
+
27
+ SystemHelper::SEARCH_FILES = 'f'
28
+ SystemHelper::SEARCH_DIRECTORIES = 'd'
29
+
30
+ include BashHelper
31
+
32
+ def self.mac?
33
+ `uname`.strip == 'Darwin'
34
+ end
35
+
36
+ def self.current_timezone
37
+ if mac?
38
+ `systemsetup -gettimezone`.strip.sub( 'Time Zone: ', '' )
39
+ else
40
+ IO.read( '/etc/timezone' ).chomp
41
+ end
42
+ end
43
+
44
+ def self.symlink( source, destination )
45
+ # Doesn't work! At least one bug in the library:
46
+ # - File.exists? returns false if a symlink exists, but points to a non-existent file
47
+ # - symlink causes an abrupt exit if the destination exists
48
+ #
49
+ # File.delete( wine_drive_link ) if File.exists?( wine_drive_link )
50
+ # File.symlink( entry, wine_drive_link )
51
+ BashHelper.simple_bash_execute "ln -sf", source, destination
52
+ end
53
+
54
+ # REFACTOR: decide not/self for all the following
55
+ #
56
+ # 'mountpoint' has inconsistent exit behavior, because if the path is not a mountpoint,
57
+ # it returns 1, but prints the message to stdout.
58
+ #
59
+ def unmount_base_mountpoint( filename )
60
+ while filename != '/'
61
+ is_mountpoint = `mountpoint #{ encode_bash_filenames( filename ) }` =~ /is a mountpoint\n$/
62
+
63
+ if is_mountpoint
64
+ simple_bash_execute "umount", filename
65
+ return
66
+ end
67
+
68
+ filename = File.dirname( filename )
69
+ end
70
+
71
+ raise "Couldn't find base mount point for file: #{ filename }"
72
+ end
73
+
74
+ def system_cores_number
75
+ if RUBY_PLATFORM =~ /darwin/i
76
+ raw_result = safe_execute "system_profiler SPHardwareDataType | grep 'Total Number Of Cores'"
77
+ raw_result[ /: (\d+)/, 1 ].to_i
78
+ else
79
+ # See https://www.ibm.com/developerworks/community/blogs/brian/entry/linux_show_the_number_of_cpu_cores_on_your_system17?lang=en
80
+ # Bash form:
81
+ #
82
+ # cat /proc/cpuinfo | egrep "core id|physical id" | tr -d "\n" | sed s/physical/\\nphysical/g | grep -v ^$ | sort | uniq | wc -l
83
+ #
84
+ IO.readlines( '/proc/cpuinfo' ).grep( /core id|physical id/ ).each_slice( 2 ).to_a.uniq.size
85
+ end
86
+ end
87
+
88
+ def unrar( file, options={} )
89
+ delete = !! options[ :delete ]
90
+
91
+ original_dir = Dir.pwd
92
+ destination_dir = File.dirname( file )
93
+
94
+ Dir.chdir( destination_dir )
95
+
96
+ simple_bash_execute "unrar x", file
97
+
98
+ File.delete( file ) if delete
99
+ ensure
100
+ Dir.chdir( original_dir )
101
+ end
102
+
103
+ # Opens :filename using the default executable.
104
+ #
105
+ def self.open_file( filename )
106
+ `xdg-open #{ filename.shellescape }`
107
+ end
108
+
109
+ # Case insensitive search
110
+ #
111
+ # options:
112
+ # :file_type: [nil] either SEARCH_FILES or SEARCH_DIRECTORIES, or nil for both.
113
+ # :skip_paths: [nil] array of (full) paths to skip
114
+ #
115
+ def self.find_files( raw_pattern, raw_search_paths, options={} )
116
+ search_paths = raw_search_paths.map { | path | path.shellescape }.join( ' ' )
117
+ pattern = raw_pattern.shellescape
118
+
119
+ case options[ :file_type ]
120
+ when SEARCH_FILES
121
+ file_type = '-type f'
122
+ when SEARCH_DIRECTORIES
123
+ file_type = '-type d'
124
+ when nil
125
+ # nothing
126
+ else
127
+ raise "Unrecognized :file_type option for find_files: #{ options[ :file_type ] }"
128
+ end
129
+
130
+ skip_paths = options[ :skip_paths ].to_a.map do | path |
131
+ path_with_pattern = File.join( path, '*' )
132
+ " -not -path " + path_with_pattern.shellescape
133
+ end.join( ' ' )
134
+
135
+ raw_result = `find #{ search_paths } -iname #{ pattern } #{ file_type } #{ skip_paths }`.chomp
136
+
137
+ raw_result.split( "\n" )
138
+ end
139
+
140
+ end
141
+
@@ -0,0 +1,3 @@
1
+ module Sapis
2
+ VERSION = '0.1.0'
3
+ end
data/lib/sapis.rb ADDED
@@ -0,0 +1,33 @@
1
+ =begin
2
+
3
+ Sav's APIs - A collection of Ruby utility helpers.
4
+ Copyright (C) 2011 Saverio Miroddi
5
+
6
+ This program is free software: you can redistribute it and/or modify
7
+ it under the terms of the GNU General Public License as published by
8
+ the Free Software Foundation, either version 3 of the License, or
9
+ (at your option) any later version.
10
+
11
+ This program is distributed in the hope that it will be useful,
12
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
13
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
+ GNU General Public License for more details.
15
+
16
+ You should have received a copy of the GNU General Public License
17
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
18
+
19
+ =end
20
+
21
+ require_relative 'sapis/version'
22
+ require_relative 'sapis/graphing_helper'
23
+ require_relative 'sapis/configuration_helper'
24
+ require_relative 'sapis/concurrency_helper'
25
+ require_relative 'sapis/generic_helper'
26
+ require_relative 'sapis/bash_helper'
27
+ require_relative 'sapis/computations_helper'
28
+ require_relative 'sapis/desktop_helper'
29
+ require_relative 'sapis/interactions_helper'
30
+ require_relative 'sapis/multimedia_helper'
31
+ require_relative 'sapis/gnome_helper'
32
+ require_relative 'sapis/system_helper'
33
+ require_relative 'sapis/sqlite_layer'