tablo_connect 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (239) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +22 -0
  3. data/README.md +43 -0
  4. data/Rakefile +26 -0
  5. data/app/assets/fonts/tablo_connect/bootstrap/glyphicons-halflings-regular.eot +0 -0
  6. data/app/assets/fonts/tablo_connect/bootstrap/glyphicons-halflings-regular.svg +288 -0
  7. data/app/assets/fonts/tablo_connect/bootstrap/glyphicons-halflings-regular.ttf +0 -0
  8. data/app/assets/fonts/tablo_connect/bootstrap/glyphicons-halflings-regular.woff +0 -0
  9. data/app/assets/fonts/tablo_connect/bootstrap/glyphicons-halflings-regular.woff2 +0 -0
  10. data/app/assets/javascripts/tablo_connect/angular/angular-animate.js +3928 -0
  11. data/app/assets/javascripts/tablo_connect/angular/angular-animate.min.js +56 -0
  12. data/app/assets/javascripts/tablo_connect/angular/angular-animate.min.js.map +8 -0
  13. data/app/assets/javascripts/tablo_connect/angular/angular-aria.js +397 -0
  14. data/app/assets/javascripts/tablo_connect/angular/angular-aria.min.js +14 -0
  15. data/app/assets/javascripts/tablo_connect/angular/angular-aria.min.js.map +8 -0
  16. data/app/assets/javascripts/tablo_connect/angular/angular-cookies.js +321 -0
  17. data/app/assets/javascripts/tablo_connect/angular/angular-cookies.min.js +9 -0
  18. data/app/assets/javascripts/tablo_connect/angular/angular-cookies.min.js.map +8 -0
  19. data/app/assets/javascripts/tablo_connect/angular/angular-csp.css +21 -0
  20. data/app/assets/javascripts/tablo_connect/angular/angular-loader.js +443 -0
  21. data/app/assets/javascripts/tablo_connect/angular/angular-loader.min.js +10 -0
  22. data/app/assets/javascripts/tablo_connect/angular/angular-loader.min.js.map +8 -0
  23. data/app/assets/javascripts/tablo_connect/angular/angular-message-format.js +980 -0
  24. data/app/assets/javascripts/tablo_connect/angular/angular-message-format.min.js +26 -0
  25. data/app/assets/javascripts/tablo_connect/angular/angular-message-format.min.js.map +8 -0
  26. data/app/assets/javascripts/tablo_connect/angular/angular-messages.js +685 -0
  27. data/app/assets/javascripts/tablo_connect/angular/angular-messages.min.js +12 -0
  28. data/app/assets/javascripts/tablo_connect/angular/angular-messages.min.js.map +8 -0
  29. data/app/assets/javascripts/tablo_connect/angular/angular-mocks.js +2470 -0
  30. data/app/assets/javascripts/tablo_connect/angular/angular-resource.js +675 -0
  31. data/app/assets/javascripts/tablo_connect/angular/angular-resource.min.js +13 -0
  32. data/app/assets/javascripts/tablo_connect/angular/angular-resource.min.js.map +8 -0
  33. data/app/assets/javascripts/tablo_connect/angular/angular-route.js +991 -0
  34. data/app/assets/javascripts/tablo_connect/angular/angular-route.min.js +15 -0
  35. data/app/assets/javascripts/tablo_connect/angular/angular-route.min.js.map +8 -0
  36. data/app/assets/javascripts/tablo_connect/angular/angular-sanitize.js +683 -0
  37. data/app/assets/javascripts/tablo_connect/angular/angular-sanitize.min.js +16 -0
  38. data/app/assets/javascripts/tablo_connect/angular/angular-sanitize.min.js.map +8 -0
  39. data/app/assets/javascripts/tablo_connect/angular/angular-scenario.js +40324 -0
  40. data/app/assets/javascripts/tablo_connect/angular/angular-touch.js +628 -0
  41. data/app/assets/javascripts/tablo_connect/angular/angular-touch.min.js +13 -0
  42. data/app/assets/javascripts/tablo_connect/angular/angular-touch.min.js.map +8 -0
  43. data/app/assets/javascripts/tablo_connect/angular/angular-ui-router.js +4370 -0
  44. data/app/assets/javascripts/tablo_connect/angular/angular.js +28904 -0
  45. data/app/assets/javascripts/tablo_connect/angular/angular.min.js +294 -0
  46. data/app/assets/javascripts/tablo_connect/angular/angular.min.js.map +8 -0
  47. data/app/assets/javascripts/tablo_connect/angular/errors.json +1 -0
  48. data/app/assets/javascripts/tablo_connect/angular/version.json +1 -0
  49. data/app/assets/javascripts/tablo_connect/angular/version.txt +1 -0
  50. data/app/assets/javascripts/tablo_connect/application.js +21 -0
  51. data/app/assets/javascripts/tablo_connect/bootstrap/affix.js +162 -0
  52. data/app/assets/javascripts/tablo_connect/bootstrap/alert.js +94 -0
  53. data/app/assets/javascripts/tablo_connect/bootstrap/button.js +120 -0
  54. data/app/assets/javascripts/tablo_connect/bootstrap/carousel.js +237 -0
  55. data/app/assets/javascripts/tablo_connect/bootstrap/collapse.js +211 -0
  56. data/app/assets/javascripts/tablo_connect/bootstrap/dropdown.js +165 -0
  57. data/app/assets/javascripts/tablo_connect/bootstrap/modal.js +337 -0
  58. data/app/assets/javascripts/tablo_connect/bootstrap/popover.js +108 -0
  59. data/app/assets/javascripts/tablo_connect/bootstrap/scrollspy.js +172 -0
  60. data/app/assets/javascripts/tablo_connect/bootstrap/tab.js +155 -0
  61. data/app/assets/javascripts/tablo_connect/bootstrap/tooltip.js +514 -0
  62. data/app/assets/javascripts/tablo_connect/bootstrap/transition.js +59 -0
  63. data/app/assets/javascripts/tablo_connect/jquery/jquery.js +9210 -0
  64. data/app/assets/javascripts/tablo_connect/ng_app/app.js +39 -0
  65. data/app/assets/javascripts/tablo_connect/ng_app/controllers/episodes_controller.js +17 -0
  66. data/app/assets/javascripts/tablo_connect/ng_app/controllers/home_controller.js +9 -0
  67. data/app/assets/javascripts/tablo_connect/ng_app/controllers/movies_controller.js +17 -0
  68. data/app/assets/javascripts/tablo_connect/ng_app/controllers/shows_controller.js +9 -0
  69. data/app/assets/javascripts/tablo_connect/ng_app/filters/titleize_filter.js +17 -0
  70. data/app/assets/javascripts/tablo_connect/ng_app/filters/url_filter.js +19 -0
  71. data/app/assets/javascripts/tablo_connect/ng_app/modules/alerts/alerts.js +9 -0
  72. data/app/assets/javascripts/tablo_connect/ng_app/modules/alerts/directives/alerts_directive.js +19 -0
  73. data/app/assets/javascripts/tablo_connect/ng_app/modules/alerts/services/alerts_service.js +22 -0
  74. data/app/assets/javascripts/tablo_connect/ng_app/modules/alerts/templates/alerts.html +6 -0
  75. data/app/assets/javascripts/tablo_connect/ng_app/modules/tablo_sync/directives/tablo_sync_directive.js +31 -0
  76. data/app/assets/javascripts/tablo_connect/ng_app/modules/tablo_sync/services/tablo_sync_service.js +20 -0
  77. data/app/assets/javascripts/tablo_connect/ng_app/modules/tablo_sync/tablo_sync.js +9 -0
  78. data/app/assets/javascripts/tablo_connect/ng_app/modules/tablo_sync/templates/tablo_sync.html +4 -0
  79. data/app/assets/javascripts/tablo_connect/ng_app/services/tablo_service.js +98 -0
  80. data/app/assets/javascripts/tablo_connect/ng_app/templates/episodes.html +36 -0
  81. data/app/assets/javascripts/tablo_connect/ng_app/templates/index.html +3 -0
  82. data/app/assets/javascripts/tablo_connect/ng_app/templates/movies.html +33 -0
  83. data/app/assets/javascripts/tablo_connect/ng_app/templates/shows.html +13 -0
  84. data/app/assets/stylesheets/tablo_connect/_bootstrap.scss +56 -0
  85. data/app/assets/stylesheets/tablo_connect/_global.scss +19 -0
  86. data/app/assets/stylesheets/tablo_connect/_movies.scss +0 -0
  87. data/app/assets/stylesheets/tablo_connect/_shows.scss +30 -0
  88. data/app/assets/stylesheets/tablo_connect/application.scss +18 -0
  89. data/app/assets/stylesheets/tablo_connect/bootstrap/_alerts.scss +73 -0
  90. data/app/assets/stylesheets/tablo_connect/bootstrap/_badges.scss +68 -0
  91. data/app/assets/stylesheets/tablo_connect/bootstrap/_breadcrumbs.scss +26 -0
  92. data/app/assets/stylesheets/tablo_connect/bootstrap/_button-groups.scss +244 -0
  93. data/app/assets/stylesheets/tablo_connect/bootstrap/_buttons.scss +168 -0
  94. data/app/assets/stylesheets/tablo_connect/bootstrap/_carousel.scss +269 -0
  95. data/app/assets/stylesheets/tablo_connect/bootstrap/_close.scss +36 -0
  96. data/app/assets/stylesheets/tablo_connect/bootstrap/_code.scss +69 -0
  97. data/app/assets/stylesheets/tablo_connect/bootstrap/_component-animations.scss +37 -0
  98. data/app/assets/stylesheets/tablo_connect/bootstrap/_dropdowns.scss +216 -0
  99. data/app/assets/stylesheets/tablo_connect/bootstrap/_forms.scss +611 -0
  100. data/app/assets/stylesheets/tablo_connect/bootstrap/_glyphicons.scss +307 -0
  101. data/app/assets/stylesheets/tablo_connect/bootstrap/_grid.scss +84 -0
  102. data/app/assets/stylesheets/tablo_connect/bootstrap/_input-groups.scss +167 -0
  103. data/app/assets/stylesheets/tablo_connect/bootstrap/_jumbotron.scss +52 -0
  104. data/app/assets/stylesheets/tablo_connect/bootstrap/_labels.scss +66 -0
  105. data/app/assets/stylesheets/tablo_connect/bootstrap/_list-group.scss +130 -0
  106. data/app/assets/stylesheets/tablo_connect/bootstrap/_media.scss +66 -0
  107. data/app/assets/stylesheets/tablo_connect/bootstrap/_mixins.scss +40 -0
  108. data/app/assets/stylesheets/tablo_connect/bootstrap/_modals.scss +150 -0
  109. data/app/assets/stylesheets/tablo_connect/bootstrap/_navbar.scss +662 -0
  110. data/app/assets/stylesheets/tablo_connect/bootstrap/_navs.scss +242 -0
  111. data/app/assets/stylesheets/tablo_connect/bootstrap/_normalize.scss +424 -0
  112. data/app/assets/stylesheets/tablo_connect/bootstrap/_pager.scss +54 -0
  113. data/app/assets/stylesheets/tablo_connect/bootstrap/_pagination.scss +89 -0
  114. data/app/assets/stylesheets/tablo_connect/bootstrap/_panels.scss +271 -0
  115. data/app/assets/stylesheets/tablo_connect/bootstrap/_popovers.scss +131 -0
  116. data/app/assets/stylesheets/tablo_connect/bootstrap/_print.scss +101 -0
  117. data/app/assets/stylesheets/tablo_connect/bootstrap/_progress-bars.scss +87 -0
  118. data/app/assets/stylesheets/tablo_connect/bootstrap/_responsive-embed.scss +35 -0
  119. data/app/assets/stylesheets/tablo_connect/bootstrap/_responsive-utilities.scss +179 -0
  120. data/app/assets/stylesheets/tablo_connect/bootstrap/_scaffolding.scss +161 -0
  121. data/app/assets/stylesheets/tablo_connect/bootstrap/_tables.scss +234 -0
  122. data/app/assets/stylesheets/tablo_connect/bootstrap/_theme.scss +291 -0
  123. data/app/assets/stylesheets/tablo_connect/bootstrap/_thumbnails.scss +38 -0
  124. data/app/assets/stylesheets/tablo_connect/bootstrap/_tooltip.scss +101 -0
  125. data/app/assets/stylesheets/tablo_connect/bootstrap/_type.scss +298 -0
  126. data/app/assets/stylesheets/tablo_connect/bootstrap/_utilities.scss +55 -0
  127. data/app/assets/stylesheets/tablo_connect/bootstrap/_variables.scss +872 -0
  128. data/app/assets/stylesheets/tablo_connect/bootstrap/_wells.scss +29 -0
  129. data/app/assets/stylesheets/tablo_connect/bootstrap/mixins/_alerts.scss +14 -0
  130. data/app/assets/stylesheets/tablo_connect/bootstrap/mixins/_background-variant.scss +12 -0
  131. data/app/assets/stylesheets/tablo_connect/bootstrap/mixins/_border-radius.scss +18 -0
  132. data/app/assets/stylesheets/tablo_connect/bootstrap/mixins/_buttons.scss +68 -0
  133. data/app/assets/stylesheets/tablo_connect/bootstrap/mixins/_center-block.scss +7 -0
  134. data/app/assets/stylesheets/tablo_connect/bootstrap/mixins/_clearfix.scss +22 -0
  135. data/app/assets/stylesheets/tablo_connect/bootstrap/mixins/_forms.scss +88 -0
  136. data/app/assets/stylesheets/tablo_connect/bootstrap/mixins/_gradients.scss +58 -0
  137. data/app/assets/stylesheets/tablo_connect/bootstrap/mixins/_grid-framework.scss +81 -0
  138. data/app/assets/stylesheets/tablo_connect/bootstrap/mixins/_grid.scss +122 -0
  139. data/app/assets/stylesheets/tablo_connect/bootstrap/mixins/_hide-text.scss +21 -0
  140. data/app/assets/stylesheets/tablo_connect/bootstrap/mixins/_image.scss +33 -0
  141. data/app/assets/stylesheets/tablo_connect/bootstrap/mixins/_labels.scss +12 -0
  142. data/app/assets/stylesheets/tablo_connect/bootstrap/mixins/_list-group.scss +32 -0
  143. data/app/assets/stylesheets/tablo_connect/bootstrap/mixins/_nav-divider.scss +10 -0
  144. data/app/assets/stylesheets/tablo_connect/bootstrap/mixins/_nav-vertical-align.scss +9 -0
  145. data/app/assets/stylesheets/tablo_connect/bootstrap/mixins/_opacity.scss +8 -0
  146. data/app/assets/stylesheets/tablo_connect/bootstrap/mixins/_pagination.scss +24 -0
  147. data/app/assets/stylesheets/tablo_connect/bootstrap/mixins/_panels.scss +24 -0
  148. data/app/assets/stylesheets/tablo_connect/bootstrap/mixins/_progress-bar.scss +10 -0
  149. data/app/assets/stylesheets/tablo_connect/bootstrap/mixins/_reset-filter.scss +8 -0
  150. data/app/assets/stylesheets/tablo_connect/bootstrap/mixins/_reset-text.scss +18 -0
  151. data/app/assets/stylesheets/tablo_connect/bootstrap/mixins/_resize.scss +6 -0
  152. data/app/assets/stylesheets/tablo_connect/bootstrap/mixins/_responsive-visibility.scss +21 -0
  153. data/app/assets/stylesheets/tablo_connect/bootstrap/mixins/_size.scss +10 -0
  154. data/app/assets/stylesheets/tablo_connect/bootstrap/mixins/_tab-focus.scss +9 -0
  155. data/app/assets/stylesheets/tablo_connect/bootstrap/mixins/_table-row.scss +28 -0
  156. data/app/assets/stylesheets/tablo_connect/bootstrap/mixins/_text-emphasis.scss +12 -0
  157. data/app/assets/stylesheets/tablo_connect/bootstrap/mixins/_text-overflow.scss +8 -0
  158. data/app/assets/stylesheets/tablo_connect/bootstrap/mixins/_vendor-prefixes.scss +222 -0
  159. data/app/controllers/tablo_connect/application_controller.rb +4 -0
  160. data/app/controllers/tablo_connect/copy_controller.rb +75 -0
  161. data/app/controllers/tablo_connect/home_controller.rb +6 -0
  162. data/app/controllers/tablo_connect/movies_controller.rb +9 -0
  163. data/app/controllers/tablo_connect/shows_controller.rb +11 -0
  164. data/app/controllers/tablo_connect/sync_controller.rb +73 -0
  165. data/app/helpers/tablo_connect/application_helper.rb +4 -0
  166. data/app/helpers/tablo_connect/movies_helper.rb +4 -0
  167. data/app/helpers/tablo_connect/shows_helper.rb +4 -0
  168. data/app/models/tablo_connect/movie.rb +14 -0
  169. data/app/models/tablo_connect/show.rb +23 -0
  170. data/app/views/layouts/tablo_connect/_header.html.erb +20 -0
  171. data/app/views/layouts/tablo_connect/application.html.erb +19 -0
  172. data/app/views/tablo_connect/home/index.html.erb +1 -0
  173. data/config/routes.rb +10 -0
  174. data/db/migrate/20151101164611_create_tablo_connect_movies.rb +18 -0
  175. data/db/migrate/20151101165020_create_tablo_connect_shows.rb +21 -0
  176. data/lib/tablo_connect.rb +7 -0
  177. data/lib/tablo_connect/engine.rb +28 -0
  178. data/lib/tablo_connect/version.rb +3 -0
  179. data/lib/tasks/tablo_connect_tasks.rake +4 -0
  180. data/spec/controllers/tablo_connect/copy_controller_spec.rb +244 -0
  181. data/spec/controllers/tablo_connect/movies_controller_spec.rb +27 -0
  182. data/spec/controllers/tablo_connect/shows_controller_spec.rb +45 -0
  183. data/spec/controllers/tablo_connect/sync_controller_spec.rb +209 -0
  184. data/spec/factories/tablo_connect_movies.rb +11 -0
  185. data/spec/factories/tablo_connect_shows.rb +15 -0
  186. data/spec/javascript/angular-environment.js +1 -0
  187. data/spec/javascript/karma.conf.js +48 -0
  188. data/spec/javascript/unit/controllers/episodes_controller_spec.js +49 -0
  189. data/spec/javascript/unit/controllers/home_controller_spec.js +34 -0
  190. data/spec/javascript/unit/controllers/movies_controller_spec.js +55 -0
  191. data/spec/javascript/unit/controllers/shows_controller_spec.js +33 -0
  192. data/spec/javascript/unit/filters/titleize_filter_spec.js +25 -0
  193. data/spec/javascript/unit/filters/url_filter_spec.js +33 -0
  194. data/spec/javascript/unit/modules/alerts/directives/alerts_directive_spec.js +39 -0
  195. data/spec/javascript/unit/modules/alerts/services/alerts_service_spec.js +51 -0
  196. data/spec/javascript/unit/modules/tablo_sync/directives/tablo_sync_directive_spec.js +68 -0
  197. data/spec/javascript/unit/modules/tablo_sync/services/tablo_sync_service_spec.js +26 -0
  198. data/spec/javascript/unit/services/tablo_service_spec.js +28 -0
  199. data/spec/models/tablo_connect/movie_spec.rb +32 -0
  200. data/spec/models/tablo_connect/show_spec.rb +65 -0
  201. data/spec/spec_helper.rb +86 -0
  202. data/spec/test_app/README.rdoc +28 -0
  203. data/spec/test_app/Rakefile +6 -0
  204. data/spec/test_app/app/assets/javascripts/application.js +13 -0
  205. data/spec/test_app/app/assets/stylesheets/application.css +15 -0
  206. data/spec/test_app/app/controllers/application_controller.rb +5 -0
  207. data/spec/test_app/app/helpers/application_helper.rb +2 -0
  208. data/spec/test_app/app/views/layouts/application.html.erb +14 -0
  209. data/spec/test_app/bin/bundle +3 -0
  210. data/spec/test_app/bin/rails +4 -0
  211. data/spec/test_app/bin/rake +4 -0
  212. data/spec/test_app/bin/setup +29 -0
  213. data/spec/test_app/config.ru +4 -0
  214. data/spec/test_app/config/application.rb +32 -0
  215. data/spec/test_app/config/boot.rb +5 -0
  216. data/spec/test_app/config/database.yml +25 -0
  217. data/spec/test_app/config/environment.rb +5 -0
  218. data/spec/test_app/config/environments/development.rb +41 -0
  219. data/spec/test_app/config/environments/production.rb +79 -0
  220. data/spec/test_app/config/environments/test.rb +42 -0
  221. data/spec/test_app/config/initializers/assets.rb +11 -0
  222. data/spec/test_app/config/initializers/backtrace_silencers.rb +7 -0
  223. data/spec/test_app/config/initializers/cookies_serializer.rb +3 -0
  224. data/spec/test_app/config/initializers/filter_parameter_logging.rb +4 -0
  225. data/spec/test_app/config/initializers/inflections.rb +16 -0
  226. data/spec/test_app/config/initializers/mime_types.rb +4 -0
  227. data/spec/test_app/config/initializers/session_store.rb +3 -0
  228. data/spec/test_app/config/initializers/wrap_parameters.rb +14 -0
  229. data/spec/test_app/config/locales/en.yml +23 -0
  230. data/spec/test_app/config/routes.rb +4 -0
  231. data/spec/test_app/config/secrets.yml +22 -0
  232. data/spec/test_app/db/schema.rb +49 -0
  233. data/spec/test_app/db/test.sqlite3 +0 -0
  234. data/spec/test_app/log/test.log +5561 -0
  235. data/spec/test_app/public/404.html +67 -0
  236. data/spec/test_app/public/422.html +67 -0
  237. data/spec/test_app/public/500.html +66 -0
  238. data/spec/test_app/public/favicon.ico +0 -0
  239. metadata +550 -0
@@ -0,0 +1,4 @@
1
+ module TabloConnect
2
+ class ApplicationController < ActionController::Base
3
+ end
4
+ end
@@ -0,0 +1,75 @@
1
+ module TabloConnect
2
+ class CopyController < ApplicationController
3
+ before_action :set_item
4
+
5
+ def index
6
+ pid = spawn(ffmpeg_copy)
7
+ Process.detach(pid)
8
+
9
+ @item.copy_status = :in_progress
10
+ @item.save
11
+
12
+ render json: {pid: pid, destination: destination_path}, status: :ok
13
+ end
14
+
15
+ def status
16
+ file_copy_status
17
+ render json: {copy_status: @item.copy_status}
18
+ end
19
+
20
+ private
21
+
22
+ def set_item
23
+ model = case params[:type]
24
+ when 'movie'
25
+ TabloConnect::Movie
26
+ when 'show'
27
+ TabloConnect::Show
28
+ else
29
+ return head :not_found if @item.nil?
30
+ end
31
+
32
+ @item = model.find_by_tablo_id(params[:tablo_id])
33
+ head :not_found if @item.nil?
34
+ end
35
+
36
+ def source_path
37
+ "#{TabloConnect.tablo_base_url}/pvr/#{@item.tablo_id}/pl/playlist.m3u8"
38
+ end
39
+
40
+ def destination_path
41
+ "#{TabloConnect.output_directory}/#{file_name}"
42
+ end
43
+
44
+ def ffmpeg_copy
45
+ "#{TabloConnect.ffmpeg_path} -i \"#{source_path}\" -bsf:a aac_adtstoasc -vcodec copy -c copy #{destination_path}"
46
+ end
47
+
48
+ def file_name
49
+ if params[:type] == 'movie'
50
+ "#{@item.title.gsub(/[^\w\.]/, ' ').squish.gsub(' ', '_')}.mp4"
51
+ elsif params[:type] == 'show'
52
+ "#{@item.show.gsub(/[^\w\.]/, ' ').squish.gsub(' ', '_')}.s#{@item.season.to_s.rjust(2, '0')}e#{@item.episode.to_s.rjust(2, '0')}_#{@item.tablo_id}.mp4"
53
+ end
54
+ end
55
+
56
+ def file_copy_status
57
+ update_copy_status(new_status) if new_status.to_s != @item.copy_status
58
+ end
59
+
60
+ def new_status
61
+ return :idle unless File.exists?(destination_path)
62
+ return :complete if file_unlocked?
63
+ :in_progress
64
+ end
65
+
66
+ def update_copy_status(new_status)
67
+ @item.copy_status = new_status
68
+ @item.save
69
+ end
70
+
71
+ def file_unlocked?
72
+ %x[lsof -F n].split("\n").grep(/#{destination_path}/).empty?
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,6 @@
1
+ module TabloConnect
2
+ class HomeController < ApplicationController
3
+ def index
4
+ end
5
+ end
6
+ end
@@ -0,0 +1,9 @@
1
+ module TabloConnect
2
+ class MoviesController < ApplicationController
3
+ def index
4
+ movies = Movie.all_order_title
5
+
6
+ render json: {movies: movies}
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,11 @@
1
+ module TabloConnect
2
+ class ShowsController < ApplicationController
3
+ def index
4
+ render json: {shows: Show.shows_with_image_id}
5
+ end
6
+
7
+ def episodes
8
+ render json: {episodes: Show.show_episodes(params.require(:show))}
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,73 @@
1
+ require 'open-uri'
2
+ require 'rest-client'
3
+
4
+ module TabloConnect
5
+ class SyncController < ApplicationController
6
+ def index
7
+ begin
8
+ item_ids = parse_items
9
+ delete_removed item_ids
10
+ update_items(item_ids)
11
+ head :ok
12
+ rescue => e
13
+ render json: {error: e.to_s}, status: :internal_server_error
14
+ end
15
+ end
16
+
17
+ private
18
+
19
+ def parse_items
20
+ page = Nokogiri::HTML(open("#{TabloConnect.tablo_base_url}/pvr/"))
21
+ page.css('tbody a').map { |row|
22
+ row[:href].gsub('/', '').to_i if row[:href].gsub('/', '') =~ /\A\d+\z/
23
+ }.compact
24
+ end
25
+
26
+ def delete_removed(new_ids)
27
+ old_ids = TabloConnect::Movie.select(:tablo_id).all + TabloConnect::Show.select(:tablo_id).all
28
+ delete_ids = old_ids.map {|i| i.tablo_id } - new_ids
29
+
30
+ TabloConnect::Movie.delete_by_tablo_id(delete_ids)
31
+ TabloConnect::Show.delete_by_tablo_id(delete_ids)
32
+ end
33
+
34
+ def update_items(tablo_ids)
35
+ tablo_ids.each do |tablo_id|
36
+ details = recording_details tablo_id
37
+
38
+ if details[:recMovie].present?
39
+ update_movie(tablo_id, details)
40
+ elsif details[:recEpisode].present?
41
+ update_show(tablo_id, details)
42
+ end
43
+ end
44
+ end
45
+
46
+ def update_movie(tablo_id, details)
47
+ item = TabloConnect::Movie.find_or_create_by(tablo_id: tablo_id)
48
+ item.title = details.try(:[], :recMovie).try(:[], :jsonForClient).try(:[], :title)
49
+ item.description = details.try(:[], :recMovie).try(:[], :jsonForClient).try(:[], :plot)
50
+ item.release_year = details.try(:[], :recMovie).try(:[], :jsonForClient).try(:[], :releaseYear)
51
+ item.air_date = details.try(:[], :recMovieAiring).try(:[], :jsonForClient).try(:[], :airDate)
52
+ item.image_id = details.try(:[], :recMovie).try(:[], :imageJson).try(:[], :images).try(:[], 0).try(:[], :imageID)
53
+ item.save!
54
+ end
55
+
56
+ def update_show(tablo_id, details)
57
+ item = TabloConnect::Show.find_or_create_by(tablo_id: tablo_id)
58
+ item.show = details.try(:[], :recEpisode).try(:[], :jsonFromTribune).try(:[], :program).try(:[], :title)
59
+ item.title = details.try(:[], :recEpisode).try(:[], :jsonFromTribune).try(:[], :program).try(:[], :episodeTitle)
60
+ item.description = details.try(:[], :recEpisode).try(:[], :jsonFromTribune).try(:[], :program).try(:[], :longDescription)
61
+ item.episode = details.try(:[], :recEpisode).try(:[], :jsonFromTribune).try(:[], :program).try(:[], :episodeNum)
62
+ item.season = details.try(:[], :recEpisode).try(:[], :jsonFromTribune).try(:[], :program).try(:[], :seasonNum)
63
+ item.rec_date = details.try(:[], :recEpisode).try(:[], :jsonForClient).try(:[], :airDate)
64
+ item.air_date = details.try(:[], :recSeries).try(:[], :jsonForClient).try(:[], :originalAirDate)
65
+ item.image_id = details.try(:[], :recSeries).try(:[], :imageJson).try(:[], :images).try(:[], 0).try(:[], :imageID)
66
+ item.save!
67
+ end
68
+
69
+ def recording_details(tablo_id)
70
+ JSON.parse(RestClient.get("#{TabloConnect.tablo_base_url}/pvr/#{tablo_id}/meta.txt")).with_indifferent_access
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,4 @@
1
+ module TabloConnect
2
+ module ApplicationHelper
3
+ end
4
+ end
@@ -0,0 +1,4 @@
1
+ module TabloConnect
2
+ module MoviesHelper
3
+ end
4
+ end
@@ -0,0 +1,4 @@
1
+ module TabloConnect
2
+ module ShowsHelper
3
+ end
4
+ end
@@ -0,0 +1,14 @@
1
+ module TabloConnect
2
+ class Movie < ActiveRecord::Base
3
+ validates_presence_of :tablo_id
4
+ validates_uniqueness_of :tablo_id
5
+
6
+ enum copy_status: [:idle, :in_progress, :complete]
7
+
8
+ scope :all_order_title, -> { self.all.order(:title) }
9
+
10
+ def self.delete_by_tablo_id(delete_ids)
11
+ self.where(tablo_id: delete_ids).delete_all
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,23 @@
1
+ module TabloConnect
2
+ class Show < ActiveRecord::Base
3
+ validates_presence_of :tablo_id
4
+ validates_uniqueness_of :tablo_id
5
+
6
+ enum copy_status: [:idle, :in_progress, :complete]
7
+
8
+ scope :shows, -> { group(:show).order(:show).count }
9
+ scope :show_episodes, ->(show) { where(show: show).order(:season, :episode, :rec_date, :title) }
10
+
11
+ def self.shows_with_image_id
12
+ self.shows.map { |show|
13
+ image_id = self.select(:image_id).distinct(:image_id).where({show: show[0]}).order(image_id: :desc).limit(1)[0].try(:[], :image_id)
14
+
15
+ {show: show[0], count: show[1], image_id: image_id}
16
+ }
17
+ end
18
+
19
+ def self.delete_by_tablo_id(delete_ids)
20
+ self.where(tablo_id: delete_ids).delete_all
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,20 @@
1
+ <nav class="navbar navbar-default">
2
+ <div class="container-fluid">
3
+ <div class="navbar-header">
4
+ <button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#bs-nav-collapse" aria-expanded="false">
5
+ <span class="sr-only">Toggle navigation</span>
6
+ <span class="icon-bar"></span>
7
+ <span class="icon-bar"></span>
8
+ <span class="icon-bar"></span>
9
+ </button>
10
+ <a class="navbar-brand" href="#/">Tablo Connect</a>
11
+ </div>
12
+
13
+ <div class="collapse navbar-collapse" id="bs-nav-collapse">
14
+ <ul class="nav navbar-nav">
15
+ <li><a ui-sref="movies">Movies</a></li>
16
+ <li><a ui-sref="shows">Shows</a></li>
17
+ </ul>
18
+ </div>
19
+ </div>
20
+ </nav>
@@ -0,0 +1,19 @@
1
+ <!DOCTYPE html>
2
+ <html ng-app="ac.tabloConnectApp">
3
+ <head>
4
+ <title>TabloConnect</title>
5
+ <%= stylesheet_link_tag "tablo_connect/application", media: "all" %>
6
+ <%= csrf_meta_tags %>
7
+ </head>
8
+ <body>
9
+ <%= render 'layouts/tablo_connect/header' %>
10
+ <div class="container-fluid">
11
+ <%= yield %>
12
+ </div>
13
+
14
+ <%= javascript_include_tag "tablo_connect/application" %>
15
+ <script>
16
+ angular.module('ac.tabloConnectApp').constant('tabloBaseUrl', '<%= "http://#{TabloConnect.tablo_ip}:#{TabloConnect.tablo_port}" %>')
17
+ </script>
18
+ </body>
19
+ </html>
@@ -0,0 +1 @@
1
+ <ui-view></ui-view>
data/config/routes.rb ADDED
@@ -0,0 +1,10 @@
1
+ TabloConnect::Engine.routes.draw do
2
+ root to: "home#index"
3
+
4
+ get 'sync', to: 'sync#index'
5
+ get 'movies', to: 'movies#index'
6
+ get 'shows', to: 'shows#index'
7
+ get 'episodes/:show', to: 'shows#episodes'
8
+ get 'copy/:tablo_id/:type', to: 'copy#index'
9
+ get 'copy/:tablo_id/:type/status', to: 'copy#status'
10
+ end
@@ -0,0 +1,18 @@
1
+ class CreateTabloConnectMovies < ActiveRecord::Migration
2
+ def change
3
+ create_table :tablo_connect_movies do |t|
4
+ t.integer :tablo_id
5
+ t.string :title
6
+ t.text :description
7
+ t.integer :release_year
8
+ t.datetime :air_date
9
+ t.integer :image_id
10
+ t.integer :copy_status, default: 0
11
+
12
+ t.timestamps null: false
13
+ end
14
+
15
+ add_index :tablo_connect_movies, :tablo_id, :unique => true
16
+ add_index :tablo_connect_movies, :title
17
+ end
18
+ end
@@ -0,0 +1,21 @@
1
+ class CreateTabloConnectShows < ActiveRecord::Migration
2
+ def change
3
+ create_table :tablo_connect_shows do |t|
4
+ t.integer :tablo_id
5
+ t.string :show
6
+ t.string :title
7
+ t.text :description
8
+ t.integer :episode
9
+ t.integer :season
10
+ t.date :air_date
11
+ t.datetime :rec_date
12
+ t.integer :image_id
13
+ t.integer :copy_status, default: 0
14
+
15
+ t.timestamps null: false
16
+ end
17
+
18
+ add_index :tablo_connect_shows, :tablo_id, :unique => true
19
+ add_index :tablo_connect_shows, :show
20
+ end
21
+ end
@@ -0,0 +1,7 @@
1
+ require "tablo_connect/engine"
2
+
3
+ module TabloConnect
4
+ def self.tablo_base_url
5
+ "http://#{TabloConnect.tablo_ip}:#{TabloConnect.tablo_port}"
6
+ end
7
+ end
@@ -0,0 +1,28 @@
1
+ module TabloConnect
2
+ class Engine < ::Rails::Engine
3
+ isolate_namespace TabloConnect
4
+
5
+ config.generators do |g|
6
+ g.test_framework :rspec
7
+ g.fixture_replacement :factory_girl, :dir => 'spec/factories'
8
+ end
9
+ end
10
+
11
+ class << self
12
+ mattr_accessor :tablo_ip
13
+ self.tablo_ip = '192.168.1.99'
14
+
15
+ mattr_accessor :tablo_port
16
+ self.tablo_port = '180809'
17
+
18
+ mattr_accessor :ffmpeg_path
19
+ self.ffmpeg_path = '/usr/local/bin/ffmpeg'
20
+
21
+ mattr_accessor :output_directory
22
+ self.output_directory = '/tmp'
23
+ end
24
+
25
+ def self.setup(&block)
26
+ yield self
27
+ end
28
+ end
@@ -0,0 +1,3 @@
1
+ module TabloConnect
2
+ VERSION = "0.0.2"
3
+ end
@@ -0,0 +1,4 @@
1
+ # desc "Explaining what the task does"
2
+ # task :tablo_connect do
3
+ # # Task goes here
4
+ # end
@@ -0,0 +1,244 @@
1
+ require 'spec_helper'
2
+
3
+ module TabloConnect
4
+ describe CopyController, type: :controller do
5
+ routes { TabloConnect::Engine.routes }
6
+
7
+ let!(:show) { FactoryGirl.create(:tablo_connect_show, show: 'Angle of Attack') }
8
+
9
+ describe "GET index" do
10
+ before do
11
+ allow(controller).to receive(:spawn).and_return(1234)
12
+ allow(controller).to receive(:destination_path).and_return('/tmp/output.mp4')
13
+ allow(Process).to receive(:detach)
14
+ end
15
+
16
+ context "when the item is not found" do
17
+ it "returns a 404" do
18
+ get :index, {tablo_id: 99999, type: 'some_type'}
19
+ expect(response.status).to eq 404
20
+ end
21
+ end
22
+
23
+ context "when the item is found" do
24
+ it "returns a 200" do
25
+ get :index, {tablo_id: show.tablo_id, type: 'show'}
26
+ expect(response.status).to eq 200
27
+ end
28
+
29
+ it "calls spawn" do
30
+ expect(controller).to receive(:spawn)
31
+ get :index, {tablo_id: show.tablo_id, type: 'show'}
32
+ end
33
+
34
+ it "sets the copy_status to in_progress" do
35
+ item = TabloConnect::Show.find_by_tablo_id show.tablo_id
36
+ expect(item.copy_status).to eq('idle')
37
+ get :index, {tablo_id: show.tablo_id, type: 'show'}
38
+ item.reload
39
+ expect(item.copy_status).to eq('in_progress')
40
+ end
41
+
42
+ it "returns an object with a shows array" do
43
+ get :index, {tablo_id: show.tablo_id, type: 'show'}
44
+ json_body = JSON.parse(response.body).with_indifferent_access
45
+ expect(json_body).to match({pid: 1234, destination: '/tmp/output.mp4'})
46
+ end
47
+ end
48
+ end
49
+
50
+ describe "GET status" do
51
+ it "calls file_copy_status" do
52
+ expect(controller).to receive(:file_copy_status)
53
+ get :status, {tablo_id: show.tablo_id, type: 'show'}
54
+ end
55
+
56
+ it "returns a 200 status code" do
57
+ get :status, {tablo_id: show.tablo_id, type: 'show'}
58
+ expect(response.status).to eq 200
59
+ end
60
+
61
+ it "returns a JSON object with the copy_status" do
62
+ get :status, {tablo_id: show.tablo_id, type: 'show'}
63
+ json_body = JSON.parse(response.body).with_indifferent_access
64
+ expect(json_body[:copy_status]).to eq show.copy_status
65
+ end
66
+ end
67
+
68
+ describe ".set_item" do
69
+ before do
70
+ allow(controller).to receive(:head).and_return(double.as_null_object)
71
+ end
72
+
73
+ context "when the type is not show or movie" do
74
+ it "returns a 404" do
75
+ allow(controller).to receive(:params).and_return({tablo_id: show.tablo_id, type: 'invalid_type'})
76
+ expect(controller).to receive(:head).with(:not_found)
77
+ controller.send(:set_item)
78
+ end
79
+ end
80
+
81
+ context "when the type is 'show' or 'movie'" do
82
+ context "when the type is 'movie'" do
83
+ it "uses the Movie model" do
84
+ allow(controller).to receive(:params).and_return({tablo_id: show.tablo_id, type: 'movie'})
85
+ expect(TabloConnect::Movie).to receive(:find_by_tablo_id).with(show.tablo_id)
86
+ controller.send(:set_item)
87
+ end
88
+ end
89
+
90
+ context "when the type is 'show'" do
91
+ it "uses the Show model" do
92
+ allow(controller).to receive(:params).and_return({tablo_id: show.tablo_id, type: 'show'})
93
+ expect(TabloConnect::Show).to receive(:find_by_tablo_id).with(show.tablo_id)
94
+ controller.send(:set_item)
95
+ end
96
+ end
97
+
98
+ context "when the item is not found" do
99
+ it "returns a 404" do
100
+ allow(controller).to receive(:params).and_return({tablo_id: 99999, type: 'show'})
101
+ controller.send(:set_item)
102
+ end
103
+ end
104
+
105
+ context "when the item is found" do
106
+ it "sets the @item instance variable" do
107
+ allow(controller).to receive(:params).and_return({tablo_id: show.tablo_id, type: 'show'})
108
+ controller.send(:set_item)
109
+ item = controller.instance_variable_get(:@item)
110
+ expect(item).to match(show)
111
+ end
112
+ end
113
+ end
114
+ end
115
+
116
+ describe ".source_path" do
117
+ it "builds the source_path with the tablo_id" do
118
+ controller.instance_variable_set(:@item, show)
119
+ path = controller.send(:source_path)
120
+ expect(path).to eq "#{TabloConnect.tablo_base_url}/pvr/#{show.tablo_id}/pl/playlist.m3u8"
121
+ end
122
+ end
123
+
124
+ describe ".destination_path" do
125
+ it "builds the destination path with the file_name method" do
126
+ controller.instance_variable_set(:@item, show)
127
+ allow(controller).to receive(:file_name).and_return('output.mp4')
128
+ path = controller.send(:destination_path)
129
+ expect(path).to eq "#{TabloConnect.output_directory}/output.mp4"
130
+ end
131
+ end
132
+
133
+ describe ".ffmpeg_copy" do
134
+ it "returns the ffmpeg copy command string" do
135
+ allow(controller).to receive(:source_path).and_return('/source/path/input.mp4')
136
+ allow(controller).to receive(:destination_path).and_return('/destination/path/output.mp4')
137
+ expect(controller.send(:ffmpeg_copy)).to eq "#{TabloConnect.ffmpeg_path} -i \"/source/path/input.mp4\" -bsf:a aac_adtstoasc -vcodec copy -c copy /destination/path/output.mp4"
138
+ end
139
+ end
140
+
141
+ describe ".file_name" do
142
+ before do
143
+ controller.instance_variable_set(:@item, show)
144
+ end
145
+
146
+ context "when type is 'movie'" do
147
+ it "returns the unix safe movie file name" do
148
+ allow(controller).to receive(:params).and_return({type: 'movie'})
149
+ expect(controller.send(:file_name)).to eq('The_Harder_They_Fall.mp4')
150
+ end
151
+ end
152
+
153
+ context "when type is 'show'" do
154
+ it "returns the unix safe show file name" do
155
+ allow(controller).to receive(:params).and_return({type: 'show'})
156
+ expect(controller.send(:file_name)).to eq("Angle_of_Attack.s01e01_#{show.tablo_id}.mp4")
157
+ end
158
+ end
159
+ end
160
+
161
+ describe ".file_copy_status" do
162
+ before do
163
+ controller.instance_variable_set(:@item, show)
164
+ end
165
+
166
+ context "when the file exists" do
167
+ before do
168
+ allow(controller).to receive(:destination_path).and_return(Rails.root.join('public', '404.html').to_s)
169
+ end
170
+
171
+ context "when the file is in use" do
172
+ it "updates the copy_status to :in_progress" do
173
+ allow(controller).to receive(:file_unlocked?).and_return(false)
174
+ controller.send(:file_copy_status)
175
+ expect(show.copy_status).to eq 'in_progress'
176
+ end
177
+ end
178
+
179
+ context "when the file is not in use" do
180
+ it "updates the copy_status to :complete" do
181
+ allow(controller).to receive(:file_unlocked?).and_return(true)
182
+ controller.send(:file_copy_status)
183
+ expect(show.copy_status).to eq 'complete'
184
+ end
185
+ end
186
+
187
+ context "when the new_status is equal to the current status" do
188
+ it "does not update the copy_status" do
189
+ allow(controller).to receive(:file_unlocked?).and_return(true)
190
+ show.copy_status = :complete
191
+ show.save
192
+ expect(show.copy_status).to eq 'complete'
193
+ expect(controller).to_not receive(:update_copy_status).with(:complete)
194
+ controller.send(:file_copy_status)
195
+ expect(show.copy_status).to eq 'complete'
196
+ end
197
+ end
198
+ end
199
+
200
+ context "when the file does not exist" do
201
+ before do
202
+ allow(controller).to receive(:destination_path).and_return(Rails.root.join('public', 'non_existent.html').to_s)
203
+ end
204
+
205
+ it "updates the copy_status to :idle" do
206
+ show.copy_status = :in_progress
207
+ show.save!
208
+ expect(show.copy_status).to eq 'in_progress'
209
+ controller.send(:file_copy_status)
210
+ expect(show.copy_status).to eq 'idle'
211
+ end
212
+ end
213
+ end
214
+
215
+ describe ".new_status" do
216
+ context "when file does not exist" do
217
+ it "returns :idle" do
218
+ allow(File).to receive(:exists?).and_return(false)
219
+ expect(controller.send(:new_status)).to eq :idle
220
+ end
221
+ end
222
+
223
+ context "when file exists" do
224
+ before do
225
+ allow(File).to receive(:exists?).and_return(true)
226
+ end
227
+
228
+ context "when file is not in use" do
229
+ it "returns :complete" do
230
+ allow(controller).to receive(:file_unlocked?).and_return(true)
231
+ expect(controller.send(:new_status)).to eq :complete
232
+ end
233
+ end
234
+
235
+ context "when file is in use" do
236
+ it "returns :in_progress" do
237
+ allow(controller).to receive(:file_unlocked?).and_return(false)
238
+ expect(controller.send(:new_status)).to eq :in_progress
239
+ end
240
+ end
241
+ end
242
+ end
243
+ end
244
+ end