budik 1.0.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,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