simple_pvr 1.0.0 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (93) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +1 -0
  3. data/.ruby-version +1 -0
  4. data/.travis.yml +1 -4
  5. data/Gemfile.lock +72 -61
  6. data/README.md +51 -33
  7. data/bin/pvr_server +14 -6
  8. data/changelog.txt +11 -0
  9. data/development_server +14 -6
  10. data/features/channel_overview.feature +1 -1
  11. data/features/recordings.feature +30 -0
  12. data/features/step_definitions/pvr_steps.rb +34 -0
  13. data/features/support/env.rb +1 -2
  14. data/features/support/paths.rb +2 -0
  15. data/lib/simple_pvr.rb +2 -0
  16. data/lib/simple_pvr/model/database_initializer.rb +8 -0
  17. data/lib/simple_pvr/model/programme.rb +12 -2
  18. data/lib/simple_pvr/model/programme_actor.rb +14 -0
  19. data/lib/simple_pvr/model/programme_category.rb +14 -0
  20. data/lib/simple_pvr/model/programme_director.rb +13 -0
  21. data/lib/simple_pvr/model/programme_presenter.rb +13 -0
  22. data/lib/simple_pvr/model/recording.rb +12 -0
  23. data/lib/simple_pvr/programme_icon_fetcher.rb +15 -0
  24. data/lib/simple_pvr/pvr_initializer.rb +4 -4
  25. data/lib/simple_pvr/recorder.rb +4 -3
  26. data/lib/simple_pvr/recording_manager.rb +44 -24
  27. data/lib/simple_pvr/recording_planner.rb +3 -3
  28. data/lib/simple_pvr/scheduler.rb +12 -4
  29. data/lib/simple_pvr/server/base_controller.rb +12 -13
  30. data/lib/simple_pvr/server/channels_controller.rb +5 -5
  31. data/lib/simple_pvr/server/programmes_controller.rb +2 -2
  32. data/lib/simple_pvr/server/schedules_controller.rb +4 -4
  33. data/lib/simple_pvr/server/secured_controller.rb +71 -0
  34. data/lib/simple_pvr/server/shows_controller.rb +13 -8
  35. data/lib/simple_pvr/server/status_controller.rb +1 -1
  36. data/lib/simple_pvr/server/upcoming_recordings_controller.rb +1 -1
  37. data/lib/simple_pvr/version.rb +1 -1
  38. data/lib/simple_pvr/xmltv_reader.rb +37 -4
  39. data/public/css/typeahead.js-bootstrap.css +83 -0
  40. data/public/index.html +45 -37
  41. data/public/js/angular/http-auth-interceptor.js +122 -0
  42. data/public/js/app.js +4 -37
  43. data/public/js/controllers.js +22 -14
  44. data/public/js/directives.js +102 -0
  45. data/public/js/filters.js +0 -31
  46. data/public/js/services.js +64 -0
  47. data/public/js/typeahead/typeahead.min.js +7 -0
  48. data/public/partials/about.html +15 -13
  49. data/public/partials/channels.html +41 -36
  50. data/public/partials/programme.html +20 -4
  51. data/public/partials/programmeListing.html +14 -13
  52. data/public/partials/schedule.html +91 -86
  53. data/public/partials/schedules.html +28 -16
  54. data/public/partials/search.html +10 -11
  55. data/public/partials/show.html +26 -9
  56. data/public/partials/shows.html +5 -6
  57. data/public/partials/status.html +1 -1
  58. data/public/templates/loginDialog.html +30 -0
  59. data/public/templates/logoutLink.html +1 -0
  60. data/public/templates/titleSearch.html +5 -3
  61. data/simple_pvr.gemspec +6 -4
  62. data/spec/resources/dummyImage.png +1 -0
  63. data/spec/resources/programmes-with-categories.xmltv +27 -0
  64. data/spec/resources/programmes-with-credits.xmltv +24 -0
  65. data/spec/resources/programmes-with-icons.xmltv +25 -0
  66. data/spec/resources/programmes-with-presenters.xmltv +19 -0
  67. data/spec/resources/{programs-without-icon.xmltv → programmes-without-icon.xmltv} +0 -0
  68. data/spec/resources/{programs.xmltv → programmes.xmltv} +0 -4
  69. data/spec/simple_pvr/ffmpeg_spec.rb +24 -22
  70. data/spec/simple_pvr/hdhomerun_spec.rb +69 -67
  71. data/spec/simple_pvr/model/channel_spec.rb +101 -101
  72. data/spec/simple_pvr/model/programme_spec.rb +104 -104
  73. data/spec/simple_pvr/model/schedule_spec.rb +74 -74
  74. data/spec/simple_pvr/programme_icon_fetcher_spec.rb +25 -0
  75. data/spec/simple_pvr/pvr_initializer_spec.rb +40 -38
  76. data/spec/simple_pvr/recorder_spec.rb +37 -26
  77. data/spec/simple_pvr/recording_manager_spec.rb +160 -133
  78. data/spec/simple_pvr/recording_planner_spec.rb +213 -211
  79. data/spec/simple_pvr/scheduler_spec.rb +189 -172
  80. data/spec/simple_pvr/server/secured_controller_spec.rb +118 -0
  81. data/spec/simple_pvr/xmltv_reader_spec.rb +89 -41
  82. data/test/karma.conf.js +7 -4
  83. data/test/unit/filtersSpec.js +0 -36
  84. metadata +79 -63
  85. data/public/css/bootstrap-responsive.min.css +0 -9
  86. data/public/css/bootstrap.min.css +0 -9
  87. data/public/img/glyphicons-halflings-white.png +0 -0
  88. data/public/img/glyphicons-halflings.png +0 -0
  89. data/public/js/angular/angular-resource.min.js +0 -10
  90. data/public/js/angular/angular.min.js +0 -162
  91. data/public/js/bootstrap/bootstrap.min.js +0 -6
  92. data/public/js/jquery/jquery.min.js +0 -5
  93. data/test/lib/angular/angular-mocks.js +0 -1768
@@ -1,51 +1,59 @@
1
1
  <!doctype html>
2
2
  <html lang="en" ng-app="simplePvr" ng-cloak>
3
3
  <head>
4
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
5
- <base href="/">
6
- <link href="/app/css/bootstrap.min.css" rel="stylesheet">
7
- <link href="/app/css/simplepvr.css" rel="stylesheet">
8
- <style type="text/css">
9
- body {
10
- padding-top: 60px;
11
- padding-bottom: 40px;
12
- }
4
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
5
+ <base href="/">
6
+ <link href="//netdna.bootstrapcdn.com/bootstrap/3.0.3/css/bootstrap.min.css" rel="stylesheet">
7
+ <link href="/app/css/typeahead.js-bootstrap.css" rel="stylesheet">
8
+ <link href="/app/css/simplepvr.css" rel="stylesheet">
9
+
10
+ <style>
11
+ body { padding-top: 70px; }
13
12
  </style>
14
- <link href="/app/css/bootstrap-responsive.min.css" rel="stylesheet">
15
13
 
16
- <script src="app/js/jquery/jquery.min.js"></script>
17
- <script src="app/js/angular/angular.min.js"></script>
18
- <script src="app/js/angular/angular-resource.min.js"></script>
19
- <script src="app/js/bootstrap/bootstrap.min.js"></script>
14
+ <script src="//ajax.googleapis.com/ajax/libs/jquery/1.10.2/jquery.min.js"></script>
15
+ <script src="//ajax.googleapis.com/ajax/libs/angularjs/1.2.6/angular.min.js"></script>
16
+ <script src="//ajax.googleapis.com/ajax/libs/angularjs/1.2.6/angular-route.min.js"></script>
17
+ <script src="//ajax.googleapis.com/ajax/libs/angularjs/1.2.6/angular-resource.min.js"></script>
18
+ <script src="//ajax.googleapis.com/ajax/libs/angularjs/1.2.6/angular-cookies.min.js"></script>
19
+ <script src="//netdna.bootstrapcdn.com/bootstrap/3.0.3/js/bootstrap.min.js"></script>
20
+ <script src="app/js/angular/http-auth-interceptor.js"></script>
21
+ <script src="app/js/typeahead/typeahead.min.js"></script>
20
22
 
21
- <script src="app/js/controllers.js"></script>
22
- <script src="app/js/services.js"></script>
23
- <script src="app/js/filters.js"></script>
24
- <script src="app/js/app.js"></script>
23
+ <script src="app/js/controllers.js"></script>
24
+ <script src="app/js/services.js"></script>
25
+ <script src="app/js/filters.js"></script>
26
+ <script src="app/js/directives.js"></script>
27
+ <script src="app/js/app.js"></script>
25
28
  </head>
26
29
 
27
30
  <body>
28
- <div class="navbar navbar-fixed-top">
29
- <div class="navbar-inner">
30
- <div class="container">
31
- <button type="button" class="btn btn-navbar" data-toggle="collapse" data-target=".nav-collapse">
32
- <span class="icon-bar"></span>
33
- <span class="icon-bar"></span>
34
- <span class="icon-bar"></span>
35
- </button>
36
- <a class="brand" href="about">SimplePVR</a>
37
- <div class="nav-collapse collapse">
38
- <ul class="nav">
39
- <navbar-item route="/schedules">Schedules</navbar-item>
40
- <navbar-item route="/channels">Channels</navbar-item>
41
- <navbar-item route="/shows">Recordings</navbar-item>
42
- <navbar-item route="/status">Status</navbar-item>
43
- </ul>
44
- <title-search></title-search>
45
- </div>
46
- </div>
31
+ <login-dialog></login-dialog>
32
+
33
+ <nav class="navbar navbar-default navbar-fixed-top" role="navigation">
34
+ <div class="container">
35
+ <div class="navbar-header">
36
+ <button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-ex1-collapse">
37
+ <span class="sr-only">Toggle navigation</span>
38
+ <span class="icon-bar"></span>
39
+ <span class="icon-bar"></span>
40
+ <span class="icon-bar"></span>
41
+ </button>
42
+ <a class="navbar-brand" href="about">SimplePVR</a>
43
+ </div>
44
+
45
+ <div class="collapse navbar-collapse navbar-ex1-collapse">
46
+ <ul class="nav navbar-nav">
47
+ <navbar-item route="/schedules">Schedules</navbar-item>
48
+ <navbar-item route="/channels">Channels</navbar-item>
49
+ <navbar-item route="/shows">Recordings</navbar-item>
50
+ <navbar-item route="/status">Status</navbar-item>
51
+ </ul>
52
+ <title-search></title-search>
53
+ <logout-link></logout-link>
47
54
  </div>
48
55
  </div>
56
+ </nav>
49
57
 
50
58
  <div class="container" id="contents" ng-view>
51
59
  </div>
@@ -0,0 +1,122 @@
1
+ /*global angular:true, browser:true */
2
+
3
+ /**
4
+ * @license HTTP Auth Interceptor Module for AngularJS
5
+ * (c) 2012 Witold Szczerba
6
+ * License: MIT
7
+ */
8
+ (function () {
9
+ 'use strict';
10
+
11
+ angular.module('http-auth-interceptor', ['http-auth-interceptor-buffer'])
12
+
13
+ .factory('authService', ['$rootScope','httpBuffer', function($rootScope, httpBuffer) {
14
+ return {
15
+ /**
16
+ * Call this function to indicate that authentication was successfull and trigger a
17
+ * retry of all deferred requests.
18
+ * @param data an optional argument to pass on to $broadcast which may be useful for
19
+ * example if you need to pass through details of the user that was logged in
20
+ */
21
+ loginConfirmed: function(data, configUpdater) {
22
+ var updater = configUpdater || function(config) {return config;};
23
+ $rootScope.$broadcast('event:auth-loginConfirmed', data);
24
+ httpBuffer.retryAll(updater);
25
+ },
26
+
27
+ /**
28
+ * Call this function to indicate that authentication should not proceed.
29
+ * All deferred requests will be abandoned or rejected (if reason is provided).
30
+ * @param data an optional argument to pass on to $broadcast.
31
+ * @param reason if provided, the requests are rejected; abandoned otherwise.
32
+ */
33
+ loginCancelled: function(data, reason) {
34
+ httpBuffer.rejectAll(reason);
35
+ $rootScope.$broadcast('event:auth-loginCancelled', data);
36
+ }
37
+ };
38
+ }])
39
+
40
+ /**
41
+ * $http interceptor.
42
+ * On 401 response (without 'ignoreAuthModule' option) stores the request
43
+ * and broadcasts 'event:angular-auth-loginRequired'.
44
+ */
45
+ .config(['$httpProvider', function($httpProvider) {
46
+ $httpProvider.interceptors.push(['$rootScope', '$q', 'httpBuffer', function($rootScope, $q, httpBuffer) {
47
+ return {
48
+ responseError: function(rejection) {
49
+ if (rejection.status === 401 && !rejection.config.ignoreAuthModule) {
50
+ var deferred = $q.defer();
51
+ httpBuffer.append(rejection.config, deferred);
52
+ $rootScope.$broadcast('event:auth-loginRequired', rejection);
53
+ return deferred.promise;
54
+ }
55
+ // otherwise, default behaviour
56
+ return $q.reject(rejection);
57
+ }
58
+ };
59
+ }]);
60
+ }]);
61
+
62
+ /**
63
+ * Private module, a utility, required internally by 'http-auth-interceptor'.
64
+ */
65
+ angular.module('http-auth-interceptor-buffer', [])
66
+
67
+ .factory('httpBuffer', ['$injector', function($injector) {
68
+ /** Holds all the requests, so they can be re-requested in future. */
69
+ var buffer = [];
70
+
71
+ /** Service initialized later because of circular dependency problem. */
72
+ var $http;
73
+
74
+ function retryHttpRequest(config, deferred) {
75
+ // Make room for new 'Authenticate' header value
76
+ delete config.headers['Authorization'];
77
+
78
+ function successCallback(response) {
79
+ deferred.resolve(response);
80
+ }
81
+ function errorCallback(response) {
82
+ deferred.reject(response);
83
+ }
84
+ $http = $http || $injector.get('$http');
85
+ $http(config).then(successCallback, errorCallback);
86
+ }
87
+
88
+ return {
89
+ /**
90
+ * Appends HTTP request configuration object with deferred response attached to buffer.
91
+ */
92
+ append: function(config, deferred) {
93
+ buffer.push({
94
+ config: config,
95
+ deferred: deferred
96
+ });
97
+ },
98
+
99
+ /**
100
+ * Abandon or reject (if reason provided) all the buffered requests.
101
+ */
102
+ rejectAll: function(reason) {
103
+ if (reason) {
104
+ for (var i = 0; i < buffer.length; ++i) {
105
+ buffer[i].deferred.reject(reason);
106
+ }
107
+ }
108
+ buffer = [];
109
+ },
110
+
111
+ /**
112
+ * Retries all the buffered requests clears the buffer.
113
+ */
114
+ retryAll: function(updater) {
115
+ for (var i = 0; i < buffer.length; ++i) {
116
+ retryHttpRequest(updater(buffer[i].config), buffer[i].deferred);
117
+ }
118
+ buffer = [];
119
+ }
120
+ };
121
+ }]);
122
+ })();
@@ -1,42 +1,9 @@
1
1
  'use strict';
2
2
 
3
- angular.module('simplePvr', ['simplePvrServices', 'simplePvrFilters']).
4
- directive('titleSearch', function() {
5
- return {
6
- templateUrl: '/app/templates/titleSearch.html',
7
- restrict: 'E',
8
- replace: true,
9
- controller: SearchProgrammesCtrl,
10
- link: function(scope, element, attributes, controller) {
11
- element.find('input').typeahead({
12
- source: scope.autocomplete,
13
- updater: scope.updater
14
- });
15
- }
16
- };
17
- }).
18
- directive('navbarItem', function($location) {
19
- return {
20
- template: '<li><a ng-href="{{route}}" ng-transclude></a></li>',
21
- restrict: 'E',
22
- transclude: true,
23
- replace: true,
24
- scope: { route:'@route' },
25
- link: function(scope, element, attributes, controller) {
26
- scope.$on('$routeChangeSuccess', function() {
27
- var path = $location.path();
28
- var isSamePath = path == scope.route;
29
- var isSubpath = path.indexOf(scope.route + '/') == 0;
30
- if (isSamePath || isSubpath) {
31
- element.addClass('active');
32
- } else {
33
- element.removeClass('active');
34
- }
35
- });
36
- }
37
- };
38
- }).
39
- config(function($routeProvider, $locationProvider) {
3
+ angular.module('simplePvr', ['ngRoute', 'ngCookies', 'simplePvrServices', 'simplePvrFilters', 'simplePvrDirectives', 'http-auth-interceptor']).
4
+ config(function($routeProvider, $locationProvider, $httpProvider) {
5
+ $httpProvider.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';
6
+
40
7
  $locationProvider.html5Mode(true).hashPrefix('');
41
8
 
42
9
  $routeProvider.
@@ -1,5 +1,24 @@
1
1
  'use strict';
2
2
 
3
+ function CredentialsController($scope, loginService) {
4
+ $scope.credentials = { userName: '', password: '' };
5
+
6
+ $scope.submit = function() {
7
+ loginService.setUserNameAndPassword($scope.credentials.userName, $scope.credentials.password);
8
+ }
9
+ }
10
+
11
+ function LoginController($scope, $location, loginService) {
12
+ $scope.isLoggedIn = function() {
13
+ return loginService.isLoggedIn();
14
+ }
15
+
16
+ $scope.logOut = function() {
17
+ loginService.logOut();
18
+ $location.path('/about');
19
+ }
20
+ }
21
+
3
22
  function SchedulesCtrl($scope, $http, Schedule, UpcomingRecording, Channel) {
4
23
  var updateView = function() {
5
24
  $scope.schedules = Schedule.query();
@@ -50,7 +69,7 @@ function ChannelsCtrl($scope, $http, Channel) {
50
69
  if (programme == null) {
51
70
  return '';
52
71
  }
53
- return programme.is_conflicting ? 'error' : (programme.is_scheduled ? 'success' : '');
72
+ return programme.is_conflicting ? 'danger' : (programme.is_scheduled ? 'success' : '');
54
73
  }
55
74
  $scope.hideChannel = function(channel) {
56
75
  // I wish Angular could let me define this operation on the Channel object
@@ -71,7 +90,7 @@ function ProgrammeListingCtrl($scope, $routeParams, ProgrammeListing) {
71
90
  $scope.programmeListing = ProgrammeListing.get({channelId: $scope.channelId, date: $scope.date});
72
91
 
73
92
  $scope.classForProgrammeLine = function(programme) {
74
- return programme.is_conflicting ? 'error' : (programme.is_scheduled ? 'success' : '');
93
+ return programme.is_conflicting ? 'danger' : (programme.is_scheduled ? 'success' : '');
75
94
  }
76
95
  }
77
96
 
@@ -119,7 +138,7 @@ function ShowCtrl($scope, $routeParams, $http, Show, Recording) {
119
138
  }
120
139
 
121
140
  $scope.deleteRecording = function(recording) {
122
- if (confirm("Really delete recording\n" + recording.episode + "\nof show\n" + $scope.show.name + "\n?")) {
141
+ if (confirm("Really delete this recording of show\n" + $scope.show.name + "\n?")) {
123
142
  recording.$delete(loadRecordings);
124
143
  }
125
144
  }
@@ -134,17 +153,6 @@ function ShowCtrl($scope, $routeParams, $http, Show, Recording) {
134
153
  }
135
154
 
136
155
  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.updater = function(item) {
142
- $scope.$apply(function() {
143
- $scope.title = item;
144
- });
145
- return item;
146
- }
147
-
148
156
  $scope.search = function() {
149
157
  $location.path('/search').search({query: $scope.title});
150
158
  }
@@ -0,0 +1,102 @@
1
+ 'use strict';
2
+
3
+ angular.module('simplePvrDirectives', []).
4
+ directive('loginDialog', function($timeout) {
5
+ return {
6
+ templateUrl: '/app/templates/loginDialog.html',
7
+ restrict: 'E',
8
+ replace: true,
9
+ controller: CredentialsController,
10
+ link: function(scope, element, attributes, controller) {
11
+ var isShowing = false;
12
+
13
+ element.on('shown.bs.modal', function(e) {
14
+ element.find('#userName').focus();
15
+ });
16
+
17
+ scope.$on('event:auth-loginRequired', function() {
18
+ if (isShowing) {
19
+ return;
20
+ }
21
+
22
+ // If we're in the process of hiding the modal, we need to wait for
23
+ // all CSS animations to complete before showing the modal again.
24
+ // Otherwise, we might end up with an invisible modal, making the whole
25
+ // view rather unusable. I've been unable to control the transitions
26
+ // between "showing", "shown", "hiding", and "hidden" tightly using
27
+ // JQuery notifications without collecting more and more modal backdrops
28
+ // in the DOM, so the dirty solution here is to simply wait a second
29
+ // before showing the log-in dialog.
30
+ isShowing = true;
31
+ $timeout(function() {
32
+ element.modal('show');
33
+ isShowing = false;
34
+ }, 1000);
35
+ });
36
+
37
+ scope.$on('event:auth-loginConfirmed', function() {
38
+ element.modal('hide');
39
+ scope.credentials.password = '';
40
+ });
41
+ }
42
+ }
43
+ }).
44
+ directive('logoutLink', function() {
45
+ return {
46
+ templateUrl: '/app/templates/logoutLink.html',
47
+ restrict: 'E',
48
+ replace: true,
49
+ controller: LoginController
50
+ }
51
+ }).
52
+ directive('titleSearch', function(loginService) {
53
+ return {
54
+ templateUrl: '/app/templates/titleSearch.html',
55
+ restrict: 'E',
56
+ replace: true,
57
+ controller: SearchProgrammesCtrl,
58
+ link: function(scope, element, attributes, controller) {
59
+ var inputField = element.find('input');
60
+ inputField.typeahead({
61
+ remote: '/api/programmes/title_search?query=%QUERY'
62
+ });
63
+
64
+ var updateTitle = function() {
65
+ scope.$apply(function() {
66
+ scope.title = inputField.val();
67
+ });
68
+ };
69
+ var updateTitleAndPerformSearch = function() {
70
+ scope.$apply(function() {
71
+ scope.title = inputField.val();
72
+ scope.search();
73
+ })
74
+ }
75
+
76
+ inputField.change(updateTitle);
77
+ inputField.on('typeahead:autocompleted', updateTitle);
78
+ inputField.on('typeahead:selected', updateTitleAndPerformSearch);
79
+ }
80
+ };
81
+ }).
82
+ directive('navbarItem', function($location) {
83
+ return {
84
+ template: '<li><a ng-href="{{route}}" ng-transclude></a></li>',
85
+ restrict: 'E',
86
+ transclude: true,
87
+ replace: true,
88
+ scope: { route:'@route' },
89
+ link: function(scope, element, attributes, controller) {
90
+ scope.$on('$routeChangeSuccess', function() {
91
+ var path = $location.path();
92
+ var isSamePath = path == scope.route;
93
+ var isSubpath = path.indexOf(scope.route + '/') == 0;
94
+ if (isSamePath || isSubpath) {
95
+ element.addClass('active');
96
+ } else {
97
+ element.removeClass('active');
98
+ }
99
+ });
100
+ }
101
+ };
102
+ });
@@ -1,37 +1,6 @@
1
1
  'use strict';
2
2
 
3
3
  angular.module('simplePvrFilters', []).
4
- filter('chunk', function() {
5
- function chunkArray(array, chunkSize) {
6
- var result = [];
7
- var currentChunk = [];
8
- for (var i=0; i<array.length; i++) {
9
- currentChunk.push(array[i]);
10
- if (currentChunk.length == chunkSize) {
11
- result.push(currentChunk);
12
- currentChunk = [];
13
- }
14
- }
15
- if (currentChunk.length > 0) {
16
- result.push(currentChunk);
17
- }
18
- return result;
19
- }
20
-
21
- function defineHashKeys(array) {
22
- for (var i=0; i<array.length; i++) {
23
- array[i].$$hashKey = i;
24
- }
25
- }
26
-
27
- return function(array, chunkSize) {
28
- if (!(array instanceof Array)) return array;
29
- if (!chunkSize) return array;
30
- var result = chunkArray(array, chunkSize);
31
- defineHashKeys(result);
32
- return result;
33
- }
34
- }).
35
4
  filter('formatEpisode', function() {
36
5
  return function(episodeNum) {
37
6
  return episodeNum ? episodeNum.replace(' .', '').replace('. ', '') : '';