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,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 }