apple-tv-converter 0.6.2 → 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,77 @@
1
+ module AppleTvConverter
2
+ module Metadata
3
+ class Imdb
4
+ def self.get_metadata(media, interactive = true, language = 'en')
5
+ printf "* Getting info from IMDB" if interactive
6
+
7
+ metadata_id = media.get_metadata_id(:imdb, :show)
8
+
9
+ if !metadata_id
10
+ search = ::Imdb::Search.new(media.show)
11
+
12
+ search.movies.delete_if do |item|
13
+ item.title.strip =~ /(?:(?:\(TV\s*(?:Movie|(?:Mini.?)?Series|Episode))|(?:Video(?:\s*Game)?))/i
14
+ end
15
+
16
+ metadata_id = if search.movies.length > 1 && interactive
17
+ choice = 0
18
+ puts "\n *"
19
+ while true
20
+ puts %Q[ | Several movies found, choose the intended one#{" (showing only the first 20 of #{search.movies.length} results)" if search.movies.length > 20}:]
21
+
22
+ search.movies[0...20].each_with_index do |item, index|
23
+ puts " | #{(index + 1).to_s.rjust(search.movies.length.to_s.length)} - #{item.title.strip} (id: #{item.id})"
24
+ if item.also_known_as.any?
25
+ akas = item.also_known_as[0...5].each do |aka|
26
+ puts " | #{' '.rjust(search.movies.length.to_s.length)} AKA: #{(aka.is_a?(Hash) ? aka[:title] : aka).strip}"
27
+ end
28
+ end
29
+ end
30
+
31
+ printf " |\n *- What's your choice (1..#{[search.movies.length, 20].min})? "
32
+ choice = STDIN.gets.chomp.to_i
33
+
34
+ break if choice.between?(1, [search.movies.length, 20].min)
35
+
36
+ puts " | Invalid choice!"
37
+ puts " |"
38
+ end
39
+
40
+ printf " * Getting info from IMDB"
41
+ search.movies[choice - 1].id
42
+ else
43
+ search.movies.first.id rescue nil
44
+ end
45
+ end
46
+
47
+ # begin
48
+ if metadata_id
49
+ imdb_movie = ::Imdb::Movie.new(metadata_id)
50
+
51
+ media.metadata.name = imdb_movie.title.gsub(/"/, '"')
52
+ media.metadata.genre = imdb_movie.genres.first.gsub(/"/, '"') if imdb_movie.genres.any?
53
+ media.metadata.description = imdb_movie.plot.gsub(/"/, '"') if imdb_movie.plot
54
+ media.release_date = imdb_movie.year if imdb_movie.year
55
+ media.metadata.director = (imdb_movie.director.first || '').gsub(/"/, '"') if imdb_movie.director.any?
56
+ media.metadata.codirector = imdb_movie.director[1].gsub(/"/, '"') if imdb_movie.director.length > 1
57
+ media.metadata.artwork = imdb_movie.poster if imdb_movie.poster
58
+
59
+ media.set_metadata_id :imdb, :show, metadata_id
60
+
61
+ puts " [DONE]" if interactive
62
+ end
63
+ # rescue OpenURI::HTTPError => e
64
+ # media.set_metadata_id :imdb, :show, nil
65
+ # media.imdb_movie = nil
66
+ # puts (e.message =~ /404/ ? " [NOT FOUND]" : " [ERROR]") if interactive
67
+ # rescue
68
+ # if media.get_metadata_id(:imdb, :show).nil?
69
+ # puts " [NOT FOUND]" if interactive
70
+ # else
71
+ # raise e
72
+ # end
73
+ # end
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,23 @@
1
+ module AppleTvConverter
2
+ module Metadata
3
+ class Info
4
+ attr_accessor :name, :genre, :description, :release_date
5
+ attr_accessor :tv_show, :tv_show_season, :tv_show_episode, :tv_network
6
+ attr_accessor :screenwriters, :director, :codirector
7
+ attr_accessor :artwork_filename
8
+
9
+ def initialize(media)
10
+ @media = media
11
+ end
12
+
13
+ def artwork ; @media.artwork_filename ; end
14
+ def artwork=(value) ; AppleTvConverter.copy value, @media.artwork_filename ; end
15
+
16
+ def sort_name ; return @media.is_tv_show_episode? ? "#{tv_show} S#{tv_show_season.to_s.rjust(2, '0')}E#{tv_show_episode.to_s.rjust(2, '0')}" : name ; end
17
+ def sort_album ; return tv_show ; end
18
+ def sort_album_artist ; return tv_show ; end
19
+ def sort_composer ; return tv_show ; end
20
+ def sort_show ; return "#{tv_show} Season #{tv_show_season.to_s.rjust(2, '0')}" ; end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,104 @@
1
+ module AppleTvConverter
2
+ module Metadata
3
+ class MovieDb
4
+ def self.get_metadata(media, interactive = true, language = 'en')
5
+ show_id = nil
6
+
7
+ if media.get_metadata_id(:tmdb, :show)
8
+ # We have an id, assume it for the search
9
+ show_id = media.get_metadata_id(:tmdb, :show)
10
+ else
11
+ printf "* Searching TheMovieDb.org "
12
+
13
+ # Query the data
14
+ results = search(media)
15
+ if results
16
+ puts "[DONE]"
17
+
18
+ if results[:total_results] > 0
19
+ if results[:total_results] == 1 || !interactive
20
+ # Only 1 result, or non-interactive, use the first result
21
+ show_id = results[:results].first[:id]
22
+ else
23
+ # More than one result, ask the user
24
+ choice = 0
25
+ puts "\n *"
26
+
27
+ while true
28
+ puts %Q[ | Several shows found, choose the intended one:]
29
+
30
+ results[:results].each_with_index do |item, index|
31
+ puts " | #{(index + 1).to_s.rjust(results[:total_results].to_s.length)} - #{item[:title]} (#{Date.parse(item[:release_date]).year}) (id: #{item[:id]})"
32
+ end
33
+
34
+ printf " |\n *- What's your choice (1..#{results[:results].length})? "
35
+ choice = STDIN.gets.chomp.to_i
36
+
37
+ break if choice.between?(1, results[:results].length)
38
+
39
+ puts " | Invalid choice!"
40
+ puts " |"
41
+ end
42
+
43
+ show_id = results[:results][choice - 1][:id]
44
+ end
45
+ else
46
+ # It's not found, return false to continue with other services
47
+ return false
48
+ end
49
+ else
50
+ puts "[ERROR]"
51
+ end
52
+ end
53
+
54
+ if show_id.to_i > 0
55
+ printf "* Fetching metadata from TheMovieDb.org "
56
+
57
+ # Fetch the detailed data
58
+ data = get(show_id)
59
+
60
+ if data
61
+ puts "[DONE]"
62
+ media.metadata.name = data[:title]
63
+ media.metadata.genre = data[:genres].first[:name]
64
+ media.metadata.description = data[:overview]
65
+ media.metadata.release_date = data[:release_date]
66
+ # media.metadata.screenwriters = data[:release_date]
67
+ # media.metadata.director = data[:release_date]
68
+ # media.metadata.codirector = data[:release_date]
69
+ media.metadata.artwork = poster_url(data[:poster_path])
70
+
71
+ media.release_date = Date.parse(media.metadata.release_date).year
72
+ media.set_metadata_id :tmdb, :show, show_id
73
+
74
+ return true
75
+ else
76
+ puts "[ERROR]"
77
+ end
78
+ end
79
+ end
80
+
81
+ private
82
+
83
+ def self.api_key; return '5ebb3f1009ddd14d244cbe1645b616a0' ; end
84
+ def self.base_url; return 'http://api.themoviedb.org/3/'; end
85
+ def self.build_url(path, params = {}) ; return "#{base_url}#{path}" ; end
86
+ def self.build_params(params = {}) ; return params.merge(:api_key => api_key) ; end
87
+ def self.poster_url(extra) ; return "#{configuration[:images][:base_url]}original#{extra}" ; end
88
+
89
+ def self.search(media) ; request 'search/movie', :query => media.show ; end
90
+ def self.get(id) ; request("movie/#{id}", :append_to_response => 'credits') ; end
91
+ def self.configuration ; @configuration ||= request('configuration') ; end
92
+
93
+ def self.request(url, params = {})
94
+ data = { :params => build_params(params), :accept => 'application/json', :block_response => true }
95
+ begin
96
+ return JSON.parse(RestClient.get(build_url(url), data), :symbolize_names => true, :symbolize_keys => true)
97
+ rescue => e
98
+ return nil
99
+ end
100
+ end
101
+ end
102
+ end
103
+ end
104
+
@@ -0,0 +1,303 @@
1
+ module AppleTvConverter
2
+ module Metadata
3
+ class TvDb
4
+ require 'httparty'
5
+ require 'yaml'
6
+ require 'net/http'
7
+ require 'zip/zip'
8
+ require 'xml'
9
+
10
+ include HTTParty
11
+
12
+ base_uri 'thetvdb.com/api'
13
+
14
+ def self.search(media, interactive = true, language = 'en')
15
+ get_updates_from_server
16
+
17
+ if media.get_metadata_id(:tvdb, :show)
18
+ show_id = media.get_metadata_id(:tvdb, :show)
19
+ else
20
+ data = load_config_file('show_ids') || {}
21
+
22
+ # http://thetvdb.com/api/GetSeries.php?seriesname=
23
+ unless data.has_key?(media.show)
24
+ printf "* Searching TheTVDB "
25
+
26
+ show_ids = get_and_parse_data_from_server('show_ids', '/GetSeries.php', { :query => { :seriesname => media.show } }, ['Data', 'Series']) do |loaded_data|
27
+ puts "[DONE]"
28
+ loaded_data = [loaded_data].flatten
29
+
30
+ data[media.show] = if loaded_data.length > 1 && interactive
31
+ choice = 0
32
+ puts "\n *"
33
+
34
+ while true
35
+ puts %Q[ | Several shows found, choose the intended one:]
36
+
37
+ loaded_data.each_with_index do |item, index|
38
+ puts " | #{(index + 1).to_s.rjust(loaded_data.length.to_s.length)} - #{item['SeriesName']} (id: #{item['seriesid']})"
39
+ puts " | #{' '.rjust(loaded_data.length.to_s.length)} AKA: #{item['AliasNames']}" if item['AliasNames']
40
+ end
41
+
42
+ printf " |\n *- What's your choice (1..#{loaded_data.length})? "
43
+ choice = STDIN.gets.chomp.to_i
44
+
45
+ break if choice.between?(1, loaded_data.length)
46
+
47
+ puts " | Invalid choice!"
48
+ puts " |"
49
+ end
50
+
51
+ loaded_data[choice - 1]['seriesid']
52
+ else
53
+ loaded_data.first['seriesid']
54
+ end
55
+
56
+ # Return the new list sorted by show name
57
+ Hash[data.sort]
58
+ end
59
+
60
+ puts "[NOT FOUND]" if show_ids.empty?
61
+ end
62
+
63
+ show_id = data[media.show]
64
+ end
65
+
66
+ if show_id.to_i > 0
67
+ printf "* Getting info from TheTVDB "
68
+
69
+ # <mirrorpath_zip>/api/<apikey>/series/<seriesid>/all/<language>.zip
70
+ show_data = get_data(show_id, "/#{api_key}/series/#{show_id}/all/#{language}.zip", { :zip => true }) do |data|
71
+ show_data = xml_document_to_hash(XML::Document.string(data[language.to_s].gsub(/>\s*</im, '><')))
72
+ banners = xml_document_to_hash(XML::Document.string(data['banners'].gsub(/>\s*</im, '><'))) rescue { 'Banner' => [] }
73
+ actors = xml_document_to_hash(XML::Document.string(data['actors'].gsub(/>\s*</im, '><'))) rescue { 'Actor' => [] }
74
+
75
+ {
76
+ :series => show_data['Series'],
77
+ :episodes => [show_data['Episode']].flatten,
78
+ :banners => [banners['Banner']].flatten,
79
+ :actors => [actors['Actor']].flatten
80
+ }
81
+ end
82
+
83
+ data = {
84
+ :episode => show_data[:episodes].detect do |ep|
85
+ # For season 1, check the absolute number first (for cartoons, etc.), and then check the usual season/episode combo
86
+ (media.season.to_i == 1 && ep['absolute_number'].to_i == media.number.to_i) || (ep['SeasonNumber'].to_i == media.season.to_i && ep['EpisodeNumber'].to_i == media.number.to_i)
87
+ end,
88
+ :show => show_data
89
+ }
90
+
91
+ if data[:episode]
92
+ media.metadata.tv_show = data[:show][:series]['SeriesName'] || media.show
93
+ media.metadata.tv_show_season = media.use_absolute_episode_numbering ? 1 : data[:episode]['SeasonNumber']
94
+ media.metadata.tv_show_episode = data[:episode][media.use_absolute_episode_numbering ? 'absolute_number' : 'EpisodeNumber']
95
+ media.metadata.tv_network = data[:show][:series]['Network']
96
+ media.metadata.name = data[:episode]['EpisodeName'] || "#{media.metadata.tv_show} S#{media.metadata.tv_show_season.to_s.rjust(2, '0')}E#{media.number.to_s.rjust(2, '0')}"
97
+ media.metadata.genre = data[:show][:series]['Genre'].gsub(/(?:^\|)|(?:\|$)/, '').split('|').first rescue nil
98
+ media.metadata.description = data[:episode]['Overview']
99
+ media.metadata.release_date = data[:episode]['FirstAired']
100
+ media.metadata.screenwriters = data[:episode]['Writer'].gsub(/(?:^\|)|(?:\|$)/, '').split('|').join(', ') rescue nil
101
+ media.metadata.director = data[:episode]['Director']
102
+ media.metadata.artwork = get_poster(media, data)
103
+
104
+ # Update some data in the media
105
+ set_metadata_id_if_not_set(:imdb, :show, data[:show][:series]['IMDB_ID']) rescue nil
106
+ media.release_date = media.metadata.release_date
107
+
108
+ # Update the episode name, if available
109
+ media.episode_title = data[:episode]['EpisodeName']
110
+ media.set_metadata_id :tvdb, :show, data[:episode]['seriesid']
111
+ media.set_metadata_id :tvdb, :season, data[:episode]['seasonid']
112
+ media.set_metadata_id :tvdb, :episode, data[:episode]['id']
113
+ media.set_metadata_id :imdb, :episode, data[:episode]['IMDB_ID']
114
+
115
+ puts "[DONE]"
116
+
117
+ return true
118
+ else
119
+ puts "[NOT FOUND]"
120
+ end
121
+ end
122
+
123
+ return false
124
+ end
125
+
126
+ def self.get_poster(media, data = nil)
127
+ local_file = File.join(AppleTvConverter.data_path, 'cache', 'tvdb', "#{media.tvdb_id}.jpg")
128
+
129
+ unless File.exists?(local_file)
130
+ artwork_filename = data[:show][:series]['poster'] || ''
131
+ artwork_filename = data[:episode]['filename'] || '' if artwork_filename.blank?
132
+ artwork_filename = "http://thetvdb.com/banners/#{artwork_filename}" if !artwork_filename.blank?
133
+
134
+ AppleTvConverter.copy artwork_filename, local_file unless artwork_filename.blank?
135
+ end
136
+
137
+ local_file
138
+ end
139
+
140
+ private
141
+
142
+ def self.api_key ; return '67FBF9F0670DBDF2' ; end
143
+ def self.local_cache_base_path
144
+ return File.expand_path(File.join(AppleTvConverter.data_path, 'cache', 'tvdb'))
145
+ end
146
+ def self.server_update_timestamp
147
+ @server_update_timestamp ||= load_config_file('update')
148
+
149
+ unless @server_update_timestamp
150
+ # http://thetvdb.com/api//Updates.php?type=none
151
+ @server_update_timestamp = get_data_from_server('/Updates.php', { :query => { :type => 'none' }})["Items"]["Time"] rescue nil
152
+ @server_update_timestamp = @server_update_timestamp.to_i unless @server_update_timestamp.nil?
153
+ save_config_file 'update', @server_update_timestamp
154
+ end
155
+
156
+ @server_update_timestamp
157
+ end
158
+
159
+ def self.load_config_file(filename)
160
+ full_filename = File.join(local_cache_base_path, filename =~ /\.yml$/ ? filename : "#{filename}.yml")
161
+ File.exists?(full_filename) ? YAML.load_file(full_filename) : nil
162
+ end
163
+
164
+ def self.save_config_file(filename, data)
165
+ full_filename = File.join(local_cache_base_path, filename =~ /\.yml$/ ? filename : "#{filename}.yml")
166
+ File.open(full_filename, 'w') { |f| f.write data.to_yaml }
167
+ end
168
+
169
+ def self.delete_config_file(filename)
170
+ full_filename = File.join(local_cache_base_path, filename =~ /\.yml$/ ? filename : "#{filename}.yml")
171
+ File.delete(full_filename) if File.exists?(full_filename)
172
+ end
173
+
174
+
175
+ def self.get_data_from_server(url, options = {})
176
+ AppleTvConverter.logger.debug " -> Getting from server: #{url}"
177
+ cache = options.delete(:cache) || true
178
+ zip = options.delete(:zip) || false
179
+ response = self.get(url, options).parsed_response
180
+
181
+ if zip
182
+ filename = File.join(local_cache_base_path, 'zip_file.zip')
183
+
184
+ begin
185
+ File.open(filename, 'wb') { |f| f.write response }
186
+ response = {}
187
+
188
+ Zip::ZipFile.open(filename) do |zipfile|
189
+ zipfile.each do |entry|
190
+ unless entry.name.downcase["__macosx"]
191
+ zip_data = zipfile.read(entry)
192
+ response[entry.name.to_s.gsub(/\.xml$/i, '')] = zip_data
193
+ end
194
+ end
195
+ end
196
+ rescue => e
197
+ ap [e, e.backtrace]
198
+
199
+ ensure
200
+ FileUtils.rm_f filename if File.exists?(filename)
201
+ end
202
+ end
203
+
204
+ return response
205
+ end
206
+
207
+ def self.get_data(filename, url, url_options, response_indexes = [])
208
+ AppleTvConverter.logger.debug "-> Getting data: #{filename}"
209
+ data = load_config_file(filename)
210
+
211
+ unless data
212
+ data = get_data_from_server(url, url_options)
213
+
214
+ if data
215
+ begin
216
+ response_indexes.each { |idx| data = data[idx] }
217
+
218
+ data = yield(data) if block_given?
219
+
220
+ save_config_file filename, data
221
+ rescue
222
+ data = nil
223
+ end
224
+ end
225
+ else
226
+ # ap ['found on cache', filename, data]
227
+ end
228
+
229
+ return data
230
+ end
231
+
232
+ def self.get_and_parse_data_from_server(filename, url, url_options, response_indexes = [])
233
+ cache = url_options.delete(:cache) || true
234
+ data = get_data_from_server(url, url_options)
235
+
236
+ if data
237
+ begin
238
+ response_indexes.each { |idx| data = data[idx] }
239
+
240
+ data = yield(data) if block_given?
241
+
242
+ save_config_file filename, data if cache
243
+ rescue
244
+ data = nil
245
+ end
246
+ end
247
+ end
248
+
249
+ def self.get_updates_from_server(options = {})
250
+ get_and_parse_data_from_server('updates', '/Updates.php', { :query => { :type => 'all', :time => load_config_file('update')}, :cache => false }, ['Items']) do |data|
251
+ if data
252
+ # Delete each show's cached data
253
+ data['Series'].each do |show_id|
254
+ delete_config_file show_id
255
+ delete_config_file "#{show_id}.jpg"
256
+ end
257
+
258
+ # Save the new timestamp
259
+ save_config_file 'update', data['Time']
260
+ @server_update_timestamp = data['Time']
261
+ end
262
+ end
263
+ end
264
+
265
+ def self.xml_document_to_hash(document)
266
+ def self.xml_node_to_hash(xml)
267
+ return nil if xml.children.empty?
268
+ return xml.children.first.to_s if xml.children.count == 1 && xml.children.first.text?
269
+
270
+ # Append a sequential number to the name to prevent replacing items that should be in an array
271
+ child_number = 0
272
+ Hash[*(xml.children.map { |child| child_number += 1 ; ["#{child.name}::#{child_number}", xml_node_to_hash(child)] }.compact.flatten(1))]
273
+ end
274
+
275
+ intermediate_hash = xml_node_to_hash(document.root)
276
+
277
+ return Hash[*(intermediate_hash.group_by do |obj|
278
+ obj.first.gsub(/::\d+$/, '')
279
+ end.map do |key, value|
280
+ # Remove the 'key' entries
281
+ value = value.flatten(1).delete_if { |v| v.to_s =~ /#{key}::\d+/ }
282
+
283
+ # Remove the sequential number from the keys
284
+ value.map! do |element|
285
+ Hash[*(element.map do |ikey, ivalue|
286
+ [ikey.gsub(/::\d+$/, ''), ivalue]
287
+ end.flatten(1))]
288
+ end
289
+
290
+ # If there's only one entry, remove the array
291
+ value = value.first if value.count == 1
292
+
293
+ [key, value]
294
+ end.flatten(1))]
295
+ end
296
+
297
+ FileUtils.mkdir_p local_cache_base_path
298
+
299
+ # Load the server timestamp on startup
300
+ server_update_timestamp
301
+ end
302
+ end
303
+ end