simple_pvr 0.0.1

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.
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