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.
- checksums.yaml +7 -0
- data/README.md +179 -0
- data/lib/sapis/bash_helper.rb +80 -0
- data/lib/sapis/computations_helper.rb +95 -0
- data/lib/sapis/concurrency_helper.rb +104 -0
- data/lib/sapis/configuration_helper.rb +128 -0
- data/lib/sapis/desktop_helper.rb +50 -0
- data/lib/sapis/generic_helper.rb +241 -0
- data/lib/sapis/gnome_helper.rb +36 -0
- data/lib/sapis/graphing_helper.rb +202 -0
- data/lib/sapis/interactions_helper.rb +148 -0
- data/lib/sapis/multimedia_helper.rb +281 -0
- data/lib/sapis/sqlite_layer.rb +166 -0
- data/lib/sapis/system_helper.rb +141 -0
- data/lib/sapis/version.rb +3 -0
- data/lib/sapis.rb +33 -0
- metadata +141 -0
|
@@ -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
|
+
|
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'
|