simple_pvr 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (102) hide show
  1. data/.rspec +1 -0
  2. data/Gemfile +4 -0
  3. data/Gemfile.lock +133 -0
  4. data/LICENSE.txt +13 -0
  5. data/README.md +169 -0
  6. data/Rakefile +16 -0
  7. data/bin/pvr_server +10 -0
  8. data/bin/pvr_xmltv +18 -0
  9. data/features/channel_overview.feature +48 -0
  10. data/features/programme_search.feature +20 -0
  11. data/features/scheduling.feature +112 -0
  12. data/features/step_definitions/pvr_steps.rb +104 -0
  13. data/features/step_definitions/web_steps.rb +219 -0
  14. data/features/support/env.rb +28 -0
  15. data/features/support/paths.rb +17 -0
  16. data/features/week_overview.feature +39 -0
  17. data/lib/simple_pvr/ffmpeg.rb +22 -0
  18. data/lib/simple_pvr/hdhomerun.rb +103 -0
  19. data/lib/simple_pvr/hdhomerun_save.sh +10 -0
  20. data/lib/simple_pvr/model/channel.rb +72 -0
  21. data/lib/simple_pvr/model/database_initializer.rb +36 -0
  22. data/lib/simple_pvr/model/programme.rb +58 -0
  23. data/lib/simple_pvr/model/recording.rb +45 -0
  24. data/lib/simple_pvr/model/schedule.rb +33 -0
  25. data/lib/simple_pvr/pvr_initializer.rb +47 -0
  26. data/lib/simple_pvr/pvr_logger.rb +14 -0
  27. data/lib/simple_pvr/recorder.rb +25 -0
  28. data/lib/simple_pvr/recording_manager.rb +101 -0
  29. data/lib/simple_pvr/recording_planner.rb +72 -0
  30. data/lib/simple_pvr/scheduler.rb +124 -0
  31. data/lib/simple_pvr/server/app_controller.rb +13 -0
  32. data/lib/simple_pvr/server/base_controller.rb +94 -0
  33. data/lib/simple_pvr/server/channels_controller.rb +68 -0
  34. data/lib/simple_pvr/server/config.ru +8 -0
  35. data/lib/simple_pvr/server/programmes_controller.rb +46 -0
  36. data/lib/simple_pvr/server/rack_maps.rb +7 -0
  37. data/lib/simple_pvr/server/schedules_controller.rb +71 -0
  38. data/lib/simple_pvr/server/shows_controller.rb +63 -0
  39. data/lib/simple_pvr/server/status_controller.rb +11 -0
  40. data/lib/simple_pvr/server/upcoming_recordings_controller.rb +18 -0
  41. data/lib/simple_pvr/version.rb +3 -0
  42. data/lib/simple_pvr/xmltv_reader.rb +83 -0
  43. data/lib/simple_pvr.rb +22 -0
  44. data/public/css/bootstrap-responsive.min.css +9 -0
  45. data/public/css/bootstrap.min.css +9 -0
  46. data/public/css/simplepvr.css +11 -0
  47. data/public/img/glyphicons-halflings-white.png +0 -0
  48. data/public/img/glyphicons-halflings.png +0 -0
  49. data/public/index.html +55 -0
  50. data/public/js/angular/angular-resource.min.js +10 -0
  51. data/public/js/angular/angular.min.js +157 -0
  52. data/public/js/app.js +145 -0
  53. data/public/js/bootstrap/bootstrap.min.js +6 -0
  54. data/public/js/controllers.js +156 -0
  55. data/public/js/services.js +27 -0
  56. data/public/partials/about.html +5 -0
  57. data/public/partials/channels.html +41 -0
  58. data/public/partials/programme.html +20 -0
  59. data/public/partials/programmeListing.html +18 -0
  60. data/public/partials/schedule.html +80 -0
  61. data/public/partials/schedules.html +44 -0
  62. data/public/partials/search.html +17 -0
  63. data/public/partials/show.html +21 -0
  64. data/public/partials/shows.html +7 -0
  65. data/public/partials/status.html +6 -0
  66. data/simple_pvr.gemspec +30 -0
  67. data/spec/resources/channels.txt +11 -0
  68. data/spec/resources/programs-without-icon.xmltv +95 -0
  69. data/spec/resources/programs.xmltv +98 -0
  70. data/spec/simple_pvr/ffmpeg_spec.rb +26 -0
  71. data/spec/simple_pvr/hdhomerun_spec.rb +82 -0
  72. data/spec/simple_pvr/model/channel_spec.rb +114 -0
  73. data/spec/simple_pvr/model/programme_spec.rb +110 -0
  74. data/spec/simple_pvr/model/schedule_spec.rb +47 -0
  75. data/spec/simple_pvr/pvr_initializer_spec.rb +50 -0
  76. data/spec/simple_pvr/recorder_spec.rb +32 -0
  77. data/spec/simple_pvr/recording_manager_spec.rb +158 -0
  78. data/spec/simple_pvr/recording_planner_spec.rb +104 -0
  79. data/spec/simple_pvr/scheduler_spec.rb +201 -0
  80. data/spec/simple_pvr/xmltv_reader_spec.rb +49 -0
  81. data/test/config/jsTestDriver-scenario.conf +10 -0
  82. data/test/config/jsTestDriver.conf +12 -0
  83. data/test/config/jstd-scenario-adapter-config.js +6 -0
  84. data/test/filtersSpec.js +97 -0
  85. data/test/lib/angular/angular-mocks.js +1719 -0
  86. data/test/lib/angular/angular-scenario.js +25937 -0
  87. data/test/lib/angular/jstd-scenario-adapter.js +185 -0
  88. data/test/lib/angular/version.txt +1 -0
  89. data/test/lib/jasmine/MIT.LICENSE +20 -0
  90. data/test/lib/jasmine/index.js +180 -0
  91. data/test/lib/jasmine/jasmine-html.js +190 -0
  92. data/test/lib/jasmine/jasmine.css +166 -0
  93. data/test/lib/jasmine/jasmine.js +2476 -0
  94. data/test/lib/jasmine/jasmine_favicon.png +0 -0
  95. data/test/lib/jasmine/version.txt +1 -0
  96. data/test/lib/jasmine-jstd-adapter/JasmineAdapter.js +196 -0
  97. data/test/lib/jasmine-jstd-adapter/version.txt +1 -0
  98. data/test/lib/jstestdriver/JsTestDriver.jar +0 -0
  99. data/test/lib/jstestdriver/version.txt +1 -0
  100. data/test/scripts/test-server.sh +14 -0
  101. data/test/scripts/test.sh +8 -0
  102. metadata +342 -0
@@ -0,0 +1,219 @@
1
+ # Taken from the cucumber-rails project.
2
+ # IMPORTANT: This file is generated by cucumber-sinatra - edit at your own peril.
3
+ # It is recommended to regenerate this file in the future when you upgrade to a
4
+ # newer version of cucumber-sinatra. Consider adding your own code to a new file
5
+ # instead of editing this one. Cucumber will automatically load all features/**/*.rb
6
+ # files.
7
+
8
+ require 'uri'
9
+ require 'cgi'
10
+ require File.expand_path(File.join(File.dirname(__FILE__), "..", "support", "paths"))
11
+
12
+ module WithinHelpers
13
+ def with_scope(locator)
14
+ locator ? within(locator) { yield } : yield
15
+ end
16
+ end
17
+ World(WithinHelpers)
18
+
19
+ Given /^(?:|I )am on (.+)$/ do |page_name|
20
+ visit path_to(page_name)
21
+ end
22
+
23
+ When /^(?:|I )go to (.+)$/ do |page_name|
24
+ visit path_to(page_name)
25
+ end
26
+
27
+ When /^(?:|I )press "([^\"]*)"(?: within "([^\"]*)")?$/ do |button, selector|
28
+ with_scope(selector) do
29
+ click_button(button)
30
+ end
31
+ end
32
+
33
+ When /^(?:|I )follow "([^\"]*)"(?: within "([^\"]*)")?$/ do |link, selector|
34
+ with_scope(selector) do
35
+ click_link(link)
36
+ end
37
+ end
38
+
39
+ When /^(?:|I )fill in "([^\"]*)" with "([^\"]*)"(?: within "([^\"]*)")?$/ do |field, value, selector|
40
+ with_scope(selector) do
41
+ fill_in(field, :with => value)
42
+ end
43
+ end
44
+
45
+ When /^(?:|I )fill in "([^\"]*)" for "([^\"]*)"(?: within "([^\"]*)")?$/ do |value, field, selector|
46
+ with_scope(selector) do
47
+ fill_in(field, :with => value)
48
+ end
49
+ end
50
+
51
+ # Use this to fill in an entire form with data from a table. Example:
52
+ #
53
+ # When I fill in the following:
54
+ # | Account Number | 5002 |
55
+ # | Expiry date | 2009-11-01 |
56
+ # | Note | Nice guy |
57
+ # | Wants Email? | |
58
+ #
59
+ # TODO: Add support for checkbox, select og option
60
+ # based on naming conventions.
61
+ #
62
+ When /^(?:|I )fill in the following(?: within "([^\"]*)")?:$/ do |selector, fields|
63
+ with_scope(selector) do
64
+ fields.rows_hash.each do |name, value|
65
+ When %{I fill in "#{name}" with "#{value}"}
66
+ end
67
+ end
68
+ end
69
+
70
+ When /^(?:|I )select "([^\"]*)" from "([^\"]*)"(?: within "([^\"]*)")?$/ do |value, field, selector|
71
+ with_scope(selector) do
72
+ select(value, :from => field)
73
+ end
74
+ end
75
+
76
+ When /^(?:|I )check "([^\"]*)"(?: within "([^\"]*)")?$/ do |field, selector|
77
+ with_scope(selector) do
78
+ check(field)
79
+ end
80
+ end
81
+
82
+ When /^(?:|I )uncheck "([^\"]*)"(?: within "([^\"]*)")?$/ do |field, selector|
83
+ with_scope(selector) do
84
+ uncheck(field)
85
+ end
86
+ end
87
+
88
+ When /^(?:|I )choose "([^\"]*)"(?: within "([^\"]*)")?$/ do |field, selector|
89
+ with_scope(selector) do
90
+ choose(field)
91
+ end
92
+ end
93
+
94
+ When /^(?:|I )attach the file "([^\"]*)" to "([^\"]*)"(?: within "([^\"]*)")?$/ do |path, field, selector|
95
+ with_scope(selector) do
96
+ attach_file(field, path)
97
+ end
98
+ end
99
+
100
+ Then /^(?:|I )should see JSON:$/ do |expected_json|
101
+ require 'json'
102
+ expected = JSON.pretty_generate(JSON.parse(expected_json))
103
+ actual = JSON.pretty_generate(JSON.parse(response.body))
104
+ expected.should == actual
105
+ end
106
+
107
+ Then /^(?:|I )should see "([^\"]*)"(?: within "([^\"]*)")?$/ do |text, selector|
108
+ with_scope(selector) do
109
+ if page.respond_to? :should
110
+ page.should have_content(text)
111
+ else
112
+ assert page.has_content?(text)
113
+ end
114
+ end
115
+ end
116
+
117
+ Then /^(?:|I )should see \/([^\/]*)\/(?: within "([^\"]*)")?$/ do |regexp, selector|
118
+ regexp = Regexp.new(regexp)
119
+ with_scope(selector) do
120
+ if page.respond_to? :should
121
+ page.should have_xpath('//*', :text => regexp)
122
+ else
123
+ assert page.has_xpath?('//*', :text => regexp)
124
+ end
125
+ end
126
+ end
127
+
128
+ Then /^(?:|I )should not see "([^\"]*)"(?: within "([^\"]*)")?$/ do |text, selector|
129
+ with_scope(selector) do
130
+ if page.respond_to? :should
131
+ page.should have_no_content(text)
132
+ else
133
+ assert page.has_no_content?(text)
134
+ end
135
+ end
136
+ end
137
+
138
+ Then /^(?:|I )should not see \/([^\/]*)\/(?: within "([^\"]*)")?$/ do |regexp, selector|
139
+ regexp = Regexp.new(regexp)
140
+ with_scope(selector) do
141
+ if page.respond_to? :should
142
+ page.should have_no_xpath('//*', :text => regexp)
143
+ else
144
+ assert page.has_no_xpath?('//*', :text => regexp)
145
+ end
146
+ end
147
+ end
148
+
149
+ Then /^the "([^\"]*)" field(?: within "([^\"]*)")? should contain "([^\"]*)"$/ do |field, selector, value|
150
+ with_scope(selector) do
151
+ field = find_field(field)
152
+ field_value = (field.tag_name == 'textarea') ? field.text : field.value
153
+ if field_value.respond_to? :should
154
+ field_value.should =~ /#{value}/
155
+ else
156
+ assert_match(/#{value}/, field_value)
157
+ end
158
+ end
159
+ end
160
+
161
+ Then /^the "([^\"]*)" field(?: within "([^\"]*)")? should not contain "([^\"]*)"$/ do |field, selector, value|
162
+ with_scope(selector) do
163
+ field = find_field(field)
164
+ field_value = (field.tag_name == 'textarea') ? field.text : field.value
165
+ if field_value.respond_to? :should_not
166
+ field_value.should_not =~ /#{value}/
167
+ else
168
+ assert_no_match(/#{value}/, field_value)
169
+ end
170
+ end
171
+ end
172
+
173
+ Then /^the "([^\"]*)" checkbox(?: within "([^\"]*)")? should be checked$/ do |label, selector|
174
+ with_scope(selector) do
175
+ field_checked = find_field(label)['checked']
176
+ if field_checked.respond_to? :should
177
+ field_checked.should == 'checked'
178
+ else
179
+ assert_equal 'checked', field_checked
180
+ end
181
+ end
182
+ end
183
+
184
+ Then /^the "([^\"]*)" checkbox(?: within "([^\"]*)")? should not be checked$/ do |label, selector|
185
+ with_scope(selector) do
186
+ field_checked = find_field(label)['checked']
187
+ if field_checked.respond_to? :should_not
188
+ field_checked.should_not == 'checked'
189
+ else
190
+ assert_not_equal 'checked', field_checked
191
+ end
192
+ end
193
+ end
194
+
195
+ Then /^(?:|I )should be on (.+)$/ do |page_name|
196
+ current_path = URI.parse(current_url).path
197
+ if current_path.respond_to? :should
198
+ current_path.should == path_to(page_name)
199
+ else
200
+ assert_equal path_to(page_name), current_path
201
+ end
202
+ end
203
+
204
+ Then /^(?:|I )should have the following query string:$/ do |expected_pairs|
205
+ query = URI.parse(current_url).query
206
+ actual_params = query ? CGI.parse(query) : {}
207
+ expected_params = {}
208
+ expected_pairs.rows_hash.each_pair{|k,v| expected_params[k] = v.split(',')}
209
+
210
+ if actual_params.respond_to? :should
211
+ actual_params.should == expected_params
212
+ else
213
+ assert_equal expected_params, actual_params
214
+ end
215
+ end
216
+
217
+ Then /^show me the page$/ do
218
+ save_and_open_page
219
+ end
@@ -0,0 +1,28 @@
1
+ ENV['RACK_ENV'] = 'test'
2
+ require 'capybara'
3
+ require 'capybara/cucumber'
4
+ require 'rspec'
5
+
6
+ require File.join(File.dirname(__FILE__), '../../lib/simple_pvr')
7
+
8
+ SimplePvr::PvrInitializer.setup_for_integration_test
9
+ SimplePvr::RecordingPlanner.read
10
+
11
+ Capybara.app = eval "Rack::Builder.new {( " + SimplePvr::PvrInitializer.rack_maps_file + ")}"
12
+ Capybara.default_driver = :selenium
13
+ Capybara.default_wait_time = 5
14
+ Capybara.ignore_hidden_elements = true # AngularJS shows and hides elements all the time, so this is important
15
+
16
+ class SimplePvrWorld
17
+ include Capybara::DSL
18
+ include RSpec::Expectations
19
+ include RSpec::Matchers
20
+ end
21
+
22
+ World do
23
+ SimplePvrWorld.new
24
+ end
25
+
26
+ Before do
27
+ SimplePvr::Model::DatabaseInitializer.clear
28
+ end
@@ -0,0 +1,17 @@
1
+ module NavigationHelpers
2
+ def path_to(page_name)
3
+ case page_name
4
+ when 'the schedules page'
5
+ '/'
6
+ when 'the channel overview page'
7
+ '/channels'
8
+ when 'the status page'
9
+ '/status'
10
+ else
11
+ raise "Can't find mapping from \"#{page_name}\" to a path.\n" +
12
+ "Now, go and add a mapping in #{__FILE__}"
13
+ end
14
+ end
15
+ end
16
+
17
+ World(NavigationHelpers)
@@ -0,0 +1,39 @@
1
+ Feature: Week overview
2
+ In order to find out what's on a specific channel
3
+ As a user
4
+ I want the weekly schedules for a channel
5
+
6
+ Background:
7
+ Given the following channels:
8
+ | name |
9
+ | Channel 1 |
10
+ | Channel 2 |
11
+ And the following programmes:
12
+ | title | subtitle | channel | day |
13
+ | Bonderøven | Danish documentary | Channel 1 | 1 |
14
+ | Blood and Bone | American action movie from 2009 | Channel 1 | 2 |
15
+ | The future is here | American action movie from 2032 | Channel 1 | 8 |
16
+ | The past is here | American action movie from 1992 | Channel 1 | -1 |
17
+ | Noddy | Children's programme | Channel 2 | 3 |
18
+ And I have navigated to the week overview for channel "Channel 1"
19
+
20
+ Scenario: I see programmes for the following week on the channel
21
+ Then I should see "Bonderøven"
22
+ And I should see "Blood and Bone"
23
+ But I should not see "The future is here"
24
+ And I should not see "Noddy"
25
+
26
+ Scenario: I can go to the next week
27
+ When I follow ">>"
28
+ Then I should see "The future is here"
29
+ But I should not see "Bonderøven"
30
+
31
+ Scenario: I can go to the previous week
32
+ When I follow "<<"
33
+ Then I should see "The past is here"
34
+ But I should not see "Bonderøven"
35
+
36
+ Scenario: I can go to the programme description
37
+ Given I follow "Bonderøven"
38
+ Then I should see "Danish documentary"
39
+ And I should see "Episode 23/40"
@@ -0,0 +1,22 @@
1
+ module SimplePvr
2
+ class Ffmpeg
3
+ def self.create_thumbnail_for(path)
4
+ thumbnail_file_name = path + '/thumbnail.png'
5
+ log_file_name = path + '/thumbnail.png.log'
6
+
7
+ pid = Process.spawn("ffmpeg -i \"#{path}/stream.ts\" -ss 00:05:00.000 -f image2 -vframes 1 -vf scale=300:ih*300/iw \"#{thumbnail_file_name}\" > \"#{log_file_name}\" 2>&1")
8
+ Process.detach(pid)
9
+ end
10
+
11
+ def self.transcode_to_webm(path)
12
+ stream_file_name = path + '/stream.ts'
13
+ webm_file_name = path + '/stream.webm'
14
+ log_file_name = path + '/stream.webm.log'
15
+
16
+ unless File.exists?(webm_file_name)
17
+ pid = Process.spawn("ffmpeg -i \"#{stream_file_name}\" -b 64k -vf scale=640:ih*640/iw \"#{webm_file_name}\" > \"#{log_file_name}\" 2>&1")
18
+ Process.detach(pid)
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,103 @@
1
+ module SimplePvr
2
+ #
3
+ # Simple fake version of HDHomeRun class. Makes it possible to run integration tests.
4
+ #
5
+ class HDHomeRunFake
6
+ def scan_for_channels; end
7
+ def start_recording(tuner, frequency, programme_id, directory); end
8
+ def stop_recording(tuner); end
9
+ end
10
+
11
+ #
12
+ # Encapsulates all the HDHomeRun-specific functionality. Do not initialize HDHomeRun objects yourself,
13
+ # but get the current instance through PvrInitializer.
14
+ #
15
+ class HDHomeRun
16
+ attr_reader :device_id
17
+
18
+ def initialize
19
+ @device_id = discover
20
+ @tuner_pids = [nil, nil]
21
+ FileUtils.rm(tuner_control_file(0)) if File.exists?(tuner_control_file(0))
22
+ FileUtils.rm(tuner_control_file(1)) if File.exists?(tuner_control_file(1))
23
+ end
24
+
25
+ def scan_for_channels
26
+ file_name = 'channels.txt'
27
+ scan_channels_with_tuner(file_name)
28
+ Model::Channel.clear
29
+ read_channels_file(file_name)
30
+ end
31
+
32
+ def start_recording(tuner, frequency, program_id, directory)
33
+ set_tuner_to_frequency(tuner, frequency)
34
+ set_tuner_to_program(tuner, program_id)
35
+ @tuner_pids[tuner] = spawn_recorder_process(tuner, directory)
36
+ PvrLogger.info("Process ID for recording on tuner #{tuner}: #{@tuner_pids[tuner]}")
37
+ end
38
+
39
+ def stop_recording(tuner)
40
+ pid = @tuner_pids[tuner]
41
+ PvrLogger.info("Stopping process #{pid} for tuner #{tuner}")
42
+ send_control_c_to_process(tuner, pid)
43
+ reset_tuner_frequency(tuner)
44
+ @tuner_pids[tuner] = nil
45
+ end
46
+
47
+ private
48
+ def discover
49
+ IO.popen('hdhomerun_config discover') do |pipe|
50
+ output = pipe.read
51
+ return $1 if output =~ /^hdhomerun device (.*) found at .*$/
52
+
53
+ raise Exception, "No device found: #{output}"
54
+ end
55
+ end
56
+
57
+ def scan_channels_with_tuner(file_name)
58
+ system "hdhomerun_config #{@device_id} scan /tuner0 #{file_name}"
59
+ end
60
+
61
+ def read_channels_file(file_name)
62
+ channel_frequency = nil
63
+
64
+ File.open(file_name, 'r:UTF-8') do |file|
65
+ file.each_line do |line|
66
+ if line =~ /^SCANNING: (\d*) .*$/
67
+ channel_frequency = $1.to_i
68
+ elsif line =~ /^PROGRAM (\d*): \d* (.*)$/
69
+ channel_id = $1.to_i
70
+ channel_name = $2.strip
71
+ Model::Channel.add(channel_name, channel_frequency, channel_id)
72
+ end
73
+ end
74
+ end
75
+ end
76
+
77
+ def set_tuner_to_frequency(tuner, frequency)
78
+ system "hdhomerun_config #{@device_id} set /tuner#{tuner}/channel auto:#{frequency}"
79
+ end
80
+
81
+ def set_tuner_to_program(tuner, program_id)
82
+ system "hdhomerun_config #{@device_id} set /tuner#{tuner}/program #{program_id}"
83
+ end
84
+
85
+ def spawn_recorder_process(tuner, directory)
86
+ FileUtils.touch(tuner_control_file(tuner))
87
+ spawn File.dirname(__FILE__) + "/hdhomerun_save.sh #{@device_id} #{tuner} \"#{directory}/stream.ts\" \"#{directory}/hdhomerun_save.log\" \"#{tuner_control_file(tuner)}\""
88
+ end
89
+
90
+ def reset_tuner_frequency(tuner)
91
+ system "hdhomerun_config #{@device_id} set /tuner#{tuner}/channel none"
92
+ end
93
+
94
+ def send_control_c_to_process(tuner, pid)
95
+ FileUtils.rm(tuner_control_file(tuner))
96
+ Process.wait(pid)
97
+ end
98
+
99
+ def tuner_control_file(tuner)
100
+ File.dirname(__FILE__) + "/tuner#{tuner}.lock"
101
+ end
102
+ end
103
+ end
@@ -0,0 +1,10 @@
1
+ #!/bin/bash
2
+
3
+ hdhomerun_config $1 save /tuner$2 "$3" > "$4" 2>&1 &
4
+ COMMAND_PID=$!
5
+
6
+ while [ -f $5 ]; do
7
+ sleep 1
8
+ done
9
+
10
+ kill -SIGINT $COMMAND_PID
@@ -0,0 +1,72 @@
1
+ module SimplePvr
2
+ module Model
3
+ class Channel
4
+ include DataMapper::Resource
5
+ storage_names[:default] = 'channels'
6
+
7
+ property :id, Serial
8
+ property :name, String
9
+ property :frequency, Integer
10
+ property :channel_id, Integer
11
+ property :icon_url, String, length: 250
12
+ property :hidden, Boolean, required: true, default: false
13
+
14
+ has n, :programmes
15
+
16
+ def self.add(name, frequency, id)
17
+ self.create(
18
+ name: name,
19
+ frequency: frequency,
20
+ channel_id: id
21
+ )
22
+ end
23
+
24
+ def self.with_current_programmes(id)
25
+ decorated_with_current_programmes(get(id), Time.now)
26
+ end
27
+
28
+ def self.all_with_current_programmes
29
+ now = Time.now
30
+ self.all(order: :name).map {|channel| decorated_with_current_programmes(channel, now) }
31
+ end
32
+
33
+ def self.clear
34
+ Programme.destroy
35
+ self.destroy
36
+ end
37
+
38
+ def self.with_name(name)
39
+ result = self.first(name: name)
40
+ raise "Unknown channel: '#{name}'" unless result
41
+ result
42
+ end
43
+
44
+ private
45
+ def self.decorated_with_current_programmes(channel, now)
46
+ current_programme = current_programme_for(channel, now)
47
+ number_of_upcoming_programmes = current_programme ? 3 : 4
48
+ upcoming_programmes = upcoming_programmes_for(channel, number_of_upcoming_programmes, now)
49
+ {
50
+ channel: channel,
51
+ current_programme: current_programme,
52
+ upcoming_programmes: upcoming_programmes
53
+ }
54
+ end
55
+
56
+ def self.current_programme_for(channel, now)
57
+ unless channel.hidden
58
+ result = Programme.all(:channel => channel, :start_time.lt => now, fields: [:id, :title, :start_time, :duration], order: :start_time.desc, limit: 1)[0]
59
+ result if result && result.start_time.advance(seconds: result.duration) >= now
60
+ end
61
+ end
62
+
63
+ def self.upcoming_programmes_for(channel, limit, now)
64
+ if channel.hidden
65
+ []
66
+ else
67
+ Programme.all(:channel => channel, :start_time.gte => now, fields: [:id, :title, :start_time], order: :start_time, limit: limit)
68
+ end
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,36 @@
1
+ require 'data_mapper'
2
+ require 'dm-migrations'
3
+ require 'active_support/time_with_zone'
4
+ require File.dirname(__FILE__) + '/channel'
5
+ require File.dirname(__FILE__) + '/programme'
6
+ require File.dirname(__FILE__) + '/schedule'
7
+ require File.dirname(__FILE__) + '/recording'
8
+
9
+ DataMapper.finalize
10
+
11
+ module SimplePvr
12
+ module Model
13
+ class DatabaseInitializer
14
+ def self.setup(database_file_name = nil)
15
+ database_file_name ||= Dir.pwd + '/database.sqlite'
16
+ DataMapper.setup(:default, "sqlite://#{database_file_name}")
17
+ DataMapper.auto_upgrade!
18
+ end
19
+
20
+ def self.clear
21
+ Schedule.destroy
22
+ Programme.destroy
23
+ Channel.destroy
24
+ end
25
+
26
+ def self.prepare_for_test
27
+ return if @initialized
28
+
29
+ @database_file_name = Dir.pwd + '/spec/resources/test.sqlite'
30
+ File.delete(@database_file_name) if File.exists?(@database_file_name)
31
+ self.setup(@database_file_name)
32
+ @initialized = true
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,58 @@
1
+ module SimplePvr
2
+ module Model
3
+ class Programme
4
+ include DataMapper::Resource
5
+ storage_names[:default] = 'programmes'
6
+
7
+ property :id, Serial
8
+ property :title, String, index: true, :length => 255
9
+ property :subtitle, String, :length => 255
10
+ property :description, Text
11
+ property :start_time, DateTime
12
+ property :duration, Integer
13
+ property :episode_num, String, index: true
14
+
15
+ belongs_to :channel
16
+
17
+ def self.clear
18
+ Programme.destroy
19
+ end
20
+
21
+ def self.add(channel, title, subtitle, description, start_time, duration, episode_num)
22
+ channel.programmes.create(
23
+ channel: channel,
24
+ title: title,
25
+ subtitle: subtitle,
26
+ description: description,
27
+ start_time: start_time,
28
+ duration: duration.to_i,
29
+ episode_num: episode_num)
30
+ end
31
+
32
+ def self.with_title(title)
33
+ Programme.all(title: title, order: :start_time)
34
+ end
35
+
36
+ def self.on_channel_with_title(channel, title)
37
+ Programme.all(channel: channel, title: title, order: :start_time)
38
+ end
39
+
40
+ def self.on_channel_with_title_and_start_time(channel, title, start_time)
41
+ Programme.all(channel: channel, title: title, start_time: start_time)
42
+ end
43
+
44
+ def self.titles_containing(text)
45
+ # Maybe there's a smarter way to do substring search than constructing "%#{text}%"? I'd like
46
+ # a version where the original input is escaped properly. However, this method is not
47
+ # dangerous, it just means that the user can enter "%" or "_" in the search string for a
48
+ # wildcard.
49
+ Programme.all(:title.like => "%#{text}%", fields: [:title], order: :title, limit: 8, unique: true).map {|programme| programme.title }
50
+ end
51
+
52
+ def self.with_title_containing(text)
53
+ # Same "LIKE" comments as above...
54
+ Programme.all(:title.like => "%#{text}%", order: :start_time, limit: 20)
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,45 @@
1
+ module SimplePvr
2
+ module Model
3
+ class Recording
4
+ attr_accessor :channel, :show_name, :start_time, :duration, :programme, :conflicting
5
+
6
+ def initialize(channel, show_name, start_time, duration, programme=nil)
7
+ @channel = channel
8
+ @show_name = show_name
9
+ @start_time = start_time
10
+ @duration = duration
11
+ @programme = programme
12
+ end
13
+
14
+ def expired?
15
+ expired_at(Time.now)
16
+ end
17
+
18
+ def expired_at(time)
19
+ end_time < time
20
+ end
21
+
22
+ def conflicting?
23
+ conflicting
24
+ end
25
+
26
+ def inspect
27
+ "'#{@show_name}' from '#{@channel.name}' at '#{@start_time}' with duration #{@duration} and programme #{@programme}"
28
+ end
29
+
30
+ def ==(other)
31
+ other != nil &&
32
+ other.channel == @channel &&
33
+ other.show_name == @show_name &&
34
+ other.start_time == @start_time &&
35
+ other.duration == @duration &&
36
+ other.programme == @programme
37
+ end
38
+
39
+ private
40
+ def end_time
41
+ @start_time.advance(seconds: duration)
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,33 @@
1
+ module SimplePvr
2
+ module Model
3
+ class Schedule
4
+ include DataMapper::Resource
5
+ storage_names[:default] = 'schedules'
6
+
7
+ property :id, Serial
8
+ property :type, Enum[:specification, :exception]
9
+ property :title, String, :length => 255
10
+ # If specified (and channel is specified too), this schedule is for a specific
11
+ # programme at a specific channel at a specific time
12
+ property :start_time, DateTime
13
+ property :filter_by_weekday, Boolean
14
+ property :monday, Boolean
15
+ property :tuesday, Boolean
16
+ property :wednesday, Boolean
17
+ property :thursday, Boolean
18
+ property :friday, Boolean
19
+ property :saturday, Boolean
20
+ property :sunday, Boolean
21
+
22
+ belongs_to :channel, required: false
23
+
24
+ def self.add_specification(options)
25
+ Schedule.create(
26
+ type: :specification,
27
+ title: options[:title],
28
+ channel: options[:channel],
29
+ start_time: options[:start_time])
30
+ end
31
+ end
32
+ end
33
+ end