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,47 @@
1
+ module SimplePvr
2
+ class PvrInitializer
3
+ def self.setup
4
+ Model::DatabaseInitializer.setup
5
+ setup_with_hdhomerun(HDHomeRun.new)
6
+ @hdhomerun.scan_for_channels if Model::Channel.all.empty?
7
+ end
8
+
9
+ def self.setup_for_integration_test
10
+ Model::DatabaseInitializer.prepare_for_test
11
+ setup_with_hdhomerun(HDHomeRunFake.new)
12
+ end
13
+
14
+ def self.hdhomerun
15
+ @hdhomerun
16
+ end
17
+
18
+ def self.recording_manager
19
+ @recording_manager
20
+ end
21
+
22
+ def self.scheduler
23
+ @scheduler
24
+ end
25
+
26
+ def self.rackup_file_path
27
+ File.dirname(__FILE__) + '/server/config.ru'
28
+ end
29
+
30
+ def self.rack_maps_file
31
+ File.read(File.dirname(__FILE__) + '/server/rack_maps.rb')
32
+ end
33
+
34
+ def self.sleep_forever
35
+ forever = 6000.days
36
+ sleep forever
37
+ end
38
+
39
+ private
40
+ def self.setup_with_hdhomerun(hdhomerun)
41
+ @hdhomerun = hdhomerun
42
+ @recording_manager = RecordingManager.new
43
+ @scheduler = Scheduler.new
44
+ @scheduler.start
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,14 @@
1
+ require 'logger'
2
+
3
+ module SimplePvr
4
+ class PvrLogger
5
+ @@logger = Logger.new(STDOUT)
6
+ @@logger.formatter = proc do |severity, datetime, progname, msg|
7
+ "[#{datetime.strftime('%Y-%m-%d %H:%M:%S')}] #{severity} #{msg}\n"
8
+ end
9
+
10
+ def self.info(message)
11
+ @@logger.info(message)
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,25 @@
1
+ require 'fileutils'
2
+ require File.dirname(__FILE__) + '/hdhomerun'
3
+ require File.dirname(__FILE__) + '/pvr_logger'
4
+
5
+ module SimplePvr
6
+ class Recorder
7
+ def initialize(tuner, recording)
8
+ @tuner, @recording = tuner, recording
9
+ end
10
+
11
+ def start!
12
+ @directory = PvrInitializer.recording_manager.create_directory_for_recording(@recording)
13
+ PvrInitializer.hdhomerun.start_recording(@tuner, @recording.channel.frequency, @recording.channel.channel_id, @directory)
14
+
15
+ PvrLogger.info "Started recording #{@recording.show_name} in #{@directory}"
16
+ end
17
+
18
+ def stop!
19
+ PvrInitializer.hdhomerun.stop_recording(@tuner)
20
+ Ffmpeg.create_thumbnail_for(@directory)
21
+
22
+ PvrLogger.info "Stopped recording #{@recording.show_name}"
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,101 @@
1
+ module SimplePvr
2
+ RecordingMetadata = Struct.new(:has_thumbnail, :has_webm, :show_name, :episode, :channel, :subtitle, :description, :start_time, :duration)
3
+
4
+ class RecordingManager
5
+ def initialize(recordings_directory=nil)
6
+ @recordings_directory = recordings_directory || Dir.pwd + '/recordings'
7
+ end
8
+
9
+ def shows
10
+ Dir.new(@recordings_directory).entries - ['.', '..']
11
+ end
12
+
13
+ def delete_show(show_name)
14
+ FileUtils.rm_rf(directory_for_show(show_name))
15
+ end
16
+
17
+ def episodes_of(show_name)
18
+ episodes = Dir.new(directory_for_show(show_name)).entries - ['.', '..']
19
+ episodes.map do |episode|
20
+ metadata_for(show_name, episode)
21
+ end
22
+ end
23
+
24
+ def metadata_for(show_name, episode)
25
+ metadata_file_name = directory_for_show_and_episode(show_name, episode) + '/metadata.yml'
26
+ metadata = File.exists?(metadata_file_name) ? YAML.load_file(metadata_file_name) : {}
27
+
28
+ thumbnail_file_name = directory_for_show_and_episode(show_name, episode) + '/thumbnail.png'
29
+ has_thumbnail = File.exists?(thumbnail_file_name)
30
+
31
+ webm_file_name = directory_for_show_and_episode(show_name, episode) + '/stream.webm'
32
+ has_webm = File.exists?(webm_file_name)
33
+
34
+ RecordingMetadata.new(
35
+ has_thumbnail,
36
+ has_webm,
37
+ show_name,
38
+ episode,
39
+ metadata[:channel],
40
+ metadata[:subtitle],
41
+ metadata[:description],
42
+ metadata[:start_time],
43
+ metadata[:duration])
44
+ end
45
+
46
+ def delete_show_episode(show_name, episode)
47
+ FileUtils.rm_rf(@recordings_directory + '/' + show_name + '/' + episode)
48
+ end
49
+
50
+ def create_directory_for_recording(recording)
51
+ show_directory = directory_for_show(recording.show_name)
52
+ ensure_directory_exists(show_directory)
53
+
54
+ new_sequence_number = next_sequence_number_for(show_directory)
55
+ recording_directory = "#{show_directory}/#{new_sequence_number}"
56
+ ensure_directory_exists(recording_directory)
57
+
58
+ create_metadata(recording_directory, recording)
59
+
60
+ recording_directory
61
+ end
62
+
63
+ def directory_for_show_and_episode(show_name, episode)
64
+ directory_for_show(show_name) + '/' + episode
65
+ end
66
+
67
+ private
68
+ def directory_for_show(show_name)
69
+ sanitized_directory_name = show_name.gsub(/\"|\'|\*|\.|\/|\\|:/, '')
70
+ directory_name = sanitized_directory_name.present? ? sanitized_directory_name : 'Unnamed'
71
+ @recordings_directory + '/' + directory_name
72
+ end
73
+
74
+ def ensure_directory_exists(directory)
75
+ FileUtils.makedirs(directory) unless File.exists?(directory)
76
+ end
77
+
78
+ def next_sequence_number_for(base_directory)
79
+ largest_current_sequence_number = Dir.new(base_directory).map {|dir_name| dir_name.to_i }.max
80
+ 1 + largest_current_sequence_number
81
+ end
82
+
83
+ def create_metadata(directory, recording)
84
+ metadata = {
85
+ title: recording.show_name,
86
+ channel: recording.channel.name,
87
+ start_time: recording.start_time,
88
+ duration: recording.duration
89
+ }
90
+
91
+ if recording.programme
92
+ metadata.merge!({
93
+ subtitle: recording.programme.subtitle,
94
+ description: recording.programme.description
95
+ })
96
+ end
97
+
98
+ File.open(directory + '/metadata.yml', 'w') {|f| f.write(metadata.to_yaml) }
99
+ end
100
+ end
101
+ end
@@ -0,0 +1,72 @@
1
+ module SimplePvr
2
+ class RecordingPlanner
3
+ def self.read
4
+ planner = self.new
5
+ planner.read
6
+ end
7
+
8
+ def initialize
9
+ @recordings = []
10
+ end
11
+
12
+ def read
13
+ schedules = Model::Schedule.all
14
+ specifications = schedules.find_all {|s| s.type == :specification }
15
+ exceptions = schedules.find_all {|s| s.type == :exception }
16
+
17
+ specifications.each do |specification|
18
+ title = specification.title
19
+ if specification.channel && specification.start_time
20
+ programmes = Model::Programme.on_channel_with_title_and_start_time(specification.channel, specification.title, specification.start_time)
21
+ elsif specification.channel
22
+ programmes = Model::Programme.on_channel_with_title(specification.channel, specification.title)
23
+ else
24
+ programmes = Model::Programme.with_title(specification.title)
25
+ end
26
+
27
+ programmes_with_exceptions_removed = programmes.find_all {|programme| !matches_exception(programme, exceptions) }
28
+ programmes_filtered_by_weekdays = programmes_with_exceptions_removed.find_all {|programme| on_allowed_weekday(programme, specification) }
29
+ add_programmes(title, programmes_filtered_by_weekdays)
30
+ end
31
+
32
+ PvrInitializer.scheduler.recordings = @recordings
33
+ end
34
+
35
+ private
36
+ def matches_exception(programme, exceptions)
37
+ exceptions.any? do |exception|
38
+ programme.title == exception.title &&
39
+ programme.channel == exception.channel &&
40
+ programme.start_time == exception.start_time
41
+ end
42
+ end
43
+
44
+ def on_allowed_weekday(programme, specification)
45
+ return true unless specification.filter_by_weekday
46
+
47
+ date = programme.start_time
48
+ case true
49
+ when date.monday? then specification.monday
50
+ when date.tuesday? then specification.tuesday
51
+ when date.wednesday? then specification.wednesday
52
+ when date.thursday? then specification.thursday
53
+ when date.friday? then specification.friday
54
+ when date.saturday? then specification.saturday
55
+ when date.sunday? then specification.sunday
56
+ else false
57
+ end
58
+ end
59
+
60
+ def add_programmes(title, programmes)
61
+ programmes.each do |programme|
62
+ start_time = programme.start_time.advance(minutes: -2)
63
+ duration = programme.duration + 7.minutes
64
+ add_recording(title, programme.channel, start_time, duration, programme)
65
+ end
66
+ end
67
+
68
+ def add_recording(title, channel, start_time, duration, programme=nil)
69
+ @recordings << SimplePvr::Model::Recording.new(channel, title, start_time, duration, programme)
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,124 @@
1
+ module SimplePvr
2
+ class Scheduler
3
+ attr_reader :upcoming_recordings
4
+
5
+ def initialize
6
+ @number_of_tuners = 2
7
+ @upcoming_recordings, @current_recordings, @recorders = [], [nil] * @number_of_tuners, {}
8
+ @mutex = Mutex.new
9
+ end
10
+
11
+ def start
12
+ @thread = Thread.new do
13
+ while true
14
+ @mutex.synchronize { process }
15
+ sleep 1
16
+ end
17
+ end
18
+ end
19
+
20
+ def recordings=(recordings)
21
+ @mutex.synchronize do
22
+ @upcoming_recordings = recordings.sort_by {|r| r.start_time }.find_all {|r| !r.expired? }
23
+
24
+ @scheduled_programmes = programme_ids_from(@upcoming_recordings)
25
+ stop_current_recordings_not_relevant_anymore
26
+ @upcoming_recordings = remove_current_recordings(@upcoming_recordings)
27
+ mark_conflicting_recordings(@upcoming_recordings)
28
+ @conflicting_programmes = programme_ids_from(@upcoming_recordings.find_all {|r| r.conflicting? })
29
+ end
30
+ end
31
+
32
+ def is_scheduled?(programme)
33
+ @scheduled_programmes[programme.id] != nil
34
+ end
35
+
36
+ def is_conflicting?(programme)
37
+ @conflicting_programmes[programme.id] != nil
38
+ end
39
+
40
+ def status_text
41
+ @mutex.synchronize do
42
+ return 'Idle' unless is_recording?
43
+
44
+ status_texts = active_recordings.map {|recording| "'#{recording.show_name}' on channel '#{recording.channel.name}'"}
45
+ 'Recording ' + status_texts.join(', ')
46
+ end
47
+ end
48
+
49
+ def process
50
+ check_expiration_of_current_recordings
51
+ check_start_of_coming_recordings
52
+ end
53
+
54
+ private
55
+ def mark_conflicting_recordings(recordings)
56
+ concurrent_recordings = active_recordings
57
+ recordings.each do |recording|
58
+ concurrent_recordings = concurrent_recordings.reject {|r| r.expired_at(recording.start_time) }
59
+ recording.conflicting = concurrent_recordings.size >= @number_of_tuners
60
+ concurrent_recordings << recording unless recording.conflicting?
61
+ end
62
+ end
63
+
64
+ def is_recording?
65
+ !active_recordings.empty?
66
+ end
67
+
68
+ def active_recordings
69
+ @current_recordings.find_all {|recording| recording != nil }
70
+ end
71
+
72
+ def programme_ids_from(recordings)
73
+ result = {}
74
+ recordings.each do |recording|
75
+ result[recording.programme.id] = true if recording.programme
76
+ end
77
+ result
78
+ end
79
+
80
+ def remove_current_recordings(recordings)
81
+ recordings.find_all {|recording| !@current_recordings.include?(recording) }
82
+ end
83
+
84
+ def stop_current_recordings_not_relevant_anymore
85
+ @current_recordings.each do |recording|
86
+ stop_recording(recording) if recording && !@upcoming_recordings.include?(recording)
87
+ end
88
+ end
89
+
90
+ def check_expiration_of_current_recordings
91
+ @current_recordings.each do |recording|
92
+ stop_recording(recording) if recording && recording.expired?
93
+ end
94
+ end
95
+
96
+ def check_start_of_coming_recordings
97
+ while should_start_next_recording
98
+ start_next_recording
99
+ end
100
+ end
101
+
102
+ def should_start_next_recording
103
+ next_recording = @upcoming_recordings[0]
104
+ next_recording && next_recording.start_time <= Time.now
105
+ end
106
+
107
+ def stop_recording(recording)
108
+ @recorders[recording].stop!
109
+ @recorders[recording] = nil
110
+ @current_recordings[@current_recordings.find_index(recording)] = nil
111
+ end
112
+
113
+ def start_next_recording
114
+ next_recording = @upcoming_recordings.delete_at(0)
115
+ available_slot = @current_recordings.find_index(nil)
116
+ if available_slot
117
+ recorder = Recorder.new(available_slot, next_recording)
118
+ @current_recordings[available_slot] = next_recording
119
+ @recorders[next_recording] = recorder
120
+ recorder.start!
121
+ end
122
+ end
123
+ end
124
+ end
@@ -0,0 +1,13 @@
1
+ module SimplePvr
2
+ module Server
3
+ class AppController < BaseController
4
+ get '/app/*' do |path|
5
+ send_file File.join(settings.public_folder, path)
6
+ end
7
+
8
+ get '/*' do
9
+ send_file File.join(settings.public_folder, 'index.html')
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,94 @@
1
+ require 'sinatra/base'
2
+
3
+ Time::DATE_FORMATS[:programme_date] = '%F'
4
+ Time::DATE_FORMATS[:day] = '%a, %d %b'
5
+
6
+ module SimplePvr
7
+ module Server
8
+ class BaseController < Sinatra::Base
9
+ include ERB::Util
10
+
11
+ http_username, http_password = ENV['username'], ENV['password']
12
+ if http_username && http_password
13
+ PvrLogger.info('Securing server with Basic HTTP Authentication')
14
+ use Rack::Auth::Basic, 'Restricted Area' do |username, password|
15
+ [username, password] == [http_username, http_password]
16
+ end
17
+ else
18
+ PvrLogger.info('Beware: Unsecured server. Do not expose to the rest of the world!')
19
+ end
20
+
21
+ configure do
22
+ set :public_folder, File.dirname(__FILE__) + '/../../../public/'
23
+ mime_type :webm, 'video/webm'
24
+ end
25
+
26
+ def reload_schedules
27
+ RecordingPlanner.read
28
+ end
29
+
30
+ def programme_hash(programme)
31
+ is_scheduled = PvrInitializer.scheduler.is_scheduled?(programme)
32
+ {
33
+ id: programme.id,
34
+ channel: { id: programme.channel.id, name: programme.channel.name },
35
+ title: programme.title,
36
+ subtitle: programme.subtitle,
37
+ description: programme.description,
38
+ start_time: programme.start_time,
39
+ is_scheduled: is_scheduled,
40
+ episode_num: programme.episode_num
41
+ }
42
+ end
43
+
44
+ def recording_hash(show_id, recording)
45
+ path = PvrInitializer.recording_manager.directory_for_show_and_episode(show_id, recording.episode)
46
+ {
47
+ id: recording.episode,
48
+ show_id: show_id,
49
+ episode: recording.episode,
50
+ subtitle: recording.subtitle,
51
+ description: recording.description,
52
+ start_time: recording.start_time,
53
+ channel_name: recording.channel,
54
+ has_thumbnail: recording.has_thumbnail,
55
+ has_webm: recording.has_webm,
56
+ local_file_url: 'file://' + File.join(path, 'stream.ts')
57
+ }
58
+ end
59
+
60
+ def channel_with_current_programmes_hash(channel_with_current_programmes)
61
+ channel = channel_with_current_programmes[:channel]
62
+ current_programme = channel_with_current_programmes[:current_programme]
63
+ upcoming_programmes = channel_with_current_programmes[:upcoming_programmes]
64
+
65
+ current_programme_map = current_programme ? programme_summary_hash(current_programme) : nil
66
+ upcoming_programmes_map = programme_summaries_hash(upcoming_programmes)
67
+
68
+ {
69
+ id: channel.id,
70
+ name: channel.name,
71
+ hidden: channel.hidden,
72
+ icon_url: channel.icon_url,
73
+ current_programme: channel,
74
+ current_programme: current_programme_map,
75
+ upcoming_programmes: upcoming_programmes_map
76
+ }
77
+ end
78
+
79
+ def programme_summaries_hash(programmes)
80
+ programmes.map {|programme| programme_summary_hash(programme) }
81
+ end
82
+
83
+ def programme_summary_hash(programme)
84
+ {
85
+ id: programme.id,
86
+ title: programme.title,
87
+ start_time: programme.start_time,
88
+ is_scheduled: PvrInitializer.scheduler.is_scheduled?(programme),
89
+ is_conflicting: PvrInitializer.scheduler.is_conflicting?(programme)
90
+ }
91
+ end
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,68 @@
1
+ module SimplePvr
2
+ module Server
3
+ class ChannelsController < BaseController
4
+ get '/' do
5
+ Model::Channel.all_with_current_programmes.map do |channel_with_current_programmes|
6
+ channel_with_current_programmes_hash(channel_with_current_programmes)
7
+ end.to_json
8
+ end
9
+
10
+ get '/:id' do |id|
11
+ channel_with_current_programmes_hash(Model::Channel.with_current_programmes(id)).to_json
12
+ end
13
+
14
+ post '/:id/hide' do |id|
15
+ channel = Model::Channel.get(id)
16
+ channel.hidden = true
17
+ channel.save
18
+ {
19
+ id: channel.id,
20
+ name: channel.name,
21
+ hidden: channel.hidden
22
+ }.to_json
23
+ end
24
+
25
+ post '/:id/show' do |id|
26
+ channel = Model::Channel.get(id)
27
+ channel.hidden = false
28
+ channel.save
29
+ {
30
+ id: channel.id,
31
+ name: channel.name,
32
+ hidden: channel.hidden
33
+ }.to_json
34
+ end
35
+
36
+ get '/:channel_id/programme_listings/:date/?' do |channel_id, date_string|
37
+ if date_string == 'today'
38
+ now = Time.now
39
+ this_date = Time.local(now.year, now.month, now.day)
40
+ else
41
+ this_date = Time.parse(date_string)
42
+ end
43
+ previous_date = this_date.advance(days: -7)
44
+ next_date = this_date.advance(days: 7)
45
+ channel = Model::Channel.get(channel_id)
46
+
47
+ days = (0..6).map do |date_advanced|
48
+ from_date = this_date.advance(days: date_advanced)
49
+ to_date = this_date.advance(days: date_advanced + 1)
50
+ programmes = Model::Programme.all(channel: channel, start_time: (from_date..to_date), order: :start_time)
51
+
52
+ {
53
+ date: from_date.to_s(:programme_date),
54
+ programmes: programme_summaries_hash(programmes)
55
+ }
56
+ end
57
+
58
+ {
59
+ channel: { id: channel.id, name: channel.name, icon_url: channel.icon_url },
60
+ previous_date: previous_date.to_s(:programme_date),
61
+ this_date: this_date.to_s(:programme_date),
62
+ next_date: next_date.to_s(:programme_date),
63
+ days: days
64
+ }.to_json
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,8 @@
1
+ SimplePvr::PvrInitializer.setup
2
+ SimplePvr::RecordingPlanner.read
3
+
4
+ # Due to a lazy initialization bug in ActiveSupport:
5
+ # http://stackoverflow.com/questions/5267700/undefined-method-encode-for-activesupportjsonmodule
6
+ {:a => :b}.to_json
7
+
8
+ eval SimplePvr::PvrInitializer.rack_maps_file
@@ -0,0 +1,46 @@
1
+ module SimplePvr
2
+ module Server
3
+ class ProgrammesController < BaseController
4
+ get '/title_search' do
5
+ Model::Programme.titles_containing(params['query']).to_json
6
+ end
7
+
8
+ get '/search' do
9
+ Model::Programme.with_title_containing(params['query']).map {|programme| programme_hash(programme) }.to_json
10
+ end
11
+
12
+ get '/:id' do |id|
13
+ programme = Model::Programme.get(id)
14
+ programme_hash(programme).to_json
15
+ end
16
+
17
+ post '/:id/record_on_any_channel' do |id|
18
+ programme = Model::Programme.get(id.to_i)
19
+ Model::Schedule.add_specification(title: programme.title)
20
+ reload_schedules
21
+ programme_hash(programme).to_json
22
+ end
23
+
24
+ post '/:id/record_on_this_channel' do |id|
25
+ programme = Model::Programme.get(id.to_i)
26
+ Model::Schedule.add_specification(title: programme.title, channel: programme.channel)
27
+ reload_schedules
28
+ programme_hash(programme).to_json
29
+ end
30
+
31
+ post '/:id/record_just_this_programme' do |id|
32
+ programme = Model::Programme.get(id.to_i)
33
+ Model::Schedule.add_specification(title: programme.title, channel: programme.channel, start_time: programme.start_time)
34
+ reload_schedules
35
+ programme_hash(programme).to_json
36
+ end
37
+
38
+ post '/:id/exclude' do |id|
39
+ programme = Model::Programme.get(id.to_i)
40
+ Model::Schedule.create(type: :exception, title: programme.title, channel: programme.channel, start_time: programme.start_time)
41
+ reload_schedules
42
+ programme_hash(programme).to_json
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,7 @@
1
+ map('/') { run SimplePvr::Server::AppController }
2
+ map('/api/channels') { run SimplePvr::Server::ChannelsController }
3
+ map('/api/programmes') { run SimplePvr::Server::ProgrammesController }
4
+ map('/api/schedules') { run SimplePvr::Server::SchedulesController }
5
+ map('/api/shows') { run SimplePvr::Server::ShowsController }
6
+ map('/api/status') { run SimplePvr::Server::StatusController }
7
+ map('/api/upcoming_recordings') { run SimplePvr::Server::UpcomingRecordingsController }