simple_pvr 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.rspec +1 -0
- data/Gemfile +4 -0
- data/Gemfile.lock +133 -0
- data/LICENSE.txt +13 -0
- data/README.md +169 -0
- data/Rakefile +16 -0
- data/bin/pvr_server +10 -0
- data/bin/pvr_xmltv +18 -0
- data/features/channel_overview.feature +48 -0
- data/features/programme_search.feature +20 -0
- data/features/scheduling.feature +112 -0
- data/features/step_definitions/pvr_steps.rb +104 -0
- data/features/step_definitions/web_steps.rb +219 -0
- data/features/support/env.rb +28 -0
- data/features/support/paths.rb +17 -0
- data/features/week_overview.feature +39 -0
- data/lib/simple_pvr/ffmpeg.rb +22 -0
- data/lib/simple_pvr/hdhomerun.rb +103 -0
- data/lib/simple_pvr/hdhomerun_save.sh +10 -0
- data/lib/simple_pvr/model/channel.rb +72 -0
- data/lib/simple_pvr/model/database_initializer.rb +36 -0
- data/lib/simple_pvr/model/programme.rb +58 -0
- data/lib/simple_pvr/model/recording.rb +45 -0
- data/lib/simple_pvr/model/schedule.rb +33 -0
- data/lib/simple_pvr/pvr_initializer.rb +47 -0
- data/lib/simple_pvr/pvr_logger.rb +14 -0
- data/lib/simple_pvr/recorder.rb +25 -0
- data/lib/simple_pvr/recording_manager.rb +101 -0
- data/lib/simple_pvr/recording_planner.rb +72 -0
- data/lib/simple_pvr/scheduler.rb +124 -0
- data/lib/simple_pvr/server/app_controller.rb +13 -0
- data/lib/simple_pvr/server/base_controller.rb +94 -0
- data/lib/simple_pvr/server/channels_controller.rb +68 -0
- data/lib/simple_pvr/server/config.ru +8 -0
- data/lib/simple_pvr/server/programmes_controller.rb +46 -0
- data/lib/simple_pvr/server/rack_maps.rb +7 -0
- data/lib/simple_pvr/server/schedules_controller.rb +71 -0
- data/lib/simple_pvr/server/shows_controller.rb +63 -0
- data/lib/simple_pvr/server/status_controller.rb +11 -0
- data/lib/simple_pvr/server/upcoming_recordings_controller.rb +18 -0
- data/lib/simple_pvr/version.rb +3 -0
- data/lib/simple_pvr/xmltv_reader.rb +83 -0
- data/lib/simple_pvr.rb +22 -0
- data/public/css/bootstrap-responsive.min.css +9 -0
- data/public/css/bootstrap.min.css +9 -0
- data/public/css/simplepvr.css +11 -0
- data/public/img/glyphicons-halflings-white.png +0 -0
- data/public/img/glyphicons-halflings.png +0 -0
- data/public/index.html +55 -0
- data/public/js/angular/angular-resource.min.js +10 -0
- data/public/js/angular/angular.min.js +157 -0
- data/public/js/app.js +145 -0
- data/public/js/bootstrap/bootstrap.min.js +6 -0
- data/public/js/controllers.js +156 -0
- data/public/js/services.js +27 -0
- data/public/partials/about.html +5 -0
- data/public/partials/channels.html +41 -0
- data/public/partials/programme.html +20 -0
- data/public/partials/programmeListing.html +18 -0
- data/public/partials/schedule.html +80 -0
- data/public/partials/schedules.html +44 -0
- data/public/partials/search.html +17 -0
- data/public/partials/show.html +21 -0
- data/public/partials/shows.html +7 -0
- data/public/partials/status.html +6 -0
- data/simple_pvr.gemspec +30 -0
- data/spec/resources/channels.txt +11 -0
- data/spec/resources/programs-without-icon.xmltv +95 -0
- data/spec/resources/programs.xmltv +98 -0
- data/spec/simple_pvr/ffmpeg_spec.rb +26 -0
- data/spec/simple_pvr/hdhomerun_spec.rb +82 -0
- data/spec/simple_pvr/model/channel_spec.rb +114 -0
- data/spec/simple_pvr/model/programme_spec.rb +110 -0
- data/spec/simple_pvr/model/schedule_spec.rb +47 -0
- data/spec/simple_pvr/pvr_initializer_spec.rb +50 -0
- data/spec/simple_pvr/recorder_spec.rb +32 -0
- data/spec/simple_pvr/recording_manager_spec.rb +158 -0
- data/spec/simple_pvr/recording_planner_spec.rb +104 -0
- data/spec/simple_pvr/scheduler_spec.rb +201 -0
- data/spec/simple_pvr/xmltv_reader_spec.rb +49 -0
- data/test/config/jsTestDriver-scenario.conf +10 -0
- data/test/config/jsTestDriver.conf +12 -0
- data/test/config/jstd-scenario-adapter-config.js +6 -0
- data/test/filtersSpec.js +97 -0
- data/test/lib/angular/angular-mocks.js +1719 -0
- data/test/lib/angular/angular-scenario.js +25937 -0
- data/test/lib/angular/jstd-scenario-adapter.js +185 -0
- data/test/lib/angular/version.txt +1 -0
- data/test/lib/jasmine/MIT.LICENSE +20 -0
- data/test/lib/jasmine/index.js +180 -0
- data/test/lib/jasmine/jasmine-html.js +190 -0
- data/test/lib/jasmine/jasmine.css +166 -0
- data/test/lib/jasmine/jasmine.js +2476 -0
- data/test/lib/jasmine/jasmine_favicon.png +0 -0
- data/test/lib/jasmine/version.txt +1 -0
- data/test/lib/jasmine-jstd-adapter/JasmineAdapter.js +196 -0
- data/test/lib/jasmine-jstd-adapter/version.txt +1 -0
- data/test/lib/jstestdriver/JsTestDriver.jar +0 -0
- data/test/lib/jstestdriver/version.txt +1 -0
- data/test/scripts/test-server.sh +14 -0
- data/test/scripts/test.sh +8 -0
- 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 }
|