royw-dvdprofiler2xbmc 0.0.5 → 0.0.6

Sign up to get free protection for your applications and to get access to all the features.
Files changed (42) hide show
  1. data/.gitignore +8 -0
  2. data/History.txt +5 -0
  3. data/Manifest.txt +33 -6
  4. data/PostInstall.txt +54 -3
  5. data/README.rdoc +38 -11
  6. data/Rakefile +1 -1
  7. data/bin/dvdprofiler2xbmc +1 -1
  8. data/dvdprofiler2xbmc.gemspec +7 -7
  9. data/lib/dvdprofiler2xbmc.rb +15 -7
  10. data/lib/dvdprofiler2xbmc/app_config.rb +15 -5
  11. data/lib/dvdprofiler2xbmc/{app.rb → controllers/app.rb} +6 -6
  12. data/lib/dvdprofiler2xbmc/controllers/fanart_controller.rb +73 -0
  13. data/lib/dvdprofiler2xbmc/controllers/nfo_controller.rb +280 -0
  14. data/lib/dvdprofiler2xbmc/controllers/thumbnail_controller.rb +70 -0
  15. data/lib/dvdprofiler2xbmc/extensions.rb +5 -2
  16. data/lib/dvdprofiler2xbmc/{collection.rb → models/collection.rb} +29 -7
  17. data/lib/dvdprofiler2xbmc/models/dvdprofiler_profile.rb +127 -0
  18. data/lib/dvdprofiler2xbmc/models/imdb_profile.rb +230 -0
  19. data/lib/dvdprofiler2xbmc/{media.rb → models/media.rb} +8 -75
  20. data/lib/dvdprofiler2xbmc/{media_files.rb → models/media_files.rb} +2 -3
  21. data/lib/dvdprofiler2xbmc/models/tmdb_movie.rb +136 -0
  22. data/lib/dvdprofiler2xbmc/models/tmdb_profile.rb +112 -0
  23. data/lib/dvdprofiler2xbmc/models/xbmc_info.rb +124 -0
  24. data/lib/dvdprofiler2xbmc/open_cache_extension.rb +39 -0
  25. data/lib/dvdprofiler2xbmc/views/cli.rb +171 -0
  26. data/spec/cache_extensions.rb +120 -0
  27. data/spec/dvdprofiler2xbmc_spec.rb +101 -0
  28. data/spec/dvdprofiler_profile_spec.rb +51 -0
  29. data/spec/imdb_profile_spec.rb +60 -0
  30. data/spec/nfo_controller_spec.rb +76 -0
  31. data/spec/samples/Collection.xml +273964 -0
  32. data/spec/samples/Die Hard - 1988.nfo +264 -0
  33. data/spec/samples/The Egg and I.dummy b/data/spec/samples/The Egg and → I.dummy +0 -0
  34. data/spec/spec.opts +1 -0
  35. data/spec/spec_helper.rb +18 -0
  36. data/spec/tmdb_movie_spec.rb +84 -0
  37. data/spec/tmdb_profile_spec.rb +105 -0
  38. data/spec/xbmc_info_spec.rb +68 -0
  39. data/tasks/rspec.rake +21 -0
  40. metadata +35 -11
  41. data/lib/dvdprofiler2xbmc/cli.rb +0 -128
  42. data/lib/dvdprofiler2xbmc/nfo.rb +0 -240
@@ -0,0 +1,127 @@
1
+ # This is the model for the DVD Profiler profile which is used
2
+ # to find meta data from DVD Profiler's exported Collection.xml
3
+ #
4
+ # Usage:
5
+ #
6
+ # profiles = DvdprofilerProfile.all(:titles => ['The Alamo'])
7
+ #
8
+ # profile = DvdprofilerProfile.first(:isbn => '012345678901')
9
+ # or
10
+ # profile = DvdprofilerProfile.first(:title => 'movie title')
11
+ #
12
+ # puts profile.dvd_hash[:key]
13
+ # puts profile.to_xml
14
+ # puts profile.isbn
15
+ # puts profile.title
16
+ # profile.save(media.path_to(:dvdprofiler_xml_extension))
17
+ #
18
+ class DvdprofilerProfile
19
+
20
+ # options:
21
+ # :isbn => String
22
+ # :title => String
23
+ # returns: Array of DvdprofilerProfile instances
24
+ def self.all(options={})
25
+ # :isbn_dvd_hash, :title_isbn_hash, :isbn_title_hash
26
+ result = []
27
+
28
+ # try finding by isbn first
29
+ if options.has_key?(:isbn) && !options[:isbn].blank?
30
+ dvd_hash = collection.isbn_dvd_hash[options[:isbn]]
31
+ unless dvd_hash.blank?
32
+ result << DvdprofilerProfile.new(dvd_hash, options[:isbn])
33
+ end
34
+ end
35
+
36
+ # if unable to find by isbn, then try finding by title
37
+ if result.empty? && options.has_key?(:title)
38
+ isbns = self.find_isbns(options)
39
+ unless isbns.blank?
40
+ isbns.each do |isbn|
41
+ dvd_hash = collection.isbn_dvd_hash[isbn]
42
+ unless dvd_hash.blank?
43
+ result << DvdprofilerProfile.new(dvd_hash, isbn, options[:title])
44
+ end
45
+ end
46
+ end
47
+ end
48
+
49
+ # return all profiles if neither :isbn nor :title are given
50
+ if result.empty? && !options.has_key?(:isbn) && !options.has_key?(:title)
51
+ collection.isbn_dvd_hash.each do |isbn, dvd_hash|
52
+ result << DvdprofilerProfile.new(dvd_hash, isbn)
53
+ end
54
+ end
55
+
56
+ result
57
+ end
58
+
59
+ # options:
60
+ # :isbn => String
61
+ # :title => String
62
+ # returns: DvdprofilerProfile instance or nil
63
+ def self.first(options={})
64
+ all(options).first
65
+ end
66
+
67
+ # look up ISBN by title
68
+ # expects a :title option
69
+ # returns Array of ISBN Strings
70
+ def self.find_isbns(options={})
71
+ result = []
72
+ if options.has_key?(:title)
73
+ result = [collection.title_isbn_hash[Collection.title_pattern(options[:title])]].flatten.uniq.compact
74
+ end
75
+ result
76
+ end
77
+
78
+ protected
79
+
80
+ def self.collection
81
+ @collection ||= Collection.new(File.expand_path(AppConfig[:collection_filespec]))
82
+ end
83
+
84
+ def initialize(dvd_hash, isbn, title=nil)
85
+ @dvd_hash = dvd_hash
86
+ @isbn = isbn
87
+ @title = title
88
+ @title ||= @dvd_hash[:title]
89
+ end
90
+
91
+ public
92
+
93
+ attr_reader :isbn, :title, :dvd_hash
94
+
95
+ def to_xml
96
+ data = @dvd_hash.stringify_keys
97
+ data.delete_if { |key, value| value.nil? }
98
+ xml = XmlSimple.xml_out(data, 'NoAttr' => true, 'RootName' => 'movie')
99
+ end
100
+
101
+ def save(filespec)
102
+ begin
103
+ xml = self.to_xml
104
+ unless xml.blank?
105
+ AppConfig[:logger].debug { "saving #{filespec}" }
106
+ save_to_file(filespec, xml)
107
+ end
108
+ rescue Exception => e
109
+ AppConfig[:logger].error { "Unable to save dvdprofiler profile to #{filespec} - #{e.to_s}" }
110
+ end
111
+ end
112
+
113
+ protected
114
+
115
+ def save_to_file(filespec, data)
116
+ new_filespec = filespec + AppConfig[:new_extension]
117
+ File.open(new_filespec, "w") do |file|
118
+ file.puts(data)
119
+ end
120
+ backup_filespec = filespec + AppConfig[:backup_extension]
121
+ File.delete(backup_filespec) if File.exist?(backup_filespec)
122
+ File.rename(filespec, backup_filespec) if File.exist?(filespec)
123
+ File.rename(new_filespec, filespec)
124
+ File.delete(new_filespec) if File.exist?(new_filespec)
125
+ end
126
+
127
+ end
@@ -0,0 +1,230 @@
1
+ # This is the model for the IDMB profile which is used
2
+ # to find ImdbMovie meta data from either online or from
3
+ # a cached file.
4
+ #
5
+ # Usage:
6
+ #
7
+ # profiles = ImdbProfile.all(:titles => ['The Alamo'])
8
+ #
9
+ # profile = ImdbProfile.first(:imdb_id => 'tt0123456')
10
+ # or
11
+ # profile = ImdbProfile.first(:titles => ['movie title 1', 'movie title 2',...]
12
+ # :media_years => ['2000'],
13
+ # :production_years => ['1999'],
14
+ # :released_years => ['2002', '2008']
15
+ # :filespec => media.path_to(:imdb_xml_extension))
16
+ # puts profile.movie['key'].first
17
+ # puts profile.to_xml
18
+ # puts profile.imdb_id
19
+ #
20
+ class ImdbProfile
21
+
22
+ # options:
23
+ # :imdb_id => self.imdb_id,
24
+ # :titles => titles,
25
+ # :media_years => [@media.year.to_i],
26
+ # :production_years => @dvd_hash[:productionyear],
27
+ # :released_years => @dvd_hash[:released]
28
+ # returns Array of ImdbProfile instances
29
+ def self.all(options={})
30
+ AppConfig[:logger].info { "ImdbProfile.all(#{options.inspect})" }
31
+ result = []
32
+ if has_option?(options, :imdb_id) || (has_option?(options, :filespec) && File.exist?(options[:filespec]))
33
+ result << ImdbProfile.new(options[:imdb_id], options[:filespec])
34
+ elsif has_option?(options, :titles)
35
+ result += self.lookup(options[:titles],
36
+ options[:media_years],
37
+ options[:production_years],
38
+ options[:released_years]
39
+ ).collect{|ident| ImdbProfile.new(ident, options[:filespec])}
40
+ end
41
+ result
42
+ end
43
+
44
+ # options:
45
+ # :imdb_id => self.imdb_id,
46
+ # :titles => titles,
47
+ # :media_years => [@media.year.to_i],
48
+ # :production_years => @dvd_hash[:productionyear],
49
+ # :released_years => @dvd_hash[:released]
50
+ # returns ImdbProfile instance or nil
51
+ def self.first(options={})
52
+ self.all(options).first
53
+ end
54
+
55
+ protected
56
+
57
+ def self.has_option?(options, key)
58
+ options.has_key?(key) && !options[key].blank?
59
+ end
60
+
61
+ def initialize(ident, filespec=nil)
62
+ @imdb_id = ident
63
+ @filespec = filespec
64
+ load
65
+ end
66
+
67
+ public
68
+
69
+ attr_reader :imdb_id, :movie
70
+
71
+ def to_xml
72
+ xml = ''
73
+ unless @movie.blank?
74
+ @movie.delete_if { |key, value| value.nil? }
75
+ xml = XmlSimple.xml_out(@movie, 'NoAttr' => true, 'RootName' => 'movie')
76
+ end
77
+ xml
78
+ end
79
+
80
+ protected
81
+
82
+ # @movie keys => [:title, :directors, :poster_url, :tiny_poster_url, :poster,
83
+ # :rating, :cast_members, :writers, :year, :genres, :plot,
84
+ # :tagline, :aspect_ratio, :length, :release_date, :countries,
85
+ # :languages, :color, :company, :photos, :raw_title,
86
+ # :release_year, :also_known_as, :mpaa, :certifications]
87
+ # returns Hash or nil
88
+ def load
89
+ if !@filespec.blank? && File.exist?(@filespec)
90
+ AppConfig[:logger].debug { "loading movie filespec=> #{@filespec.inspect}" }
91
+ @movie = from_xml(open(@filespec).read)
92
+ elsif !@imdb_id.blank?
93
+ AppConfig[:logger].debug { "loading movie from imdb.com, filespec=> #{@filespec.inspect}" }
94
+ @movie = ImdbMovie.new(@imdb_id.gsub(/^tt/, '')).to_hash
95
+ @movie['id'] = 'tt' + @imdb_id.gsub(/^tt/, '') unless @movie.blank?
96
+ save(@filespec) unless @filespec.blank?
97
+ end
98
+ unless @movie.blank?
99
+ @imdb_id = @movie['id']
100
+ else
101
+ @movie = nil
102
+ end
103
+ end
104
+
105
+ def from_xml(xml)
106
+ begin
107
+ movie = XmlSimple.xml_in(xml)
108
+ rescue Exception => e
109
+ AppConfig[:logger].warn { "Error converting from xml: #{e.to_s}" }
110
+ movie = nil
111
+ end
112
+ movie
113
+ end
114
+
115
+ def save(filespec)
116
+ begin
117
+ xml = self.to_xml
118
+ unless xml.blank?
119
+ AppConfig[:logger].debug { "saving #{filespec}" }
120
+ save_to_file(filespec, xml)
121
+ end
122
+ rescue Exception => e
123
+ AppConfig[:logger].error "Unable to save imdb profile to #{filespec} - #{e.to_s}"
124
+ end
125
+ end
126
+
127
+ def save_to_file(filespec, data)
128
+ new_filespec = filespec + AppConfig[:new_extension]
129
+ File.open(new_filespec, "w") do |file|
130
+ file.puts(data)
131
+ end
132
+ backup_filespec = filespec + AppConfig[:backup_extension]
133
+ File.delete(backup_filespec) if File.exist?(backup_filespec)
134
+ File.rename(filespec, backup_filespec) if File.exist?(filespec)
135
+ File.rename(new_filespec, filespec)
136
+ File.delete(new_filespec) if File.exist?(new_filespec)
137
+ end
138
+
139
+ # lookup IMDB title using years as the secondary search key
140
+ # the titles should behave as an Array, the intent here is to be
141
+ # able to try to find the exact title from DVD Profiler and if that
142
+ # fails, to try to find the title pattern
143
+ # The search order is:
144
+ # 1) media_years should be from media filename
145
+ # 2) production years
146
+ # 3) production years plus/minus a year
147
+ # 4) released years
148
+ # 5) released years plus/minus a year
149
+ # 6) no years
150
+ def self.lookup(titles, media_years, production_years, released_years)
151
+ AppConfig[:logger].info { "lookup(#{titles.inspect}, #{media_years.inspect}, #{production_years.inspect}, #{released_years.inspect})" }
152
+ idents = []
153
+ year_sets = []
154
+ year_sets << media_years unless media_years.blank?
155
+ year_sets << fuzzy_years(production_years, 0)
156
+ year_sets << fuzzy_years(production_years, -1..1)
157
+ year_sets << fuzzy_years(released_years, 0)
158
+ year_sets << fuzzy_years(released_years, -1..1)
159
+ year_sets << [] if media_years.blank?
160
+
161
+ titles.flatten.uniq.compact.each do |title|
162
+ [false, true].each do |search_akas|
163
+ AppConfig[:logger].debug { (search_akas ? 'Search AKAs' : 'Do not search AKAs') }
164
+ imdb_search = ImdbSearch.new(title, search_akas)
165
+ @cache ||= {}
166
+ imdb_search.set_cache(@cache)
167
+
168
+ year_sets.each do |years|
169
+ new_idents = find_id(imdb_search, title, years, search_akas)
170
+ AppConfig[:logger].debug { "new_idents => #{new_idents.inspect}" }
171
+ idents += new_idents
172
+ break unless new_idents.blank?
173
+ end
174
+ break unless idents.blank?
175
+ end
176
+ break unless idents.blank?
177
+ end
178
+ idents.uniq.compact
179
+ end
180
+
181
+ # Different databases seem to mix up released versus production years.
182
+ # So we combine both into a Array of integer years.
183
+ # fuzzy is an integer range, basically expand each known year by the fuzzy range
184
+ # i.e., let production and released year both be 2000 and fuzzy=-1..1,
185
+ # then the returned years would be [1999, 2000, 2001]
186
+ def self.fuzzy_years(source_years, fuzzy)
187
+ years = []
188
+ unless source_years.blank?
189
+ years = [source_years].flatten.collect do |date|
190
+ a = []
191
+ if date.to_s =~ /(\d{4})/
192
+ y = $1.to_i
193
+ a = [*fuzzy].collect do
194
+ |f| y.to_i + f
195
+ end
196
+ end
197
+ a
198
+ end
199
+ end
200
+ result = years.flatten.uniq.compact.sort
201
+ AppConfig[:logger].debug {"fuzzy_years(#{source_years}, #{fuzzy}) => #{result.join(', ')}"}
202
+ result
203
+ end
204
+
205
+ # try to find the imdb id for the movie
206
+ def self.find_id(imdb_search, title, years, search_akas)
207
+ idents = []
208
+
209
+ AppConfig[:logger].info { "Searching IMDB for \"#{title}\" (#{years.join(", ")})" }
210
+ unless title.blank?
211
+ begin
212
+ movies = imdb_search.movies
213
+ AppConfig[:logger].debug { "movies => (#{movies.collect{|m| [m.id, m.year, m.title]}.inspect})"}
214
+ if movies.size == 1
215
+ idents = [movies.first.id.to_s]
216
+ elsif movies.size > 1
217
+ AppConfig[:logger].debug { "years => #{years.inspect}"}
218
+ same_year_movies = movies.select{ |m| !m.year.blank? && years.include?(m.year.to_i) }
219
+ idents = same_year_movies.collect{|m| m.id.to_s}
220
+ AppConfig[:logger].debug { "same_year_movies => (#{same_year_movies.collect{|m| [m.id, m.year, m.title]}.inspect})"}
221
+ end
222
+ rescue Exception => e
223
+ AppConfig[:logger].error { "Error searching IMDB - " + e.to_s }
224
+ AppConfig[:logger].error { e.backtrace.join("\n") }
225
+ end
226
+ end
227
+ AppConfig[:logger].debug { "IMDB id => #{idents.join(', ')}" } unless idents.blank?
228
+ idents
229
+ end
230
+ end
@@ -1,47 +1,25 @@
1
1
  # == Synopsis
2
2
  # Media encapsulates information about a single media file
3
3
  class Media
4
- attr_reader :media_path, :image_files, :year, :media_subdirs, :title, :title_with_year
4
+ attr_reader :media_path, :image_files, :fanart_files, :year, :media_subdirs, :title, :title_with_year
5
+ attr_accessor :isbn, :imdb_id
5
6
 
6
7
  DISC_NUMBER_REGEX = /\.(cd|part|disk|disc)\d+/i
7
8
 
8
- def initialize(directory, media_file, collection)
9
- @collection = collection
9
+ def initialize(directory, media_file)
10
10
  @media_subdirs = File.dirname(media_file)
11
11
  @media_path = File.expand_path(File.join(directory, media_file))
12
+
13
+ cwd = File.expand_path(Dir.getwd)
12
14
  Dir.chdir(File.dirname(@media_path))
13
15
  @nfo_files = Dir.glob("*.{#{AppConfig[:nfo_extensions].join(',')}}")
14
16
  @image_files = Dir.glob("*.{#{AppConfig[:thumbnail_extension]}}")
17
+ @fanart_files = Dir.glob("*fanart*}")
18
+ Dir.chdir(cwd)
19
+
15
20
  @year = $1 if File.basename(@media_path) =~ /\s\-\s(\d{4})/
16
21
  @title = find_title(@media_path)
17
22
  @title_with_year = find_title_with_year(@title, @year)
18
-
19
- @nfo = NFO.new(self, @collection)
20
- @loaded = false
21
- end
22
-
23
- # load existing meta-data
24
- def load
25
- @nfo.load
26
- @loaded = true
27
- end
28
-
29
- # update the meta-data and thumbnails
30
- def update
31
- load unless @loaded
32
- @nfo.update
33
- @nfo.save
34
- update_thumbnail
35
- end
36
-
37
- # return the ISBN or nil
38
- def isbn
39
- @nfo.isbn
40
- end
41
-
42
- # return the IMDB ID or nil
43
- def imdb_id
44
- @nfo.imdb_id
45
23
  end
46
24
 
47
25
  # return a path to a file file based on the media's filespec
@@ -71,51 +49,6 @@ class Media
71
49
 
72
50
  protected
73
51
 
74
- # update the movie's thumbnail (.tbn) image
75
- def update_thumbnail
76
- if @nfo.isbn.blank?
77
- unless @nfo.imdb_id.blank?
78
- if @image_files.empty?
79
- fetch_imdb_thumbnail(@nfo.imdb_id)
80
- end
81
- end
82
- else
83
- copy_thumbnail(@nfo.isbn)
84
- end
85
- end
86
-
87
- # fetch the thumbnail from IMDB and save as path_to('tbn')
88
- def fetch_imdb_thumbnail(imdb_id)
89
- imdb_movie = ImdbMovie.new(imdb_id.gsub(/^tt/, ''))
90
- source_uri = imdb_movie.poster.image
91
- dest_image_filespec = path_to(:thumbnail_extension)
92
- puts "fetch_imdb_thumbnail(#{imdb_id}) => #{source_uri}"
93
- begin
94
- File.open(dest_image_filespec, "wb") {|f| f.write(open(source_uri).read)}
95
- rescue Exception => e
96
- AppConfig[:logger].error { "Error downloading image from \"#{source_uri}\" to \"#{dest_image_filespec}\" - #{e.to_s}" }
97
- end
98
- end
99
-
100
- # copy images from .../isbn.jpg to .../basename.jpg
101
- def copy_thumbnail(isbn)
102
- src_image_filespec = File.join(AppConfig[:images_dir], "#{isbn}f.jpg")
103
- if File.exist?(src_image_filespec)
104
- dest_image_filespec = path_to(:thumbnail_extension)
105
- do_copy = true
106
- if File.exist?(dest_image_filespec)
107
- if File.mtime(src_image_filespec) <= File.mtime(dest_image_filespec)
108
- do_copy = false
109
- end
110
- end
111
- begin
112
- File.copy(src_image_filespec, dest_image_filespec) if do_copy
113
- rescue Exception => e
114
- AppConfig[:logger].error {e.to_s}
115
- end
116
- end
117
- end
118
-
119
52
  # return the media's title extracted from the filename and cleaned up
120
53
  def find_title(media_path)
121
54
  # ditch extensions including disc number (ex, a.part2.b => a, a.cd1.b => a)