tivohmo 0.1.0

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 (81) hide show
  1. checksums.yaml +7 -0
  2. data/.coveralls.yml +1 -0
  3. data/.gitignore +17 -0
  4. data/.travis.yml +10 -0
  5. data/Gemfile +7 -0
  6. data/LICENSE.txt +514 -0
  7. data/README.md +50 -0
  8. data/Rakefile +7 -0
  9. data/bin/tivohmo +8 -0
  10. data/contrib/tivohmo.conf +17 -0
  11. data/contrib/tivohmo.plist +22 -0
  12. data/lib/tivohmo/adapters/filesystem/application.rb +23 -0
  13. data/lib/tivohmo/adapters/filesystem/file_item.rb +29 -0
  14. data/lib/tivohmo/adapters/filesystem/folder_container.rb +105 -0
  15. data/lib/tivohmo/adapters/filesystem.rb +3 -0
  16. data/lib/tivohmo/adapters/plex/application.rb +41 -0
  17. data/lib/tivohmo/adapters/plex/category.rb +72 -0
  18. data/lib/tivohmo/adapters/plex/episode.rb +26 -0
  19. data/lib/tivohmo/adapters/plex/metadata.rb +24 -0
  20. data/lib/tivohmo/adapters/plex/movie.rb +26 -0
  21. data/lib/tivohmo/adapters/plex/qualified_category.rb +51 -0
  22. data/lib/tivohmo/adapters/plex/season.rb +39 -0
  23. data/lib/tivohmo/adapters/plex/section.rb +48 -0
  24. data/lib/tivohmo/adapters/plex/show.rb +39 -0
  25. data/lib/tivohmo/adapters/plex/transcoder.rb +19 -0
  26. data/lib/tivohmo/adapters/plex.rb +11 -0
  27. data/lib/tivohmo/adapters/streamio/metadata.rb +26 -0
  28. data/lib/tivohmo/adapters/streamio/transcoder.rb +239 -0
  29. data/lib/tivohmo/adapters/streamio.rb +17 -0
  30. data/lib/tivohmo/api/application.rb +35 -0
  31. data/lib/tivohmo/api/container.rb +31 -0
  32. data/lib/tivohmo/api/item.rb +32 -0
  33. data/lib/tivohmo/api/metadata.rb +98 -0
  34. data/lib/tivohmo/api/node.rb +115 -0
  35. data/lib/tivohmo/api/server.rb +24 -0
  36. data/lib/tivohmo/api/transcoder.rb +39 -0
  37. data/lib/tivohmo/api.rb +7 -0
  38. data/lib/tivohmo/beacon.rb +69 -0
  39. data/lib/tivohmo/cli.rb +182 -0
  40. data/lib/tivohmo/logging.rb +50 -0
  41. data/lib/tivohmo/server/views/_container.builder +19 -0
  42. data/lib/tivohmo/server/views/_item.builder +57 -0
  43. data/lib/tivohmo/server/views/container.builder +26 -0
  44. data/lib/tivohmo/server/views/item_details.builder +153 -0
  45. data/lib/tivohmo/server/views/layout.builder +2 -0
  46. data/lib/tivohmo/server/views/layout.erb +10 -0
  47. data/lib/tivohmo/server/views/server.builder +18 -0
  48. data/lib/tivohmo/server/views/server_info.builder +7 -0
  49. data/lib/tivohmo/server/views/unsupported.erb +7 -0
  50. data/lib/tivohmo/server/views/video_formats.builder +10 -0
  51. data/lib/tivohmo/server.rb +306 -0
  52. data/lib/tivohmo/version.rb +3 -0
  53. data/lib/tivohmo.rb +5 -0
  54. data/spec/adapters/filesystem/application_spec.rb +19 -0
  55. data/spec/adapters/filesystem/file_item_spec.rb +33 -0
  56. data/spec/adapters/filesystem/folder_container_spec.rb +115 -0
  57. data/spec/adapters/plex/application_spec.rb +20 -0
  58. data/spec/adapters/plex/category_spec.rb +66 -0
  59. data/spec/adapters/plex/episode_spec.rb +22 -0
  60. data/spec/adapters/plex/metadata_spec.rb +24 -0
  61. data/spec/adapters/plex/movie_spec.rb +22 -0
  62. data/spec/adapters/plex/qualified_category_spec.rb +51 -0
  63. data/spec/adapters/plex/season_spec.rb +22 -0
  64. data/spec/adapters/plex/section_spec.rb +38 -0
  65. data/spec/adapters/plex/show_spec.rb +22 -0
  66. data/spec/adapters/plex/transcoder_spec.rb +27 -0
  67. data/spec/adapters/streamio/metadata_spec.rb +34 -0
  68. data/spec/adapters/streamio/transcoder_spec.rb +42 -0
  69. data/spec/api/application_spec.rb +63 -0
  70. data/spec/api/container_spec.rb +34 -0
  71. data/spec/api/item_spec.rb +53 -0
  72. data/spec/api/metadata_spec.rb +59 -0
  73. data/spec/api/node_spec.rb +178 -0
  74. data/spec/api/server_spec.rb +24 -0
  75. data/spec/api/transcoder_spec.rb +25 -0
  76. data/spec/beacon_spec.rb +87 -0
  77. data/spec/cli_spec.rb +227 -0
  78. data/spec/server_spec.rb +458 -0
  79. data/spec/spec_helper.rb +123 -0
  80. data/tivohmo.gemspec +46 -0
  81. metadata +416 -0
@@ -0,0 +1,23 @@
1
+ require 'active_support/core_ext/string/inflections'
2
+ require 'tivohmo/adapters/streamio'
3
+
4
+ module TivoHMO
5
+ module Adapters
6
+ module Filesystem
7
+
8
+ # An Application based on a filesystem
9
+ class Application < FolderContainer
10
+ include TivoHMO::API::Application
11
+ include GemLogger::LoggerSupport
12
+ include MonitorMixin
13
+
14
+ def initialize(identifier)
15
+ super(identifier)
16
+ self.metadata_class = TivoHMO::Adapters::StreamIO::Metadata
17
+ self.transcoder_class = TivoHMO::Adapters::StreamIO::Transcoder
18
+ end
19
+ end
20
+
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,29 @@
1
+ #require 'streamio-ffmpeg'
2
+
3
+ module TivoHMO
4
+ module Adapters
5
+ module Filesystem
6
+
7
+ # An Item based on a filesystem file
8
+ class FileItem
9
+ include TivoHMO::API::Item
10
+ include GemLogger::LoggerSupport
11
+
12
+ attr_reader :full_path
13
+
14
+ def initialize(identifier)
15
+ @full_path = File.expand_path(identifier)
16
+ raise ArgumentError, "Must provide an existing file" unless File.file?(full_path)
17
+
18
+ super(full_path)
19
+
20
+ self.title = File.basename(self.identifier)
21
+ self.modified_at = File.mtime(self.identifier)
22
+ self.created_at = File.ctime(self.identifier)
23
+ end
24
+
25
+ end
26
+
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,105 @@
1
+ require 'active_support/core_ext/string/inflections'
2
+ require 'listen'
3
+
4
+ module TivoHMO
5
+ module Adapters
6
+ module Filesystem
7
+
8
+ # A Container based on a filesystem folder
9
+ class FolderContainer
10
+ include TivoHMO::API::Container
11
+ include GemLogger::LoggerSupport
12
+ include MonitorMixin
13
+
14
+ VIDEO_EXTENSIONS = %w[
15
+ tivo mpg avi wmv mov flv f4v vob mp4 m4v mkv
16
+ ts tp trp 3g2 3gp 3gp2 3gpp amv asf avs bik bix box bsf
17
+ dat dif divx dmb dpg dv dvr-ms evo eye flc fli flx gvi ivf
18
+ m1v m21 m2t m2ts m2v m2p m4e mjp mjpeg mod moov movie mp21
19
+ mpe mpeg mpv mpv2 mqv mts mvb nsv nuv nut ogm qt rm rmvb
20
+ rts scm smv ssm svi vdo vfw vid viv vivo vp6 vp7 vro webm
21
+ wm wmd wtv yuv
22
+ ]
23
+
24
+ attr_reader :full_path
25
+
26
+ attr_accessor :allowed_item_types,
27
+ :allowed_item_extensions
28
+
29
+ def initialize(identifier)
30
+ @full_path = File.expand_path(identifier)
31
+ raise ArgumentError,
32
+ "Must provide an existing directory: #{full_path}" unless File.directory?(full_path)
33
+
34
+ super(full_path)
35
+
36
+ self.allowed_item_types = %i[file dir]
37
+ self.allowed_item_extensions = VIDEO_EXTENSIONS
38
+
39
+ self.title = File.basename(self.identifier).titleize
40
+ self.modified_at = File.mtime(self.identifier)
41
+ self.created_at = File.ctime(self.identifier)
42
+
43
+ setup_change_listener
44
+ end
45
+
46
+ def children
47
+ synchronize do
48
+ if super.blank?
49
+ folders = []
50
+ files = []
51
+
52
+ Dir["#{self.full_path}/*"].each do |path|
53
+ if allowed_container?(path)
54
+ folders << FolderContainer.new(path)
55
+ elsif allowed_item?(path)
56
+ files << FileItem.new(path)
57
+ else
58
+ logger.debug "Ignoring: #{path}"
59
+ end
60
+ end
61
+
62
+ (folders + files).each {|c| add_child(c) }
63
+ end
64
+ end
65
+
66
+ super
67
+ end
68
+
69
+ protected
70
+
71
+ def setup_change_listener
72
+ logger.debug "Setting up change listener on #{identifier}"
73
+ @listener = Listen.to(identifier, ignore: /\//) do |modified, added, removed|
74
+ logger.debug "Detected filesystem change on #{identifier}"
75
+ logger.debug "modified: #{modified}"
76
+ logger.debug "added: #{added}"
77
+ logger.debug "removed: #{removed}"
78
+
79
+ # TODO: be more intelligent instead of just wiping children to cause the refresh
80
+ self.refresh
81
+
82
+ # cleanup - not strictly correct as this listener won't necessarily get triggered
83
+ # if self is removed from the parent
84
+ @listener.stop unless root.find(title_path)
85
+ logger.debug "Completed filesystem refresh on #{identifier}"
86
+ end
87
+ @listener.start
88
+ end
89
+
90
+ def allowed_container?(path)
91
+ File.directory?(path) && allowed_item_types.include?(:dir)
92
+ end
93
+
94
+ def allowed_item?(path)
95
+ ext = File.extname(path).gsub(/^./, '')
96
+ File.file?(path) &&
97
+ allowed_item_types.include?(:file) &&
98
+ allowed_item_extensions.include?(ext)
99
+ end
100
+
101
+ end
102
+
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,3 @@
1
+ require_relative 'filesystem/file_item'
2
+ require_relative 'filesystem/folder_container'
3
+ require_relative 'filesystem/application'
@@ -0,0 +1,41 @@
1
+ module TivoHMO
2
+ module Adapters
3
+ module Plex
4
+
5
+ class Application
6
+ include TivoHMO::API::Application
7
+ include GemLogger::LoggerSupport
8
+ include MonitorMixin
9
+
10
+ attr_reader :server
11
+
12
+ def initialize(identifier)
13
+ host, port = identifier.to_s.split(':')
14
+ host ||= 'localhost'
15
+ port ||= 32400
16
+ super("Plex[#{host}:#{port}]")
17
+
18
+ self.metadata_class = TivoHMO::Adapters::Plex::Metadata
19
+ self.transcoder_class = TivoHMO::Adapters::Plex::Transcoder
20
+ self.title = self.identifier
21
+
22
+ @server = ::Plex::Server.new(host, port)
23
+ end
24
+
25
+ def children
26
+ synchronize do
27
+ if super.blank?
28
+ Array(server.library.sections).each do |section|
29
+ add_child(Section.new(section))
30
+ end
31
+ end
32
+ end
33
+
34
+ super
35
+ end
36
+
37
+ end
38
+
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,72 @@
1
+ module TivoHMO
2
+ module Adapters
3
+ module Plex
4
+
5
+ class Category
6
+ include TivoHMO::API::Container
7
+ include GemLogger::LoggerSupport
8
+ include MonitorMixin
9
+
10
+ attr_reader :delegate
11
+ attr_accessor :category_type, :category_value
12
+
13
+ def initialize(delegate, category_type, category_value=nil)
14
+ # delegate is a Plex::Section
15
+ @delegate = delegate
16
+
17
+ super(delegate.key)
18
+
19
+ self.category_type = category_type
20
+ self.category_value = category_value
21
+
22
+ if category_value
23
+ self.title = category_value[:title]
24
+ else
25
+ self.title = category_type.to_s.titleize
26
+ end
27
+
28
+ self.modified_at = Time.at(delegate.updated_at.to_i)
29
+ self.created_at = Time.at(delegate.updated_at.to_i)
30
+ end
31
+
32
+ def children
33
+ synchronize do
34
+
35
+ delegate.refresh
36
+ new_modified_at = delegate.updated_at.to_i
37
+ if new_modified_at > modified_at.to_i
38
+ logger.info "Plex section was updated, refreshing"
39
+ self.modified_at = Time.at(new_modified_at)
40
+ super.clear
41
+ end
42
+
43
+ if super.blank?
44
+
45
+ if category_value
46
+ listing = delegate.send(category_type, category_value[:key])
47
+ else
48
+ listing = delegate.send(category_type)
49
+ end
50
+
51
+ Array(listing).each do |media|
52
+ if media.is_a?(::Plex::Movie)
53
+ add_child(Movie.new(media))
54
+ elsif media.is_a?(::Plex::Episode)
55
+ add_child(Episode.new(media))
56
+ elsif media.is_a?(::Plex::Show)
57
+ add_child(Show.new(media))
58
+ else
59
+ logger.error "Unknown type for #{media.class} in #{self.title}"
60
+ end
61
+ end
62
+ end
63
+ end
64
+
65
+ super
66
+ end
67
+
68
+ end
69
+
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,26 @@
1
+ module TivoHMO
2
+ module Adapters
3
+ module Plex
4
+
5
+ class Episode
6
+ include TivoHMO::API::Item
7
+ include GemLogger::LoggerSupport
8
+
9
+ attr_reader :delegate
10
+
11
+ def initialize(delegate)
12
+ # delegate is a Plex::Episode
13
+ @delegate = delegate
14
+
15
+ super(delegate.key)
16
+
17
+ self.title = delegate.title
18
+ self.modified_at = Time.at(delegate.updated_at.to_i)
19
+ self.created_at = Time.at(delegate.added_at.to_i)
20
+ end
21
+
22
+ end
23
+
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,24 @@
1
+ module TivoHMO
2
+ module Adapters
3
+ module Plex
4
+
5
+ class Metadata
6
+ include TivoHMO::API::Metadata
7
+ include GemLogger::LoggerSupport
8
+
9
+ def initialize(item)
10
+ super
11
+
12
+ begin
13
+ self.description = item.delegate.summary
14
+ self.duration = (item.delegate.duration.to_i / 1000).to_i
15
+ rescue => e
16
+ logger.error "Failed to read plex metadata: #{e}"
17
+ end
18
+ end
19
+
20
+ end
21
+
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,26 @@
1
+ module TivoHMO
2
+ module Adapters
3
+ module Plex
4
+
5
+ class Movie
6
+ include TivoHMO::API::Item
7
+ include GemLogger::LoggerSupport
8
+
9
+ attr_reader :delegate
10
+
11
+ def initialize(delegate)
12
+ # delegate is a Plex::Movie
13
+ @delegate = delegate
14
+
15
+ super(delegate.key)
16
+
17
+ self.title = delegate.title
18
+ self.modified_at = Time.at(delegate.updated_at.to_i)
19
+ self.created_at = Time.at(delegate.added_at.to_i)
20
+ end
21
+
22
+ end
23
+
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,51 @@
1
+ module TivoHMO
2
+ module Adapters
3
+ module Plex
4
+
5
+ class QualifiedCategory
6
+ include TivoHMO::API::Container
7
+ include GemLogger::LoggerSupport
8
+ include MonitorMixin
9
+
10
+ attr_reader :delegate
11
+ attr_accessor :category_type, :category_qualifier
12
+
13
+ def initialize(delegate, category_type, category_qualifier)
14
+ # delegate is a Plex::Section
15
+ @delegate = delegate
16
+
17
+ super(delegate.key)
18
+
19
+ self.category_type = category_type
20
+ self.category_qualifier = category_qualifier
21
+ self.title = category_type.to_s.titleize
22
+ self.modified_at = Time.at(delegate.updated_at.to_i)
23
+ self.created_at = Time.at(delegate.updated_at.to_i)
24
+ end
25
+
26
+ def children
27
+ synchronize do
28
+
29
+ delegate.refresh
30
+ new_modified_at = delegate.updated_at.to_i
31
+ if new_modified_at > modified_at.to_i
32
+ logger.info "Plex section was updated, refreshing"
33
+ self.modified_at = Time.at(new_modified_at)
34
+ super.clear
35
+ end
36
+
37
+ if super.blank?
38
+ Array(delegate.send(category_qualifier)).each do |category_value|
39
+ add_child(Category.new(delegate, category_type, category_value))
40
+ end
41
+ end
42
+ end
43
+
44
+ super
45
+ end
46
+
47
+ end
48
+
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,39 @@
1
+ module TivoHMO
2
+ module Adapters
3
+ module Plex
4
+
5
+ class Season
6
+ include TivoHMO::API::Container
7
+ include GemLogger::LoggerSupport
8
+ include MonitorMixin
9
+
10
+ attr_reader :delegate
11
+
12
+ def initialize(delegate)
13
+ # delegate is a Plex::Season
14
+ @delegate = delegate
15
+
16
+ super(delegate.key)
17
+
18
+ self.title = delegate.title
19
+ # self.modified_at = Time.at(delegate.updated_at.to_i)
20
+ # self.created_at = Time.at(delegate.added_at.to_i)
21
+ end
22
+
23
+ def children
24
+ synchronize do
25
+ if super.blank?
26
+ Array(delegate.episodes).each do |media|
27
+ add_child(Episode.new(media))
28
+ end
29
+ end
30
+ end
31
+
32
+ super
33
+ end
34
+
35
+ end
36
+
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,48 @@
1
+ module TivoHMO
2
+ module Adapters
3
+ module Plex
4
+
5
+ class Section
6
+ include TivoHMO::API::Container
7
+ include GemLogger::LoggerSupport
8
+ include MonitorMixin
9
+
10
+ attr_reader :delegate
11
+
12
+ def initialize(delegate)
13
+ # delegate is a Plex::Section
14
+ @delegate = delegate
15
+
16
+ super(delegate.key)
17
+
18
+ self.title = delegate.title
19
+ self.modified_at = Time.at(delegate.updated_at.to_i)
20
+ self.created_at = Time.at(delegate.updated_at.to_i)
21
+ end
22
+
23
+ def children
24
+ synchronize do
25
+ if super.blank?
26
+ add_child(Category.new(delegate, :recently_added))
27
+ #add_child(Category.new(delegate, :unwatched))
28
+ add_child(Category.new(delegate, :newest))
29
+ add_child(Category.new(delegate, :on_deck))
30
+ add_child(Category.new(delegate, :recently_viewed))
31
+ add_child(Category.new(delegate, :all))
32
+ add_child(QualifiedCategory.new(delegate, :by_genre, :genres))
33
+ add_child(QualifiedCategory.new(delegate, :by_year, :years))
34
+ add_child(QualifiedCategory.new(delegate, :by_first_character, :first_characters))
35
+ add_child(QualifiedCategory.new(delegate, :by_collection, :collections))
36
+ add_child(QualifiedCategory.new(delegate, :by_folder, :folders))
37
+ add_child(QualifiedCategory.new(delegate, :by_content_rating, :content_ratings))
38
+ end
39
+ end
40
+
41
+ super
42
+ end
43
+
44
+ end
45
+
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,39 @@
1
+ module TivoHMO
2
+ module Adapters
3
+ module Plex
4
+
5
+ class Show
6
+ include TivoHMO::API::Container
7
+ include GemLogger::LoggerSupport
8
+ include MonitorMixin
9
+
10
+ attr_reader :delegate
11
+
12
+ def initialize(delegate)
13
+ # delegate is a Plex::Show
14
+ @delegate = delegate
15
+
16
+ super(delegate.key)
17
+
18
+ self.title = delegate.title
19
+ self.modified_at = Time.at(delegate.updated_at.to_i)
20
+ self.created_at = Time.at(delegate.added_at.to_i)
21
+ end
22
+
23
+ def children
24
+ synchronize do
25
+ if super.blank?
26
+ Array(delegate.seasons).each do |media|
27
+ add_child(Season.new(media))
28
+ end
29
+ end
30
+ end
31
+
32
+ super
33
+ end
34
+
35
+ end
36
+
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,19 @@
1
+ require 'tivohmo/adapters/streamio'
2
+
3
+ module TivoHMO
4
+ module Adapters
5
+ module Plex
6
+
7
+ class Transcoder < TivoHMO::Adapters::StreamIO::Transcoder
8
+ include GemLogger::LoggerSupport
9
+
10
+ def initialize(item)
11
+ super(item)
12
+ self.source_filename = item.delegate.medias.first.parts.first.file
13
+ end
14
+
15
+ end
16
+
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,11 @@
1
+ require 'plex-ruby'
2
+ require_relative 'plex/movie'
3
+ require_relative 'plex/episode'
4
+ require_relative 'plex/season'
5
+ require_relative 'plex/show'
6
+ require_relative 'plex/category'
7
+ require_relative 'plex/qualified_category'
8
+ require_relative 'plex/section'
9
+ require_relative 'plex/application'
10
+ require_relative 'plex/metadata'
11
+ require_relative 'plex/transcoder'
@@ -0,0 +1,26 @@
1
+ module TivoHMO
2
+ module Adapters
3
+ module StreamIO
4
+
5
+ # Extracts some basic metadata using the streamio gem
6
+ class Metadata
7
+ include TivoHMO::API::Metadata
8
+ include GemLogger::LoggerSupport
9
+
10
+ attr_accessor :movie
11
+
12
+ def initialize(item)
13
+ super(item)
14
+ begin
15
+ self.movie = FFMPEG::Movie.new(item.identifier)
16
+ self.duration = movie.duration.to_i
17
+ rescue => e
18
+ logger.error "Failed to read movie metadata: #{e}"
19
+ end
20
+ end
21
+
22
+ end
23
+
24
+ end
25
+ end
26
+ end