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
@@ -5,8 +5,7 @@ class MediaFiles
5
5
 
6
6
  # given:
7
7
  # directories Array of String directory pathspecs
8
- def initialize(directories, collection)
9
- @collection = collection
8
+ def initialize(directories)
10
9
  @medias = find_medias(directories)
11
10
  @titles = find_titles(@medias)
12
11
  @duplicate_titles = find_duplicate_titles(@titles)
@@ -20,7 +19,7 @@ class MediaFiles
20
19
  directories.each do |dir|
21
20
  Dir.chdir(dir)
22
21
  medias += Dir.glob("**/*.{#{AppConfig[:media_extensions].join(',')}}").collect do |filename|
23
- Media.new(dir, filename, @collection)
22
+ Media.new(dir, filename)
24
23
  end
25
24
  end
26
25
  medias
@@ -0,0 +1,136 @@
1
+ class TmdbMovie
2
+
3
+ attr_reader :query, :document
4
+
5
+ API_KEY = '7a2f6eb9b6aa01651000f0a9324db835'
6
+
7
+ def initialize(ident)
8
+ @imdb_id = 'tt' + ident.gsub(/^tt/, '') unless ident.blank?
9
+ @query = "http://api.themoviedb.org/2.0/Movie.imdbLookup?imdb_id=#{@imdb_id}&api_key=#{API_KEY}"
10
+ end
11
+
12
+ def fanarts
13
+ result = []
14
+ begin
15
+ document['moviematches'].each do |moviematches|
16
+ moviematches['movie'].each do |movie|
17
+ backdrop = movie['backdrop']
18
+ unless backdrop.blank?
19
+ result += backdrop
20
+ end
21
+ end
22
+ end
23
+ rescue
24
+ end
25
+ result
26
+ end
27
+
28
+ def posters
29
+ result = []
30
+ begin
31
+ document['moviematches'].each do |moviematches|
32
+ moviematches['movie'].each do |movie|
33
+ result += movie['poster']
34
+ end
35
+ end
36
+ rescue
37
+ end
38
+ result
39
+ end
40
+
41
+ def idents
42
+ document['moviematches'].first['movie'].first['id'] rescue nil
43
+ end
44
+
45
+ def urls
46
+ document['moviematches'].first['movie'].first['url'] rescue nil
47
+ end
48
+
49
+ def imdb_ids
50
+ document['moviematches'].first['movie'].first['imdb'] rescue nil
51
+ end
52
+
53
+ def titles
54
+ document['moviematches'].first['movie'].first['title'] rescue nil
55
+ end
56
+
57
+ def short_overviews
58
+ document['moviematches'].first['movie'].first['short_overview'] rescue nil
59
+ end
60
+
61
+ def types
62
+ document['moviematches'].first['movie'].first['type'] rescue nil
63
+ end
64
+
65
+ def alternative_titles
66
+ document['moviematches'].first['movie'].first['alternative_title'] rescue nil
67
+ end
68
+
69
+ def releases
70
+ document['moviematches'].first['movie'].first['release'] rescue nil
71
+ end
72
+
73
+ def scores
74
+ document['moviematches'].first['movie'].first['score'] rescue nil
75
+ end
76
+
77
+ def to_hash
78
+ hash = {}
79
+ [:fanarts, :posters, :idents, :urls, :imdb_ids, :titles, :short_overviews,
80
+ :types, :alternative_titles, :releases, :scores
81
+ ].each do |sym|
82
+ begin
83
+ value = send(sym.to_s)
84
+ hash[sym.to_s] = value unless value.nil?
85
+ rescue Exception => e
86
+ puts "Error getting data for hash for #{sym} - #{e.to_s}"
87
+ end
88
+ end
89
+ hash
90
+ end
91
+
92
+ def to_xml
93
+ XmlSimple.xml_out(document, 'NoAttr' => true, 'RootName' => 'movie')
94
+ end
95
+
96
+ def to_yaml
97
+ YAML.dump(document)
98
+ end
99
+
100
+ private
101
+
102
+ # Fetch the document with retry to handle the occasional glitches
103
+ def document
104
+ if @document.nil?
105
+ html = fetch(self.query)
106
+ @document = XmlSimple.xml_in(html)
107
+ @document = nil if @document['totalResults'] == ['0']
108
+ end
109
+ @document
110
+ end
111
+
112
+ MAX_ATTEMPTS = 3
113
+ SECONDS_BETWEEN_RETRIES = 1.0
114
+
115
+ def fetch(page)
116
+ doc = nil
117
+ attempts = 0
118
+ begin
119
+ doc = read_page(page)
120
+ rescue Exception => e
121
+ attempts += 1
122
+ if attempts > MAX_ATTEMPTS
123
+ raise
124
+ else
125
+ sleep SECONDS_BETWEEN_RETRIES
126
+ retry
127
+ end
128
+ end
129
+ doc
130
+ end
131
+
132
+ def read_page(page)
133
+ open(page).read
134
+ end
135
+
136
+ end
@@ -0,0 +1,112 @@
1
+ # This is the model for the themovieDb profile which is used
2
+ # to find TmdbMovie meta data from either online or from
3
+ # a cached file.
4
+ #
5
+ # Usage:
6
+ #
7
+ # profile = TmdbProfile.first(:imdb_id => 'tt0123456')
8
+ #
9
+ # puts profile.movie['key'].first
10
+ # puts profile.to_xml
11
+ # puts profile.imdb_id
12
+ #
13
+ class TmdbProfile
14
+
15
+ # options:
16
+ # :imdb_id => String (either with or without leading 'tt')
17
+ def self.all(options={})
18
+ result = []
19
+ if has_option?(options, :imdb_id)
20
+ result << TmdbProfile.new(options[:imdb_id], options[:filespec])
21
+ end
22
+ result
23
+ end
24
+
25
+ def self.first(options={})
26
+ self.all(options).first
27
+ end
28
+
29
+ # this is intended to be stubed by rspec where it
30
+ # should return true.
31
+ def self.use_html_cache
32
+ false
33
+ end
34
+
35
+ protected
36
+
37
+ def self.has_option?(options, key)
38
+ options.has_key?(key) && !options[key].blank?
39
+ end
40
+
41
+ def initialize(ident, filespec=nil)
42
+ @imdb_id = ident
43
+ @filespec = filespec
44
+ load
45
+ end
46
+
47
+
48
+ public
49
+
50
+ attr_reader :imdb_id, :movie
51
+
52
+ def to_xml
53
+ xml = ''
54
+ unless @movie.blank?
55
+ @movie.delete_if { |key, value| value.nil? }
56
+ xml = XmlSimple.xml_out(@movie, 'NoAttr' => true, 'RootName' => 'movie')
57
+ end
58
+ xml
59
+ end
60
+
61
+ protected
62
+
63
+ def load
64
+ @movie = nil
65
+ if !@filespec.blank? && File.exist?(@filespec)
66
+ AppConfig[:logger].debug { "loading movie filespec=> #{@filespec.inspect}" }
67
+ @movie = from_xml(open(@filespec).read)
68
+ elsif !@imdb_id.blank?
69
+ AppConfig[:logger].debug { "loading movie from tmdb.com, filespec=> #{@filespec.inspect}" }
70
+ @movie = TmdbMovie.new(@imdb_id.gsub(/^tt/, '')).to_hash
71
+ save(@filespec) unless @filespec.blank?
72
+ end
73
+ if @movie.blank?
74
+ @movie = nil
75
+ end
76
+ end
77
+
78
+ def from_xml(xml)
79
+ begin
80
+ movie = XmlSimple.xml_in(xml)
81
+ rescue Exception => e
82
+ AppConfig[:logger].warn { "Error converting from xml: #{e.to_s}" }
83
+ movie = nil
84
+ end
85
+ movie
86
+ end
87
+
88
+ def save(filespec)
89
+ begin
90
+ xml = self.to_xml
91
+ unless xml.blank?
92
+ AppConfig[:logger].debug { "saving #{filespec}" }
93
+ save_to_file(filespec, xml)
94
+ end
95
+ rescue Exception => e
96
+ AppConfig[:logger].error "Unable to save tmdb profile to #{filespec} - #{e.to_s}"
97
+ end
98
+ end
99
+
100
+ def save_to_file(filespec, data)
101
+ new_filespec = filespec + AppConfig[:new_extension]
102
+ File.open(new_filespec, "w") do |file|
103
+ file.puts(data)
104
+ end
105
+ backup_filespec = filespec + AppConfig[:backup_extension]
106
+ File.delete(backup_filespec) if File.exist?(backup_filespec)
107
+ File.rename(filespec, backup_filespec) if File.exist?(filespec)
108
+ File.rename(new_filespec, filespec)
109
+ File.delete(new_filespec) if File.exist?(new_filespec)
110
+ end
111
+
112
+ end
@@ -0,0 +1,124 @@
1
+ # This is the model for the XBMC's Info profile which is used
2
+ # to manage a .nfo file
3
+ #
4
+ # Usage:
5
+ #
6
+ # profile = XbmcInfo.new(media.path_to(:nfo_extension))
7
+ #
8
+ # profile.movie['key'] = 'some value'
9
+ # puts profile.movie['key']
10
+ # puts profile.to_xml
11
+ # puts profile.save
12
+ #
13
+ class XbmcInfo
14
+
15
+ FILTER_HTML = /<[^>]*>/
16
+
17
+ def initialize(filespec)
18
+ @nfo_filespec = filespec
19
+ @movie = nil
20
+ @original_movie = nil
21
+ load
22
+ end
23
+
24
+ def movie
25
+ @movie ||= Hash.new
26
+ @movie
27
+ end
28
+
29
+ def movie=(other)
30
+ @movie = other
31
+ end
32
+
33
+ # convert the @movie hash into xml and return the xml as a String
34
+ def to_xml
35
+ xml = ''
36
+ begin
37
+ unless @movie.blank?
38
+ data = @movie.dup
39
+ data.delete_if { |key, value| value.nil? }
40
+ %w(plot tagline overview).each do |key|
41
+ if data[key].respond_to?('first')
42
+ data[key] = data[key].first
43
+ end
44
+ data[key] = data[key].gsub(FILTER_HTML, '') unless data[key].blank?
45
+ end
46
+ xml = XmlSimple.xml_out(data, 'NoAttr' => true, 'RootName' => 'movie')
47
+ end
48
+ rescue Exception => e
49
+ AppConfig[:logger].error { "Error creating nfo file - " + e.to_s}
50
+ raise e
51
+ end
52
+ xml
53
+ end
54
+
55
+ def save
56
+ begin
57
+ if dirty?
58
+ xml = self.to_xml
59
+ unless xml.blank?
60
+ AppConfig[:logger].info { "updated #{@nfo_filespec}"}
61
+ save_to_file(@nfo_filespec, xml)
62
+ end
63
+ end
64
+ rescue Exception => e
65
+ AppConfig[:logger].error "Unable to save xbmc info to #{@nfo_filespec} - #{e.to_s}"
66
+ end
67
+ end
68
+
69
+ protected
70
+
71
+ # load the .nfo file into the @movie hash
72
+ def load
73
+ begin
74
+ if File.exist?(@nfo_filespec) && (File.size(@nfo_filespec) > 1)
75
+ File.open(@nfo_filespec) do |file|
76
+ @movie = XmlSimple.xml_in(file)
77
+ @original_movie = @movie.dup
78
+ end
79
+ end
80
+ rescue Exception => e
81
+ AppConfig[:logger].error { "Error loading \"#{@nfo_filespec}\" - " + e.to_s + "\n" + e.backtrace.join("\n") }
82
+ raise e
83
+ end
84
+ end
85
+
86
+ def save_to_file(filespec, data)
87
+ new_filespec = filespec + AppConfig[:new_extension]
88
+ File.open(new_filespec, "w") do |file|
89
+ file.puts(data)
90
+ end
91
+ backup_filespec = filespec + AppConfig[:backup_extension]
92
+ File.delete(backup_filespec) if File.exist?(backup_filespec)
93
+ File.rename(filespec, backup_filespec) if File.exist?(filespec)
94
+ File.rename(new_filespec, filespec)
95
+ File.delete(new_filespec) if File.exist?(new_filespec)
96
+ end
97
+
98
+ # has any of the data changed?
99
+ def dirty?
100
+ result = false
101
+ if @original_movie.nil?
102
+ result = true
103
+ else
104
+ @movie.each do |key, value|
105
+ if @original_movie[key].nil?
106
+ result = true
107
+ break
108
+ end
109
+ if @movie[key].to_s != @original_movie[key].to_s
110
+ result = true
111
+ break
112
+ end
113
+ end
114
+ unless result
115
+ diff_keys = @movie.keys.sort - @original_movie.keys.sort
116
+ unless diff_keys.empty?
117
+ result = true
118
+ end
119
+ end
120
+ end
121
+ result
122
+ end
123
+
124
+ end
@@ -0,0 +1,39 @@
1
+
2
+ #
3
+ class Kernel
4
+ attr_accessor :html_cache_dir
5
+ @html_cache_dir = '/tmp'
6
+
7
+ # cache any files read using http protocol
8
+ def open_cache(url)
9
+ if url =~ /^https?:\/\//i
10
+ filespec = url.gsub(/^http:\//, @html_cache_dir).gsub(/\/$/, '.html')
11
+ begin
12
+ fh = open(filespec)
13
+ rescue Exception
14
+ fh = open(url)
15
+ cache_html_files(filespec, fh.read)
16
+ fh.rewind
17
+ end
18
+ else
19
+ fh = open(url)
20
+ end
21
+ fh
22
+ end
23
+
24
+ private
25
+
26
+ # this is used to save imdb pages so they may be used by rspec
27
+ def cache_html_files(filespec, html)
28
+ begin
29
+ unless File.exist?(filespec)
30
+ puts "caching #{filespec}"
31
+ File.mkdirs(File.dirname(filespec))
32
+ File.open(filespec, 'w') { |f| f.puts html }
33
+ end
34
+ rescue Exception => eMsg
35
+ puts eMsg.to_s
36
+ end
37
+ end
38
+
39
+ end
@@ -0,0 +1,171 @@
1
+ require 'commandline/optionparser'
2
+ include CommandLine
3
+
4
+ # Command Line interface for the Dvdprofiler2Xbmc application.
5
+ # All application output is via AppConfig[:logger] so we have
6
+ # to set up the logger here.
7
+ # Also handle the command line options.
8
+ # Finally creates an instance of Dvdprofiler2Xbmc and executes
9
+ # it.
10
+
11
+ module Dvdprofiler2xbmc
12
+ # == Synopsis
13
+ # Command line exit codes
14
+ class ExitCode
15
+ UNKNOWN = 3
16
+ CRITICAL = 2
17
+ WARNING = 1
18
+ OK = 0
19
+ end
20
+
21
+ class CLI
22
+ include AppConfig
23
+
24
+ def self.execute(stdout, arguments=[])
25
+ exit_code = ExitCode::OK
26
+
27
+ # we start a STDOUT logger, but it will be switched after
28
+ # the config files are read if config[:logger_output] is set
29
+
30
+ begin
31
+ # trap ^C interrupts and let the app instance cleanly exit any long loops
32
+ Signal.trap("INT") {DvdProfiler2Xbmc.interrupt}
33
+
34
+ logger = setup_logger
35
+
36
+ # parse the command line
37
+ options = setup_parser()
38
+ od = options.parse(arguments)
39
+
40
+ setup_app_config(od, logger)
41
+
42
+ unless od["--help"] || od["--version"]
43
+ # create and execute class instance here
44
+ app = DvdProfiler2Xbmc.instance
45
+ app.execute
46
+ app.report.each {|line| AppConfig[:logger].info line}
47
+ end
48
+ rescue Exception => eMsg
49
+ logger.error {eMsg.to_s}
50
+ logger.error {options.to_s}
51
+ logger.error {eMsg.backtrace.join("\n")}
52
+ exit_code = ExitCode::CRITICAL
53
+ end
54
+ exit_code
55
+ end
56
+
57
+ def self.setup_app_config(od, logger)
58
+ # load config values
59
+ AppConfig.default
60
+
61
+ # the first reinitialize_logger adds the command line logging options to the default config
62
+ # then we load the config files
63
+ # then we run reinitialize_logger again to modify the logger for any logging options from the config files
64
+
65
+ reinitialize_logger(logger, od["--quiet"], od["--debug"])
66
+ AppConfig.load
67
+ AppConfig.save
68
+ AppConfig[:imdb_query] = !od["--no_imdb_query"]
69
+ AppConfig[:logfile] = od['--output'] if od['--output']
70
+ AppConfig[:logfile_level] = od['--output_level'] if od['--output_level']
71
+ reinitialize_logger(logger, od["--quiet"], od["--debug"])
72
+
73
+ AppConfig[:do_update] = !od["--reports"]
74
+ AppConfig[:force_nfo_replacement] = od["--force_nfo_replacement"]
75
+
76
+ AppConfig[:logger].info { "logfile => #{AppConfig[:logfile].inspect}" } unless AppConfig[:logfile].nil?
77
+ AppConfig[:logger].info { "logfile_level => #{AppConfig[:logfile_level].inspect}" } unless AppConfig[:logfile_level].nil?
78
+ end
79
+
80
+ # Setup the command line option parser
81
+ # Returns:: OptionParser instances
82
+ def self.setup_parser()
83
+ options = OptionParser.new()
84
+
85
+ # flag options
86
+ [
87
+ {
88
+ :names => %w(--version -v),
89
+ :opt_found => lambda {Log4r::Logger['dvdprofiler2xbmc'].info{"Dvdprofiler2xbmc #{Dvdprofiler2xbmc::VERSION}"}},
90
+ :opt_description => "This version of dvdprofiler2xbmc"
91
+ },
92
+ {
93
+ :names => %w(--help -h),
94
+ :opt_found => lambda {Log4r::Logger['dvdprofiler2xbmc'].info{options.to_s}},
95
+ :opt_description => "This usage information"
96
+ },
97
+ {
98
+ :names => %w(--no_imdb_query -n),
99
+ :opt_description => 'Do not query IMDB.com'
100
+ },
101
+ {
102
+ :names => %w(--quiet -q),
103
+ :opt_description => 'Display error messages only'
104
+ },
105
+ {
106
+ :names => %w(--debug -d),
107
+ :opt_description => 'Display debug messages'
108
+ },
109
+ {
110
+ :names => %w(--force_nfo_replacement -f),
111
+ :opt_description => 'Delete old .nfo files and generate new ones'
112
+ },
113
+ {
114
+ :names => %w(--reports -r),
115
+ :opt_description => 'Display reports only. Do not do any updates.'
116
+ }
117
+ ].each { |opt| options << Option.new(:flag, opt) }
118
+
119
+ # non-flag options
120
+ [
121
+ {
122
+ :names => %w(--output -o),
123
+ :argument_arity => [1,1],
124
+ :arg_description => 'logfile',
125
+ :opt_description => 'Write log messages to file. Default = no log file',
126
+ :opt_found => OptionParser::GET_ARGS
127
+ },
128
+ {
129
+ :names => %w(--output_level -l),
130
+ :argument_arity => [1,1],
131
+ :arg_description => 'level',
132
+ :opt_description => 'Output logging level: DEBUG, INFO, WARN, ERROR. Default = INFO',
133
+ :opt_found => OptionParser::GET_ARGS
134
+ }
135
+ ].each { |opt| options << Option.new(opt) }
136
+
137
+ options
138
+ end
139
+
140
+ # Initial setup of logger
141
+ def self.setup_logger
142
+ logger = Log4r::Logger.new('dvdprofiler2xbmc')
143
+ logger.outputters = Log4r::StdoutOutputter.new(:console)
144
+ Log4r::Outputter[:console].formatter = Log4r::PatternFormatter.new(:pattern => "%m")
145
+ logger.level = Log4r::DEBUG
146
+ logger
147
+ end
148
+
149
+ # Reinitialize the logger using the loaded config.
150
+ # logger:: logger for any user messages
151
+ # config:: is the application's config hash.
152
+ def self.reinitialize_logger(logger, quiet, debug)
153
+ # switch the logger to the one specified in the config files
154
+ unless AppConfig[:logfile].blank?
155
+ logfile_outputter = Log4r::RollingFileOutputter.new(:logfile, :filename => AppConfig[:logfile], :maxsize => 1000000 )
156
+ logger.add logfile_outputter
157
+ AppConfig[:logfile_level] ||= 'INFO'
158
+ Log4r::Outputter[:logfile].formatter = Log4r::PatternFormatter.new(:pattern => "[%l] %d :: %M")
159
+ level_map = {'DEBUG' => Log4r::DEBUG, 'INFO' => Log4r::INFO, 'WARN' => Log4r::WARN}
160
+ logfile_outputter.level = level_map[AppConfig[:logfile_level].upcase] || Log4r::INFO
161
+ end
162
+ Log4r::Outputter[:console].level = Log4r::INFO
163
+ Log4r::Outputter[:console].level = Log4r::WARN if quiet
164
+ Log4r::Outputter[:console].level = Log4r::DEBUG if debug
165
+ Log4r::Outputter[:console].formatter = Log4r::PatternFormatter.new(:pattern => "%m")
166
+ # logger.trace = true
167
+ AppConfig[:logger] = logger
168
+ end
169
+ end
170
+ end
171
+