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,156 @@
|
|
1
|
+
'use strict';
|
2
|
+
|
3
|
+
function SchedulesCtrl($scope, $http, Schedule, UpcomingRecording, Channel) {
|
4
|
+
var updateView = function() {
|
5
|
+
$scope.schedules = Schedule.query();
|
6
|
+
$scope.upcomingRecordings = UpcomingRecording.query();
|
7
|
+
$scope.newSchedule = { title: null, channelId: 0 }
|
8
|
+
}
|
9
|
+
|
10
|
+
$scope.channels = Channel.query();
|
11
|
+
updateView();
|
12
|
+
|
13
|
+
$scope.createSchedule = function() {
|
14
|
+
var schedule = new Schedule({ title: $scope.newSchedule.title, channel_id: $scope.newSchedule.channelId })
|
15
|
+
schedule.$save(updateView);
|
16
|
+
}
|
17
|
+
|
18
|
+
$scope.deleteSchedule = function(schedule) {
|
19
|
+
schedule.$delete(updateView);
|
20
|
+
}
|
21
|
+
|
22
|
+
$scope.excludeRecording = function(recording) {
|
23
|
+
$http.post('/api/programmes/' + recording.programme_id + '/exclude').success(updateView);
|
24
|
+
}
|
25
|
+
}
|
26
|
+
|
27
|
+
function ScheduleCtrl($scope, $routeParams, $location, Schedule, Channel) {
|
28
|
+
$scope.channels = Channel.query(function() {
|
29
|
+
$scope.schedule = Schedule.get({id: $routeParams.scheduleId}, function() {
|
30
|
+
for (var i=0; i<$scope.channels.length; i++) {
|
31
|
+
var channel = $scope.channels[i]
|
32
|
+
if ($scope.schedule.channel && channel.id === $scope.schedule.channel.id) {
|
33
|
+
$scope.channel = channel;
|
34
|
+
}
|
35
|
+
}
|
36
|
+
});
|
37
|
+
});
|
38
|
+
|
39
|
+
$scope.update = function() {
|
40
|
+
$scope.schedule.channel = $scope.channel;
|
41
|
+
$scope.schedule.$save(function() { $location.path('/schedules'); });
|
42
|
+
}
|
43
|
+
}
|
44
|
+
|
45
|
+
function ChannelsCtrl($scope, $http, Channel) {
|
46
|
+
$scope.channels = Channel.query();
|
47
|
+
$scope.showHiddenChannels = false;
|
48
|
+
|
49
|
+
$scope.classForProgrammeLine = function(programme) {
|
50
|
+
if (programme == null) {
|
51
|
+
return '';
|
52
|
+
}
|
53
|
+
return programme.is_conflicting ? 'error' : (programme.is_scheduled ? 'success' : '');
|
54
|
+
}
|
55
|
+
$scope.hideChannel = function(channel) {
|
56
|
+
// I wish Angular could let me define this operation on the Channel object
|
57
|
+
$http.post('/api/channels/' + channel.id + '/hide').success(function() { channel.$get(); });
|
58
|
+
}
|
59
|
+
$scope.showChannel = function(channel) {
|
60
|
+
// I wish Angular could let me define this operation on the Channel object
|
61
|
+
$http.post('/api/channels/' + channel.id + '/show').success(function() { channel.$get(); });
|
62
|
+
}
|
63
|
+
$scope.shouldShowChannel = function(channel) {
|
64
|
+
return $scope.showHiddenChannels || !channel.hidden;
|
65
|
+
}
|
66
|
+
}
|
67
|
+
|
68
|
+
function ProgrammeListingCtrl($scope, $routeParams, ProgrammeListing) {
|
69
|
+
$scope.channelId = $routeParams.channelId;
|
70
|
+
$scope.date = $routeParams.date;
|
71
|
+
$scope.programmeListing = ProgrammeListing.get({channelId: $scope.channelId, date: $scope.date});
|
72
|
+
|
73
|
+
$scope.classForProgrammeLine = function(programme) {
|
74
|
+
return programme.is_conflicting ? 'error' : (programme.is_scheduled ? 'success' : '');
|
75
|
+
}
|
76
|
+
}
|
77
|
+
|
78
|
+
function ProgrammeCtrl($scope, $routeParams, $http, Programme) {
|
79
|
+
var loadProgramme = function() {
|
80
|
+
$scope.programme = Programme.get({id: $routeParams.programmeId});
|
81
|
+
}
|
82
|
+
var post = function(url) {
|
83
|
+
$http.post(url).success(loadProgramme);
|
84
|
+
}
|
85
|
+
|
86
|
+
$scope.recordOnThisChannel = function() {
|
87
|
+
// I wish Angular could let me define this operation on the Programme object
|
88
|
+
post('/api/programmes/' + $scope.programme.id + '/record_on_this_channel');
|
89
|
+
}
|
90
|
+
$scope.recordOnAnyChannel = function() {
|
91
|
+
// I wish Angular could let me define this operation on the Programme object
|
92
|
+
post('/api/programmes/' + $scope.programme.id + '/record_on_any_channel');
|
93
|
+
}
|
94
|
+
$scope.recordJustThisProgramme = function() {
|
95
|
+
// I wish Angular could let me define this operation on the Programme object
|
96
|
+
post('/api/programmes/' + $scope.programme.id + '/record_just_this_programme');
|
97
|
+
}
|
98
|
+
|
99
|
+
loadProgramme();
|
100
|
+
}
|
101
|
+
|
102
|
+
function ShowsCtrl($scope, $http, Show) {
|
103
|
+
var loadShows = function() {
|
104
|
+
$scope.shows = Show.query();
|
105
|
+
}
|
106
|
+
|
107
|
+
$scope.deleteEpisodes = function(show) {
|
108
|
+
if (confirm("Really delete all episodes of\n" + show.name + "\n?")) {
|
109
|
+
show.$delete(loadShows);
|
110
|
+
}
|
111
|
+
}
|
112
|
+
|
113
|
+
loadShows();
|
114
|
+
}
|
115
|
+
|
116
|
+
function ShowCtrl($scope, $routeParams, $http, Show, Recording) {
|
117
|
+
var loadRecordings = function() {
|
118
|
+
$scope.recordings = Recording.query({showId: $routeParams.showId});
|
119
|
+
}
|
120
|
+
|
121
|
+
$scope.deleteRecording = function(recording) {
|
122
|
+
if (confirm("Really delete recording\n" + recording.episode + "\nof show\n" + $scope.show.name + "\n?")) {
|
123
|
+
recording.$delete(loadRecordings);
|
124
|
+
}
|
125
|
+
}
|
126
|
+
|
127
|
+
$scope.startTranscoding = function(recording) {
|
128
|
+
// I wish Angular could let me define this operation on the Programme object
|
129
|
+
$http.post('/api/shows/' + $routeParams.showId + '/recordings/' + recording.id + '/transcode').success(loadRecordings);
|
130
|
+
}
|
131
|
+
|
132
|
+
$scope.show = Show.get({id: $routeParams.showId});
|
133
|
+
loadRecordings();
|
134
|
+
}
|
135
|
+
|
136
|
+
function SearchProgrammesCtrl($scope, $http, $location) {
|
137
|
+
$scope.autocomplete = function(query, process) {
|
138
|
+
$http.get('/api/programmes/title_search', {params: {query: query}}).success(process);
|
139
|
+
}
|
140
|
+
|
141
|
+
$scope.search = function() {
|
142
|
+
var query = $("#programme-search-query").val();
|
143
|
+
$location.path('/search').search({query: query});
|
144
|
+
}
|
145
|
+
}
|
146
|
+
|
147
|
+
function SearchCtrl($scope, $routeParams, $http) {
|
148
|
+
$scope.query = $routeParams.query;
|
149
|
+
$http.get('/api/programmes/search', {params: {query: $scope.query}}).success(function(result) {
|
150
|
+
$scope.result = result;
|
151
|
+
});
|
152
|
+
}
|
153
|
+
|
154
|
+
function StatusCtrl($scope, Status) {
|
155
|
+
$scope.status = Status.get();
|
156
|
+
}
|
@@ -0,0 +1,27 @@
|
|
1
|
+
'use strict';
|
2
|
+
|
3
|
+
angular.module('simplePvrServices', ['ngResource']).
|
4
|
+
factory('UpcomingRecording', function($resource) {
|
5
|
+
return $resource('/api/upcoming_recordings/:id');
|
6
|
+
}).
|
7
|
+
factory('Schedule', function($resource) {
|
8
|
+
return $resource('/api/schedules/:id', {id: '@id'});
|
9
|
+
}).
|
10
|
+
factory('Channel', function($resource) {
|
11
|
+
return $resource('/api/channels/:id', {id: '@id'});
|
12
|
+
}).
|
13
|
+
factory('ProgrammeListing', function($resource) {
|
14
|
+
return $resource('/api/channels/:channelId/programme_listings/:date');
|
15
|
+
}).
|
16
|
+
factory('Programme', function($resource) {
|
17
|
+
return $resource('/api/programmes/:id');
|
18
|
+
}).
|
19
|
+
factory('Show', function($resource) {
|
20
|
+
return $resource('/api/shows/:id', {id: '@id'});
|
21
|
+
}).
|
22
|
+
factory('Recording', function($resource) {
|
23
|
+
return $resource('/api/shows/:showId/recordings/:recordingId', {showId: '@show_id', recordingId: '@id'});
|
24
|
+
}).
|
25
|
+
factory('Status', function($resource) {
|
26
|
+
return $resource('/api/status');
|
27
|
+
});
|
@@ -0,0 +1,5 @@
|
|
1
|
+
<div class="hero-unit">
|
2
|
+
<h1>SimplePVR</h1>
|
3
|
+
<p>A really, really simple PVR (Personal Video Recorder) system which only supports the HDHomeRun network tuners. It's written in Ruby and is highly hackable. If you don't want to hack it, but just want a solid PVR system, no worries: It's dead-simple to use.</p>
|
4
|
+
<p><a href="https://github.com/olefriis/simplepvr" class="btn btn-primary btn-large">Learn more »</a></p>
|
5
|
+
</div>
|
@@ -0,0 +1,41 @@
|
|
1
|
+
<div class="row">
|
2
|
+
<div class="span12">
|
3
|
+
<form class="form-inline">
|
4
|
+
<input id="channel_filter" type="text" placeholder="Filter..." ng-model="channelFilter">
|
5
|
+
<label class="checkbox">
|
6
|
+
<input type="checkbox" ng-model="showHiddenChannels">Show hidden channels</input>
|
7
|
+
</label>
|
8
|
+
</form>
|
9
|
+
</div>
|
10
|
+
</div>
|
11
|
+
<div class="row" ng-repeat="channelChunk in channels | orderBy:name | filter:shouldShowChannel | filter:channelFilter | chunk:3">
|
12
|
+
<div ng-repeat="channel in channelChunk" class="span4">
|
13
|
+
<table class="table table-condensed">
|
14
|
+
<thead>
|
15
|
+
<tr>
|
16
|
+
<th colspan="2">
|
17
|
+
<img ng-show="channel.icon_url" class="channel-icon" ng-src="{{channel.icon_url}}" />
|
18
|
+
<span class="channel-name">
|
19
|
+
{{channel.name}}
|
20
|
+
</span>
|
21
|
+
<a ng-href="channels" ng-hide="channel.hidden" class="icon-remove pull-right" ng-click="hideChannel(channel)" title="Hide channel" ></a>
|
22
|
+
<a ng-href="channels" ng-show="channel.hidden" class="icon-ok pull-right" ng-click="showChannel(channel)" title="Show channel"></a>
|
23
|
+
</th>
|
24
|
+
</tr>
|
25
|
+
</thead>
|
26
|
+
<tbody>
|
27
|
+
<tr ng-show="channel.current_programme" ng-class="classForProgrammeLine(channel.current_programme)">
|
28
|
+
<td class="span1">{{channel.current_programme.start_time | date:'HH:mm'}}</td>
|
29
|
+
<td><a ng-href="/programmes/{{channel.current_programme.id}}">{{channel.current_programme.title}}</td></tr>
|
30
|
+
<tr ng-repeat="upcomingProgramme in channel.upcoming_programmes" ng-class="classForProgrammeLine(upcomingProgramme)">
|
31
|
+
<td class="span1">{{upcomingProgramme.start_time | date:'HH:mm'}}</td>
|
32
|
+
<td><a ng-href="/programmes/{{upcomingProgramme.id}}">{{upcomingProgramme.title}}</a></td></tr>
|
33
|
+
<tr>
|
34
|
+
<td colspan="2">
|
35
|
+
<a ng-href="/channels/{{channel.id}}/programmeListings/today" class="pull-right">...</a>
|
36
|
+
</td>
|
37
|
+
</tr>
|
38
|
+
</tbody>
|
39
|
+
</table>
|
40
|
+
</div>
|
41
|
+
</div>
|
@@ -0,0 +1,20 @@
|
|
1
|
+
<div class="span6">
|
2
|
+
<div class="row" ng-show="programme.id">
|
3
|
+
<h1>{{programme.title}}</h1>
|
4
|
+
<p>{{programme.start_time | date:'fullDate'}} at {{programme.start_time | date:'HH:mm'}} on channel {{programme.channel.name}}.</p>
|
5
|
+
<p>{{programme.subtitle}}</p>
|
6
|
+
<p>{{programme.description}}</p>
|
7
|
+
|
8
|
+
<div ng-show="programme.episode_num">Episode {{programme.episode_num | formatEpisode}}</div>
|
9
|
+
<div ng-show="programme.is_scheduled">
|
10
|
+
<p>This programme is being recorded.</p>
|
11
|
+
</div>
|
12
|
+
<div ng-hide="programme.is_scheduled">
|
13
|
+
<p>
|
14
|
+
<button class="btn btn-primary" type="button" ng-click="recordOnThisChannel()">Record on this channel</button>
|
15
|
+
<button class="btn btn-primary" type="button" ng-click="recordOnAnyChannel()">Record on any channel</button>
|
16
|
+
<button class="btn btn-primary" type="button" ng-click="recordJustThisProgramme()">Record just this programme</button>
|
17
|
+
</p>
|
18
|
+
</div>
|
19
|
+
</div>
|
20
|
+
</div>
|
@@ -0,0 +1,18 @@
|
|
1
|
+
<p>
|
2
|
+
<a ng-show="programmeListing.previous_date" ng-href="/channels/{{programmeListing.channel.id}}/programmeListings/{{programmeListing.previous_date}}"><<</a>
|
3
|
+
<a ng-show="programmeListing.next_date" ng-href="/channels/{{programmeListing.channel.id}}/programmeListings/{{programmeListing.next_date}}">>></a>
|
4
|
+
<input type="text" placeholder="Filter..." ng-model="programmeFilter">
|
5
|
+
<img class="channel-icon" src="{{programmeListing.channel.icon_url}}" />
|
6
|
+
<span class="channel-name">{{programmeListing.channel.name}}</span>
|
7
|
+
</p>
|
8
|
+
<div class="row" ng-repeat="dayChunk in programmeListing.days | chunk:3">
|
9
|
+
<div class="span4" ng-repeat="day in dayChunk">
|
10
|
+
<table class="table table-condensed table-striped">
|
11
|
+
<th colspan="2">{{day.date | date:'fullDate'}}</th>
|
12
|
+
<tr ng-repeat="programme in day.programmes | filter:programmeFilter" class="{{classForProgrammeLine(programme)}}">
|
13
|
+
<td>{{programme.start_time | date:'HH:mm'}}</td>
|
14
|
+
<td><a ng-href="/programmes/{{programme.id}}">{{programme.title}}</td>
|
15
|
+
</tr>
|
16
|
+
</table>
|
17
|
+
</div>
|
18
|
+
</div>
|
@@ -0,0 +1,80 @@
|
|
1
|
+
<div class="row">
|
2
|
+
<form class="form-horizontal">
|
3
|
+
<div class="control-group">
|
4
|
+
<label class="control-label" for="showName">Name of show</label>
|
5
|
+
<div class="controls">
|
6
|
+
<input type="text" ng-model="schedule.title" id="showName" placeholder="Show name">
|
7
|
+
</div>
|
8
|
+
</div>
|
9
|
+
<div class="control-group">
|
10
|
+
<label class="control-label" for="channel">On channel</label>
|
11
|
+
<div class="controls">
|
12
|
+
<select id="channel" ng-model="channel" ng-options="c.name for c in channels">
|
13
|
+
<option value="">-- Any channel --</option>
|
14
|
+
</select>
|
15
|
+
</div>
|
16
|
+
</div>
|
17
|
+
<div class="control-group">
|
18
|
+
<div class="controls">
|
19
|
+
<label class="checkbox" for="filterByWeekday">
|
20
|
+
<input type="checkbox" ng-model="schedule.filter_by_weekday">Filter by weekday
|
21
|
+
</label>
|
22
|
+
</div>
|
23
|
+
</div>
|
24
|
+
<div class="control-group" ng-show="schedule.filter_by_weekday">
|
25
|
+
<div class="controls">
|
26
|
+
<label class="checkbox" for="recordMondays">
|
27
|
+
<input type="checkbox" ng-model="schedule.monday">Record Mondays
|
28
|
+
</label>
|
29
|
+
</div>
|
30
|
+
</div>
|
31
|
+
<div class="control-group" ng-show="schedule.filter_by_weekday">
|
32
|
+
<div class="controls">
|
33
|
+
<label class="checkbox" for="recordTuesdays">
|
34
|
+
<input type="checkbox" ng-model="schedule.tuesday">Record Tuesdays
|
35
|
+
</label>
|
36
|
+
</div>
|
37
|
+
</div>
|
38
|
+
<div class="control-group" ng-show="schedule.filter_by_weekday">
|
39
|
+
<div class="controls">
|
40
|
+
<label class="checkbox" for="recordWednesdays">
|
41
|
+
<input type="checkbox" ng-model="schedule.wednesday">Record Wednesdays
|
42
|
+
</label>
|
43
|
+
</div>
|
44
|
+
</div>
|
45
|
+
<div class="control-group" ng-show="schedule.filter_by_weekday">
|
46
|
+
<div class="controls">
|
47
|
+
<label class="checkbox" for="recordThursdays">
|
48
|
+
<input type="checkbox" ng-model="schedule.thursday">Record Thursdays
|
49
|
+
</label>
|
50
|
+
</div>
|
51
|
+
</div>
|
52
|
+
<div class="control-group" ng-show="schedule.filter_by_weekday">
|
53
|
+
<div class="controls">
|
54
|
+
<label class="checkbox" for="recordFridays">
|
55
|
+
<input type="checkbox" ng-model="schedule.friday">Record Fridays
|
56
|
+
</label>
|
57
|
+
</div>
|
58
|
+
</div>
|
59
|
+
<div class="control-group" ng-show="schedule.filter_by_weekday">
|
60
|
+
<div class="controls">
|
61
|
+
<label class="checkbox" for="recordSaturdays">
|
62
|
+
<input type="checkbox" ng-model="schedule.saturday">Record Saturdays
|
63
|
+
</label>
|
64
|
+
</div>
|
65
|
+
</div>
|
66
|
+
<div class="control-group" ng-show="schedule.filter_by_weekday">
|
67
|
+
<div class="controls">
|
68
|
+
<label class="checkbox" for="recordSundays">
|
69
|
+
<input type="checkbox" ng-model="schedule.sunday">Record Sundays
|
70
|
+
</label>
|
71
|
+
</div>
|
72
|
+
</div>
|
73
|
+
<div class="control-group">
|
74
|
+
<div class="controls">
|
75
|
+
<button type="submit" class="btn btn-primary" ng-click="update()">Update</button>
|
76
|
+
<a ng-href="/schedules" class="btn">Cancel</a>
|
77
|
+
</div>
|
78
|
+
</div>
|
79
|
+
</form>
|
80
|
+
</div>
|
@@ -0,0 +1,44 @@
|
|
1
|
+
<div class="row">
|
2
|
+
<div class="span6" id="schedules">
|
3
|
+
<h1>Existing schedules</h1>
|
4
|
+
<div class="row" ng-repeat="schedule in schedules">
|
5
|
+
<div class="span6">
|
6
|
+
<p><span ng-show="schedule.is_exception">Exception:</span> {{schedule.title}}
|
7
|
+
<span ng-show="schedule.channel">on {{schedule.channel.name}}</span>
|
8
|
+
<span ng-show="schedule.start_time">{{schedule.start_time | date:'fullDate'}} at {{schedule.start_time | date:'HH:mm'}}</span>
|
9
|
+
{{schedule | filteredWeekdays}}
|
10
|
+
</p>
|
11
|
+
<p>
|
12
|
+
<a ng-href="/schedules/{{schedule.id}}" class="btn">Edit</a>
|
13
|
+
<a ng-click="deleteSchedule(schedule)" class="btn btn-warning">Delete</a>
|
14
|
+
</p>
|
15
|
+
</div>
|
16
|
+
</div>
|
17
|
+
<div class="row">
|
18
|
+
<div class="span6">
|
19
|
+
<h1>Create new schedule</h1>
|
20
|
+
<form>
|
21
|
+
<label class="control-label" for="newScheduleTitle">Name</label>
|
22
|
+
<input id="newScheduleTitle" type="text" ng-model="newSchedule.title"></p>
|
23
|
+
<label class="control-label" for="newScheduleChannel">Channel</label>
|
24
|
+
<select id="newScheduleChannel" ng-model="newSchedule.channelId">
|
25
|
+
<option value="0">-- Any channel --</option>
|
26
|
+
<option ng-repeat="channel in channels" value="{{channel.id}}">{{channel.name}}</option>
|
27
|
+
</select></p>
|
28
|
+
<span ng-show="newSchedule.title"><button ng-click="createSchedule()" class="btn">Create schedule</button></span>
|
29
|
+
</form>
|
30
|
+
</div>
|
31
|
+
</div>
|
32
|
+
</div>
|
33
|
+
<div class="span6" id="upcoming_recordings">
|
34
|
+
<h1>Upcoming recordings</h1>
|
35
|
+
<div class="row" ng-repeat="recording in upcomingRecordings">
|
36
|
+
<div class="span6">
|
37
|
+
<h2>{{recording.show_name}}<span ng-show="recording.is_conflicting" style="color: red"> (Conflicting)</span> <a ng-click="excludeRecording(recording)"title="Do not record this specific show"><i class="icon-remove"></i></a></h2>
|
38
|
+
<p>{{recording.start_time | date:'fullDate'}} at {{recording.start_time | date:'HH:mm'}} on {{recording.channel.name}}</p>
|
39
|
+
<p ng-show="recording.subtitle">{{recording.subtitle}}</p>
|
40
|
+
<p><a ng-href="/programmes/{{recording.programme_id}}">View details</a></p>
|
41
|
+
</div>
|
42
|
+
</div>
|
43
|
+
</div>
|
44
|
+
</div>
|
@@ -0,0 +1,17 @@
|
|
1
|
+
<div class="row">
|
2
|
+
<div class="span12">
|
3
|
+
<h1>Results for {{query}}</h1>
|
4
|
+
</div>
|
5
|
+
</div>
|
6
|
+
<div class="row" ng-repeat="programmeChunk in result | chunk:2">
|
7
|
+
<div class="span6" ng-repeat="programme in programmeChunk">
|
8
|
+
<h2>{{programme.title}}</h2>
|
9
|
+
<p>{{programme.start_time | date:'fullDate'}} at {{programme.start_time | date:'HH:mm'}} on channel {{programme.channel.name}}.</p>
|
10
|
+
<p>{{programme.subtitle}}</p>
|
11
|
+
<p>{{programme.description}}</p>
|
12
|
+
<p><a ng-href="/programmes/{{programme.id}}">Show details</a></p>
|
13
|
+
<div ng-show="programme.is_scheduled">
|
14
|
+
<p>This programme is being recorded.</p>
|
15
|
+
</div>
|
16
|
+
</div>
|
17
|
+
</div>
|
@@ -0,0 +1,21 @@
|
|
1
|
+
<div class="row">
|
2
|
+
<div class="span12">
|
3
|
+
<h1>Recordings of {{show.name}}</h1>
|
4
|
+
</div>
|
5
|
+
</div>
|
6
|
+
<div class="row" ng-repeat="recordingChunk in recordings | orderBy:'start_time' | chunk:2">
|
7
|
+
<div class="span6" ng-repeat="recording in recordingChunk">
|
8
|
+
<h2>{{recording.episode}}</h2>
|
9
|
+
<img ng-show="recording.has_thumbnail" class="thumbnail" ng-src="/api/shows/{{show.name}}/recordings/{{recording.episode}}/thumbnail.png">
|
10
|
+
<p>{{recording.subtitle}}</p>
|
11
|
+
<p>{{recording.start_time | date:'fullDate'}} at {{recording.start_time | date:'HH:mm'}} from channel {{recording.channel_name}}.</p>
|
12
|
+
<p>{{recording.description}}</p>
|
13
|
+
<p>
|
14
|
+
<a target="_self" ng-href="/api/shows/{{show.name}}/recordings/{{recording.episode}}/stream.ts">Raw stream</a>
|
15
|
+
-
|
16
|
+
<a ng-show="recording.has_webm" target="_self" ng-href="/api/shows/{{show.name}}/recordings/{{recording.episode}}/stream.webm">View in browser (Firefox/Chrome)</a>
|
17
|
+
<button ng-hide="recording.has_webm" class="btn" type="button" ng-click="startTranscoding(recording)">Prepare for playing in Firefox/Chrome</button>
|
18
|
+
</p>
|
19
|
+
<p><button class="btn" type="button" ng-click="deleteRecording(recording)">Delete recording</button></p>
|
20
|
+
</div>
|
21
|
+
</div>
|
@@ -0,0 +1,7 @@
|
|
1
|
+
<div class="row" ng-repeat="showChunk in shows | orderBy:'name' | chunk:2">
|
2
|
+
<div class="span6" ng-repeat="show in showChunk">
|
3
|
+
<h2>{{show.name}}</h2>
|
4
|
+
<p><a class="btn btn-primary" ng-href="/shows/{{show.id}}">Episodes</a>
|
5
|
+
<button class="btn" type="button" ng-click="deleteEpisodes(show)">Delete all episodes</button></p>
|
6
|
+
</div>
|
7
|
+
</div>
|
data/simple_pvr.gemspec
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'simple_pvr/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |gem|
|
7
|
+
gem.name = 'simple_pvr'
|
8
|
+
gem.version = SimplePvr::VERSION
|
9
|
+
gem.authors = ["Ole Friis"]
|
10
|
+
gem.email = ["olefriis@gmail.com"]
|
11
|
+
gem.description = 'TV recorder for the HDHomeRun tuners. XMLTV support, nice web GUI for planning your recordings. No playback functionality - use e.g. VLC for that.'
|
12
|
+
gem.summary = 'PVR made simple, not dumb'
|
13
|
+
gem.homepage = 'https://github.com/olefriis/simplepvr'
|
14
|
+
|
15
|
+
gem.files = `git ls-files`.split($/)
|
16
|
+
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
17
|
+
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
18
|
+
gem.require_paths = ["lib"]
|
19
|
+
|
20
|
+
gem.add_dependency 'activesupport', '~> 3.2'
|
21
|
+
gem.add_dependency 'nokogiri', '~> 1.5'
|
22
|
+
gem.add_dependency 'data_mapper', '~> 1.2'
|
23
|
+
gem.add_dependency 'dm-sqlite-adapter', '~> 1.2'
|
24
|
+
gem.add_dependency 'sinatra', '~> 1.3'
|
25
|
+
|
26
|
+
gem.add_development_dependency 'rake', '>= 10.0.0'
|
27
|
+
gem.add_development_dependency 'rspec', '~> 2.11'
|
28
|
+
gem.add_development_dependency 'cucumber', '~> 1.2'
|
29
|
+
gem.add_development_dependency 'capybara', '~> 1.1'
|
30
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
SCANNING: 283000000 (eu-cable:283)
|
2
|
+
LOCK: none (ss=80 snq=0 seq=0)
|
3
|
+
SCANNING: 282000000 (eu-cable:282)
|
4
|
+
LOCK: a8qam256-6900 (ss=79 snq=74 seq=100)
|
5
|
+
TSID: 0x0007
|
6
|
+
PROGRAM 1098: 17 DR K
|
7
|
+
PROGRAM 1165: 110 DR HD
|
8
|
+
SCANNING: 276000000 (eu-cable:276)
|
9
|
+
LOCK: none (ss=81 snq=0 seq=0)
|
10
|
+
SCANNING: 275000000 (eu-cable:275)
|
11
|
+
LOCK: none (ss=80 snq=0 seq=0)
|
@@ -0,0 +1,95 @@
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
2
|
+
<tv generator-info-name="www.ontv.dk/xmltv">
|
3
|
+
<channel id="www.ontv.dk/tv/1">
|
4
|
+
<display-name lang="dk">DR1 DK</display-name>
|
5
|
+
</channel>
|
6
|
+
<channel id="www.ontv.dk/tv/2">
|
7
|
+
<display-name lang="dk">DR2 DK</display-name>
|
8
|
+
</channel>
|
9
|
+
|
10
|
+
<programme channel="www.ontv.dk/tv/1" start="20120717060000 +0200" stop="20120717061000 +0200">
|
11
|
+
<title lang="dk">Noddy</title>
|
12
|
+
<sub-title lang="dk">Bare vær dig selv, Noddy.</sub-title>
|
13
|
+
<desc lang="dk">Tegnefilm.
|
14
|
+
Her kommer Noddy - så kom ud og leg! Den lille dreng af træ har altid travlt med at køre sine venner rundt i Legebyen - og du kan altid høre, når han er på vej!</desc>
|
15
|
+
<category lang="EN">kids</category>
|
16
|
+
<category lang="EN">serie</category>
|
17
|
+
<icon src="http://ontv.dk//imgs/print_img.php?sti=/imgs/epg/channel/2012-07-17/1/11342497600.jpg&height=300&width=300"/>
|
18
|
+
</programme>
|
19
|
+
<programme channel="www.ontv.dk/tv/1" start="20120717061000 +0200" stop="20120717062000 +0200">
|
20
|
+
<title lang="dk">Sprutte-Patruljen</title>
|
21
|
+
<desc lang="dk">Spruttepatruljen og algesuppen.</desc>
|
22
|
+
<category lang="EN">kids</category>
|
23
|
+
<category lang="EN">serie</category>
|
24
|
+
</programme>
|
25
|
+
<programme channel="www.ontv.dk/tv/1" start="20120717062000 +0200" stop="20120717064000 +0200">
|
26
|
+
<title lang="dk">Kære Sebastian</title>
|
27
|
+
<sub-title lang="dk">Geparden skal have kød.</sub-title>
|
28
|
+
<desc lang="dk">Zoo's vildeste dyr. Sebastian Klein overfaldes og skubbes omkuld af en af zoo's vildeste dyr - gederne. Det er mad tid og de er sultne.</desc>
|
29
|
+
<category lang="EN">kids</category>
|
30
|
+
<category lang="EN">serie</category>
|
31
|
+
</programme>
|
32
|
+
<programme channel="www.ontv.dk/tv/1" start="20120717064000 +0200" stop="20120717065000 +0200">
|
33
|
+
<title lang="dk">Rasmus Klump på Nordpolen</title>
|
34
|
+
<desc lang="dk">Vi tager atter med Rasmus Klump og hans venner på eventyr.</desc>
|
35
|
+
<category lang="EN">kids</category>
|
36
|
+
</programme>
|
37
|
+
<programme channel="www.ontv.dk/tv/1" start="20120717065000 +0200" stop="20120717070000 +0200">
|
38
|
+
<title lang="dk">Olivia</title>
|
39
|
+
<sub-title lang="dk">Olivia på scenen.</sub-title>
|
40
|
+
<desc lang="dk">Animation med dansk tale.
|
41
|
+
Slå griseører og øjne ud, for modige Olivia er klar til at tage verden med storm! Den skæve, skøre og sjove gris Olivia er god til alt muligt, bl.a. at gøre folk rigtig trætte. Olivia kan nemlig som ingen anden prøve tøj fra skabet eller male billeder på væggen. Olivia har også startet sit eget band og sågar optrådt i cirkus.</desc>
|
42
|
+
<category lang="EN">kids</category>
|
43
|
+
<category lang="EN">serie</category>
|
44
|
+
<icon src="http://ontv.dk//imgs/print_img.php?sti=/imgs/epg/channel/2012-07-17/1/11342500600.jpg&height=300&width=300"/>
|
45
|
+
</programme>
|
46
|
+
|
47
|
+
<programme channel="www.ontv.dk/tv/2" start="20120717080500 +0200" stop="20120717120000 +0200">
|
48
|
+
<title lang="dk">Morgenandagten på DR2</title>
|
49
|
+
<desc lang="dk">DR2 samsender med P2 Klassisk: Morgenandagten fra Københavns Domkirke. Den direkte radiotransmission af Morgenandagten ledsages af stemningsfulde billeder fra kirken samt kobbertryk af billedkunstneren Robert Jacobsen i anledning af 100-året for hans fødsel. Der er mulighed for at følge med i tekstlæsninger og salmesang, idet andagten tekstes.
|
50
|
+
www.dr.dk/tro</desc>
|
51
|
+
</programme>
|
52
|
+
<programme channel="www.ontv.dk/tv/2" start="20120717120000 +0200" stop="20120717123000 +0200">
|
53
|
+
<title lang="dk">Black Business</title>
|
54
|
+
<desc lang="dk">Hvis du har den rigtige idé, mod og eventyrlyst, kan du gøre store forretninger og tjene mange penge i Afrika. For Afrika er økonomisk set blandt de hurtigste voksende regioner i verden. Det sker lige nu, og det gælder om at komme først. Esben Sørensen er en rødhåret dansker på 30 år, der er vokset op delvist i Afrika og delvist i Danmark. Han har et pladeselskab, hvor han indtil nu kun har udgivet en enkelt plade - med sig selv. Drømmen er at Caravan Records bliver det største pladeselskab i Østafrika, og det er absolut ikke urealistisk, for Østafrika er fyldt med musikalsk talent, markedet er gigantisk og afrikanernes købekraft stiger.
|
55
|
+
Sendt første gang 25.01.11</desc>
|
56
|
+
<category lang="EN">documentary</category>
|
57
|
+
<category lang="EN">serie</category>
|
58
|
+
<episode-num system="xmltv_ns"> .1/4. </episode-num>
|
59
|
+
</programme>
|
60
|
+
<programme channel="www.ontv.dk/tv/2" start="20120717123000 +0200" stop="20120717130000 +0200">
|
61
|
+
<title lang="dk">Solens mad</title>
|
62
|
+
<desc lang="dk">Vi skal med på en rejse til madens og vinens Italien, vi skal på besøg hos nogle af de mange bønder i landet, som ikke går på kompromis med den gode smag og som tilhører den stadigt voksende bevægelse af producenter, der har en stor forståelse for, at miljø, klima og bæredygtighed er hjørnesten i alle produkter.
|
63
|
+
Overgødning og miljøgifte har ødelagt mange af Europas indsøer. I dag skal vi til Trasimeno-søen i Umbrien, den fjerdestørste indsø i Italien. Her lever fiskere og bønder side om side, begge parter meget bevidste om at bevare vandet og jorden her i landets grønne hjerte.
|
64
|
+
Nordvision fra Sverige.</desc>
|
65
|
+
<category lang="EN">entertainment</category>
|
66
|
+
<category lang="EN">serie</category>
|
67
|
+
<episode-num system="xmltv_ns"> .2/6. </episode-num>
|
68
|
+
</programme>
|
69
|
+
<programme channel="www.ontv.dk/tv/2" start="20120717130000 +0200" stop="20120717133000 +0200">
|
70
|
+
<title lang="dk">Øl i Frilandshaven</title>
|
71
|
+
<desc lang="dk">Sommerens bedste bryg bliver lavet over åben ild, og i udekøkkenet står menuen på kalvehaler i mørkt øl og jordbær i balsamico.
|
72
|
+
Sendt første gang 16.08.06</desc>
|
73
|
+
<category lang="EN">entertainment</category>
|
74
|
+
<credits>
|
75
|
+
<actor role="Organizer">Jørgen Skouboe</actor>
|
76
|
+
<actor role="Organizer">Anne Hjernøe</actor>
|
77
|
+
</credits>
|
78
|
+
</programme>
|
79
|
+
<programme channel="www.ontv.dk/tv/2" start="20120717133000 +0200" stop="20120717142500 +0200">
|
80
|
+
<title lang="dk">Intelligence - i hemmelig tjeneste</title>
|
81
|
+
<sub-title lang="dk">Italiensk actionserie fra 2009.</sub-title>
|
82
|
+
<desc lang="dk">Den tidligere jægersoldat Marco Tancredis hustru, Lidia, er blevet skudt. Marco er sikker på, at det var overlagt mord, og han er parat til at gøre hvad som helst for at finde ud af, hvem der står bag. Snart gør han en forbløffende opdagelse - nemlig at Lidia arbejdede for den italienske efterretningstjeneste. Men hvad arbejdede hun på? Og hvorfor skulle hun slås ihjel?</desc>
|
83
|
+
<category lang="EN">serie</category>
|
84
|
+
<episode-num system="xmltv_ns"> .1/12. </episode-num>
|
85
|
+
<icon src="http://ontv.dk//imgs/print_img.php?sti=/imgs/epg/channel/2012-07-17/2/21342524600.jpg&height=300&width=300"/>
|
86
|
+
</programme>
|
87
|
+
<programme channel="www.ontv.dk/tv/2" start="20120717142500 +0200" stop="20120717152000 +0200">
|
88
|
+
<title lang="dk">Intelligence - i hemmelig tjeneste</title>
|
89
|
+
<sub-title lang="dk">Italiensk actionserie fra 2009.</sub-title>
|
90
|
+
<desc lang="dk">Den tidligere jægersoldat Marco Tancredis hustru er blevet myrdet. Marco har opdaget, at hun arbejdede for den italienske efterretningstjeneste, og nu har han selv ladet sig ansætte som efterretningsagent for at finde frem til sandheden om hendes død. Marcos jagt på svar bringer ham til Rumænien - og ansigt til ansigt med en kvinde, der er ude på sit eget private hævntogt.</desc>
|
91
|
+
<category lang="EN">serie</category>
|
92
|
+
<episode-num system="xmltv_ns"> .2/12. </episode-num>
|
93
|
+
<icon src="http://ontv.dk//imgs/print_img.php?sti=/imgs/epg/channel/2012-07-17/2/21342527900.jpg&height=300&width=300"/>
|
94
|
+
</programme>
|
95
|
+
</tv>
|