budik 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,149 @@
1
+ # = config.rb
2
+ # This file contains methods for configuring the application.
3
+ #
4
+ # == Contact
5
+ #
6
+ # Author:: Petr Schmied (mailto:jblack@paworld.eu)
7
+ # Website:: http://www.paworld.eu
8
+ # Date:: September 20, 2015
9
+
10
+ module Budik
11
+ # 'Config' class loads and manages app configuration.
12
+ class Config
13
+ include Singleton
14
+
15
+ # Installs the application if not installed.
16
+ # Loads options, sources and language.
17
+ def initialize
18
+ @templates_dir = File.dirname(__FILE__) + '/../../config/templates/'
19
+ install unless installed?
20
+
21
+ @options = YAML.load_file(Dir.home + '/.budik/options.yml')
22
+ @sources = YAML.load_file(File.expand_path(@options['sources']['path']))
23
+ @lang = init_lang
24
+ end
25
+
26
+ # Sets application's language.
27
+ #
28
+ # - *Returns*:
29
+ # - R18n::Translation object.
30
+ #
31
+ def init_lang
32
+ R18n.default_places = Dir.home + '/.budik/lang/'
33
+ R18n.set(@options['lang'])
34
+ R18n.t
35
+ end
36
+
37
+ # Language strings, options and sources.
38
+ attr_accessor :lang, :options, :sources
39
+
40
+ # Opens options file for editing.
41
+ def edit
42
+ options_path = File.expand_path('~/.budik/options.yml')
43
+ open_file(options_path)
44
+ end
45
+
46
+ # Installs the application.
47
+ def install
48
+ dir = Dir.home + '/.budik/'
49
+ FileUtils.mkdir_p([dir, dir + 'lang/', dir + 'downloads/'])
50
+
51
+ install_options(dir)
52
+ install_sources(dir)
53
+ install_lang(dir)
54
+ end
55
+
56
+ # Creates options file from template.
57
+ #
58
+ # - *Args*:
59
+ # - +dir+ -> Directory containing app's configuration (String).
60
+ #
61
+ def install_options(dir)
62
+ options = @templates_dir + 'options/' + platform?.to_s + '.yml'
63
+ FileUtils.cp options, dir + 'options.yml'
64
+ end
65
+
66
+ # Creates sources file from template.
67
+ #
68
+ # - *Args*:
69
+ # - +dir+ -> Directory containing app's configuration (String).
70
+ #
71
+ def install_sources(dir)
72
+ sources = @templates_dir + 'sources/sources.yml'
73
+ FileUtils.cp sources, dir unless File.file? dir + sources
74
+ end
75
+
76
+ # Creates default language file from template.
77
+ #
78
+ # - *Args*:
79
+ # - +dir+ -> Directory containing app's configuration (String).
80
+ #
81
+ def install_lang(dir)
82
+ lang = @templates_dir + 'lang/en.yml'
83
+ FileUtils.cp lang, dir + 'lang/'
84
+ end
85
+
86
+ # Checks if the application is already installed.
87
+ def installed?
88
+ File.file?(Dir.home + '/.budik/options.yml')
89
+ end
90
+
91
+ # Opens file in default editor depending on platform.
92
+ #
93
+ # - *Args*:
94
+ # - +file+ -> File to open (String).
95
+ #
96
+ def open_file(file)
97
+ if @options['os'] == 'windows'
98
+ system('@powershell -Command "' + file + '"')
99
+ else
100
+ editor = ENV['EDITOR'] ? ENV['EDITOR'] : 'vi'
101
+ system(editor + ' "' + file + '"')
102
+ end
103
+ end
104
+
105
+ # Returns current platform application's running on.
106
+ #
107
+ # - *Returns*:
108
+ # - :windows, :linux or :rpi
109
+ #
110
+ def platform?
111
+ os = Sys::Platform.linux? ? :linux : :windows
112
+ rpi?(os) ? :rpi : os
113
+ end
114
+
115
+ # Resets app's configuration.
116
+ def reset
117
+ options = @templates_dir + 'options/' + platform?.to_s + '.yml'
118
+ FileUtils.cp(options, Dir.home + '/.budik/options.yml')
119
+ end
120
+
121
+ # Checks if application is running on Raspberry Pi.
122
+ #
123
+ # - *Args*:
124
+ # - +os+ -> Operating system (:windows or :linux)
125
+ # - *Returns*:
126
+ # - true or false
127
+ #
128
+ def rpi?(os)
129
+ return false unless os == :linux
130
+ cpuinfo = File.read('/proc/cpuinfo')
131
+ hardware = cpuinfo.scan(/[hH]ardware\s*:\s*(\w+)/).first.first
132
+ hardware =~ /BCM270[89]/
133
+ rescue
134
+ false
135
+ end
136
+
137
+ # Creates and/or opens language file for translation.
138
+ #
139
+ # - *Args*:
140
+ # - +lang+ -> Language code (String)
141
+ #
142
+ def translate(lang)
143
+ template = @templates_dir + 'lang/en.yml'
144
+ new_lang = Dir.home + '/.budik/lang/' + lang + '.yml'
145
+ FileUtils.cp template, new_lang
146
+ open_file(new_lang)
147
+ end
148
+ end
149
+ end
@@ -0,0 +1,146 @@
1
+ # = devices.rb
2
+ # This file contains methods for managing devices (storage and TV).
3
+ #
4
+ # == Contact
5
+ #
6
+ # Author:: Petr Schmied (mailto:jblack@paworld.eu)
7
+ # Website:: http://www.paworld.eu
8
+ # Date:: September 20, 2015
9
+
10
+ module Budik
11
+ # 'Devices' class manages display and storage devices.
12
+ class Devices
13
+ include Singleton
14
+
15
+ # Loads TV and storage settings.
16
+ def initialize
17
+ options = Config.instance.options
18
+ @tv = {}
19
+ @storage = { mounted: nil, awake: nil, unmount: false }
20
+
21
+ tv_load(options['tv'])
22
+ storage_load(options['sources']['download'])
23
+ end
24
+
25
+ # Returns TV and storage settings.
26
+ attr_accessor :storage, :tv
27
+
28
+ # Loads storage settings.
29
+ #
30
+ # - *Args*:
31
+ # - +options+ -> Storage options (Hash).
32
+ #
33
+ def storage_load(options)
34
+ @storage[:device] = options['device']
35
+ @storage[:partition] = options['partition']
36
+ @storage[:dir] = options['dir']
37
+
38
+ part_sub = { '$partition': @storage[:partition] }
39
+ dev_sub = { '$device': @storage[:device] }
40
+
41
+ storage_parse_cmd('mount', options['mount'], part_sub, mounted: false)
42
+ storage_parse_cmd('unmount', options['unmount'], part_sub, unmount: true)
43
+ storage_parse_cmd('sleep', options['sleep'], dev_sub, awake: false)
44
+ end
45
+
46
+ # Substitutes device and partition in (un)mount and sleep commands.
47
+ #
48
+ # == Example
49
+ #
50
+ # cmd = 'sleep'
51
+ # template = 'sudo hdparm -y $device'
52
+ # subst = { '$device': '/dev/sda' }
53
+ # state_mods = { awake: false }
54
+ #
55
+ # Parsed command: 'sudo hdparm -y /dev/sda'
56
+ # State 'awake' set to false
57
+ #
58
+ # - *Args*:
59
+ # - +cmd+ -> Command ('mount', 'unmount' or 'sleep').
60
+ # - +template+ -> Command template (String).
61
+ # - +subst+ -> Variable to substitute (Hash, variable: value).
62
+ # - +state_mods+ -> State modifiers (Hash).
63
+ #
64
+ def storage_parse_cmd(cmd, template, subst, state_mods = {})
65
+ return if template.empty?
66
+
67
+ cmd = (cmd + '_command').to_sym
68
+ var, val = subst.first
69
+ @storage[cmd] = template.gsub(var.to_s, val)
70
+ state_mods.each { |state, setting| @storage[state] = setting }
71
+ end
72
+
73
+ # Mounts partition if needed and if not already mounted
74
+ # If applicable, sets 'mounted' and 'awake' states to true
75
+ def storage_mount
76
+ unless @storage[:mounted].nil? || @storage[:mounted] == true
77
+ system(@storage[:mount_command])
78
+ end
79
+
80
+ @storage[:mounted] = true unless @storage[:mounted].nil?
81
+ @storage[:awake] = true unless @storage[:awake].nil?
82
+ end
83
+
84
+ # Unmounts partition if needed and if mounted
85
+ # If applicable, sets 'mounted' state to false
86
+ def storage_unmount
87
+ unmount = !@storage[:unmount]
88
+ unless unmount || @storage[:mounted].nil? || @storage[:mounted] == false
89
+ system(@storage[:unmount_command])
90
+ end
91
+
92
+ @storage[:mounted] = false unless @storage[:mounted].nil?
93
+ end
94
+
95
+ # Spins device down if needed and if awake
96
+ # If applicable, sets 'awake' state to false
97
+ def storage_sleep
98
+ sleep_check = @storage[:awake].nil? || @storage[:awake] == false
99
+
100
+ unless sleep_check || @storage[:mounted] == true
101
+ system(@storage[:sleep_command])
102
+ end
103
+
104
+ @storage[:awake] = false unless @storage[:awake].nil?
105
+ end
106
+
107
+ # Loads TV settings if TV is available.
108
+ #
109
+ # - *Args*:
110
+ # - +options+ -> TV options (Hash).
111
+ #
112
+ def tv_load(options)
113
+ if options['available']
114
+ @tv[:use_if_no_video] = options['use_if_no_video']
115
+ @tv[:wait_secs_after_on] = options['wait_secs_after_on']
116
+ @tv[:on] = false
117
+ else
118
+ @tv[:on] = nil
119
+ end
120
+ end
121
+
122
+ # Turns on TV if needed and if not already on
123
+ # Gives TV time to turn on, then sets active HDMI as active source
124
+ # If applicable, sets 'on' state to true
125
+ def tv_on
126
+ unless @tv[:on].nil? || @tv[:on] == true
127
+ system('echo "on 0" | cec-client -s >/dev/null')
128
+ sleep(@tv[:wait_secs_after_on]) unless @tv[:wait_secs_after_on].nil?
129
+ system('echo "as" | cec-client -s >/dev/null')
130
+ end
131
+
132
+ @tv[:on] = true unless @tv[:on].nil?
133
+ end
134
+
135
+ # Turns off TV if needed and if on
136
+ # If applicable, sets 'on' state to false
137
+ # Doesn't work on my TV
138
+ def tv_off
139
+ unless @tv[:on].nil? || @tv[:on] == false
140
+ system('echo "standby 0" | cec-client -s >/dev/null')
141
+ end
142
+
143
+ @tv[:on] = false unless @tv[:on].nil?
144
+ end
145
+ end
146
+ end
data/lib/budik/io.rb ADDED
@@ -0,0 +1,58 @@
1
+ # = io.rb
2
+ # This file contains methods for managing application's input and output.
3
+ #
4
+ # == Contact
5
+ #
6
+ # Author:: Petr Schmied (mailto:jblack@paworld.eu)
7
+ # Website:: http://www.paworld.eu
8
+ # Date:: September 20, 2015
9
+
10
+ module Budik
11
+ # 'Output' class provides information to the user via console.
12
+ class IO
13
+ include Singleton
14
+
15
+ # Loads output strings in currently set language.
16
+ def initialize
17
+ @strings = Config.instance.lang.output
18
+ end
19
+
20
+ # Outputs table formatted information about selected source
21
+ # to the console.
22
+ #
23
+ # - *Args*:
24
+ # - +source+ -> Selected source (Hash).
25
+ # - +number+ -> Number of selected source (Fixnum).
26
+ #
27
+ def run_info_table(source, number)
28
+ title = 'Budik - ' + DateTime.now.strftime('%d/%m/%Y %H:%M')
29
+
30
+ rows = []
31
+ rows << [@strings.alarm, source[:name]]
32
+ rows << [@strings.category, source[:category].to_s]
33
+ rows << [@strings.number, number.to_s]
34
+
35
+ Terminal::Table.new title: title, rows: rows
36
+ end
37
+
38
+ # Outputs formatted list of sources to the console.
39
+ #
40
+ # - *Args*:
41
+ # - +sources+ -> Parsed sources (Array of Hashes).
42
+ #
43
+ def sources_print(sources)
44
+ sources.each_with_index do |source, index|
45
+ puts '[' + index.to_s.light_white + '] ' + source[:name].yellow
46
+ end
47
+ end
48
+
49
+ # Outputs information about source being downloaded.
50
+ #
51
+ # - *Args*:
52
+ # - +source+ -> Source being downloaded (Hash).
53
+ #
54
+ def storage_download_info(source)
55
+ puts @strings.downloading + source[:name]
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,174 @@
1
+ # = player.rb
2
+ # This file contains methods for managing media players.
3
+ #
4
+ # == Contact
5
+ #
6
+ # Author:: Petr Schmied (mailto:jblack@paworld.eu)
7
+ # Website:: http://www.paworld.eu
8
+ # Date:: September 20, 2015
9
+
10
+ module Budik
11
+ # 'Player' class handles communication between app and media players.
12
+ class Player
13
+ include Singleton
14
+
15
+ # Sets player and loads its options.
16
+ def initialize
17
+ player_options = Config.instance.options['player']
18
+ @player = player_options['player']
19
+
20
+ if @player == 'omxplayer'
21
+ @player_options = player_options['omxplayer']
22
+ else
23
+ @player_options = player_options['vlc']
24
+ end
25
+ end
26
+
27
+ # Gets current player and its options.
28
+ attr_accessor :player, :player_options
29
+
30
+ # Plays a source using currently set player.
31
+ #
32
+ # - *Args*:
33
+ # - +source+ -> Source to play (Hash).
34
+ #
35
+ def play(source)
36
+ if @player == 'omxplayer'
37
+ omxplayer(source)
38
+ else
39
+ vlc(source)
40
+ end
41
+ end
42
+
43
+ # Plays a source using omxplayer.
44
+ #
45
+ # - *Args*:
46
+ # - +source+ -> Source to play (Hash).
47
+ #
48
+ def omxplayer(source)
49
+ source[:path].each_with_index do |item, index|
50
+ Open3.popen3(omx_build_command(item)) do |i, _o, _e, _t|
51
+ omx_volume_control(i) if index == 0
52
+ end
53
+ end
54
+ end
55
+
56
+ # Builds omxplayer's command with required parameters.
57
+ #
58
+ # - *Args*:
59
+ # - +item+ -> Item to play (path, String).
60
+ #
61
+ def omx_build_command(item)
62
+ command = @player_options['path']
63
+ args = '--vol ' + @player_options['default_volume'].to_s
64
+ command + ' ' + args + ' "' + Storage.instance.locate_item(item) + '"'
65
+ end
66
+
67
+ # Fades in volume using omxplayer's volup command.
68
+ #
69
+ # - *Args*:
70
+ # - +i+ -> Stdin object.
71
+ #
72
+ def omx_volume_control(i)
73
+ 7.times do
74
+ sleep(@player_options['volume_step_secs'])
75
+ i.print '+'
76
+ end
77
+ i.close
78
+ end
79
+
80
+ # Plays a source using vlc.
81
+ #
82
+ # - *Args*:
83
+ # - +source+ -> Source to play (Hash).
84
+ #
85
+ def vlc(source)
86
+ vlc_pid = spawn(vlc_build_command(source))
87
+ sleep(@player_options['wait_secs_after_run'])
88
+ vlc_volume_control(vlc_rc_connect)
89
+
90
+ Process.wait(vlc_pid)
91
+ end
92
+
93
+ # Builds VLC's command with required parameters.
94
+ #
95
+ # - *Args*:
96
+ # - +source+ -> Source to play (Hash).
97
+ #
98
+ def vlc_build_command(source)
99
+ vlc_path = Marshal.load(Marshal.dump(@player_options['path']))
100
+ vlc_path.gsub!(/(^|$)/, '"') if vlc_path =~ /\s/
101
+
102
+ args = vlc_build_args
103
+ files = vlc_cmd_add_items(source)
104
+
105
+ vlc_path + args[:rc] + args[:volume] + args[:fullscreen] + files
106
+ end
107
+
108
+ # Builds list of options/arguments fo VLC command
109
+ def vlc_build_args
110
+ rc_host = @player_options['rc_host']
111
+ rc_port = @player_options['rc_port']
112
+ rc = ' --extraintf rc --rc-host ' + rc_host + ':' + rc_port.to_s
113
+
114
+ volume = ' --volume-step ' + @player_options['volume_step'].to_s
115
+ fullscreen = @player_options['fullscreen'] ? ' --fullscreen ' : ' '
116
+
117
+ { rc: rc, volume: volume, fullscreen: fullscreen }
118
+ end
119
+
120
+ # Parses source and adds its items to the VLC command.
121
+ # Adds 'vlc://quit' to automatically quit VLC after play is over.
122
+ #
123
+ # - *Args*:
124
+ # - +source+ -> Source to play (Hash).
125
+ #
126
+ def vlc_cmd_add_items(source)
127
+ files = ''
128
+ source[:path].each do |item|
129
+ item_path = Storage.instance.locate_item(item).gsub(%r{^/}, '')
130
+ files += (vlc_cmd_item_prefix(item_path) + item_path + '" ')
131
+ end
132
+ files += 'vlc://quit'
133
+ end
134
+
135
+ # Adds 'file:///' prefix to local file paths so VLC plays them
136
+ # correctly.
137
+ #
138
+ # - *Args*:
139
+ # - +item_path+ -> Path to item.
140
+ #
141
+ def vlc_cmd_item_prefix(item_path)
142
+ is_url = (item_path =~ /\A#{URI.regexp(%w(http https))}\z/)
143
+ is_url ? '"' : '"file:///'
144
+ end
145
+
146
+ # Makes a connection to VLC's remote control interface.
147
+ # FIXME: Possible infinite loop
148
+ def vlc_rc_connect
149
+ rc_host = @player_options['rc_host']
150
+ rc_port = @player_options['rc_port']
151
+ loop do
152
+ begin
153
+ rc = TCPSocket.open(rc_host, rc_port)
154
+ return rc
155
+ rescue
156
+ next
157
+ end
158
+ end
159
+ end
160
+
161
+ # Fades in volume using VLC's remote control interface.
162
+ #
163
+ # - *Args*:
164
+ # - +rc+ -> IO object (returned by TCPSocket.open).
165
+ #
166
+ def vlc_volume_control(rc)
167
+ rc.puts 'volume ' + @player_options['default_volume'].to_s
168
+ 128.times do
169
+ sleep(@player_options['volume_fadein_secs'])
170
+ rc.puts 'volup ' + @player_options['volume_step'].to_s
171
+ end
172
+ end
173
+ end
174
+ end