osdb 0.0.10 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/bin/getsub CHANGED
@@ -1,197 +1,159 @@
1
1
  #!/usr/bin/env ruby
2
- # TODO: getsub is becomming a bit to complexe, rewrite it OO or move it in another gem
3
2
  require 'optparse'
4
3
  require 'uri'
5
4
  require 'rubygems'
6
5
  require File.expand_path(File.join(File.dirname(__FILE__), '..', 'lib', 'osdb'))
7
6
 
8
- def env_lang
9
- OSDb::Language.from_locale(ENV['LANG'])
10
- end
11
7
 
12
- @options = {:language => env_lang.to_iso639_2b, :force => false, :dir => nil }
13
- @parser ||= OptionParser.new do |opts|
14
- opts.banner = "Automatically download subs for your video files using opensubtitle.org"
15
- opts.separator ""
16
- opts.separator "Usage: getsub [options] DIRECTORY | VIDEO_FILE [VIDEO_FILE ...]"
17
- opts.separator ""
18
- opts.separator "Main options:"
19
-
20
- opts.on("-l", "--language LANGUAGE", "Sub language ISO 639-2 code like fre or eng. Default: env $LANG (#{env_lang.to_iso639_2b})") do |language|
21
- if language.to_s.length != 3
22
- STDERR.puts "Language should specified as ISO 639-2 (ie, 3 letters, like 'eng' or 'fre')"
23
- exit 1
24
- end
25
- @options[:language] = language.to_s
26
- end
8
+ class GetSub
27
9
 
28
- opts.on("-d", "--directory DIRECTORY", "Specify a directory to search recursively for movies") do |dir|
29
- @options[:dir] = dir
30
- end
31
-
32
- opts.on("-f", "--force", "Download sub even if video already has one") { @options[:force] = true }
10
+ def initialize
11
+ @options = {:language => env_lang.to_iso639_2b, :force => false, :dir => nil, :methods => 'h'}
12
+ @parser ||= OptionParser.new do |opts|
13
+ opts.banner = "Automatically download subs for your video files using opensubtitle.org"
14
+ opts.separator ""
15
+ opts.separator "Usage: getsub [options] DIRECTORY | VIDEO_FILE [VIDEO_FILE ...]"
16
+ opts.separator ""
17
+ opts.separator "Main options:"
33
18
 
34
- opts.on("-t", "--type FORMATS", "Select only subtitles in specified formats. e.g -t srt,sub") { |formats| @options[:formats] = formats.to_s.split(',') }
19
+ opts.on("-a", "--auto", "Do not ask user to resolve hash conflicts.") { @options[:auto] = true }
35
20
 
36
- end
37
- @parser.parse!
21
+ opts.on("-l", "--language LANGUAGE", "Sub language ISO 639-2 code like fre or eng. Default: env $LANG (#{env_lang.to_iso639_2b})") do |language|
22
+ if language.to_s.length != 3
23
+ STDERR.puts "Invalid argument: Language should specified as ISO 639-2 (ie, 3 letters, like 'eng' or 'fre')"
24
+ exit 1
25
+ end
26
+ @options[:language] = language.to_s
27
+ end
38
28
 
39
- def curl_available?
40
- %x{ curl --version 2> /dev/null > /dev/null }
41
- $?.success?
42
- end
29
+ opts.on("-f", "--force", "Download sub even if video already has one") { @options[:force] = true }
43
30
 
44
- def wget_available?
45
- %x{ wget --version 2> /dev/null > /dev/null }
46
- $?.success?
47
- end
31
+ opts.on("-t", "--type FORMATS", "Select only subtitles in specified formats. e.g -t srt,sub") { |formats| @options[:formats] = formats.to_s.split(',') }
48
32
 
49
- def download!(url, local_path)
50
- puts "* download #{url} to #{local_path}"
51
- if curl_available?
52
- %x{ curl -s '#{url}' | gunzip > "#{local_path}" }
53
- elsif wget_available?
54
- %x{ wget -O - -q '#{url}' | gunzip > "#{local_path}"}
55
- else
56
- puts "Can't found any curl or wget please install one of them or manualy download your sub"
57
- puts url
58
- end
59
- end
33
+ methods_help = "Ordered list of search methods. h: by movie hash, i: by name on IMDB, n: by name on OSDb, p: by filename on OSDb. e.g -s hi . Default: h"
34
+ opts.on("-s", "--search-by METHODS", methods_help) do |methods|
35
+ unless methods =~ /^[hinp]+$/
36
+ STDERR.puts "Invalid argument: Available search methods are: h, i, n and p."
37
+ exit 1
38
+ end
39
+ @options[:methods] = methods
40
+ end
60
41
 
61
- def group_by_movie_name(subs)
62
- subs.inject({}) do |hash, sub|
63
- hash[sub.movie_name] ||= []
64
- hash[sub.movie_name] << sub
65
- hash
42
+ end
66
43
  end
67
- end
68
44
 
69
- def ask_user_to_identify_movie(movies)
70
- movies.keys.each_with_index do |name, index|
71
- puts " #{index} - #{name}"
45
+ def env_lang
46
+ OSDb::Language.from_locale(ENV['LANG'])
72
47
  end
73
- print 'id: '
74
- str = STDIN.gets # TODO: rule #1, never trust user input
75
- puts
76
- movies[movies.keys[str.to_i]] || []
77
- end
78
-
79
- def normalize_name(name)
80
- name.downcase.gsub(/[\s\.\-\_]+/, ' ')
81
- end
82
48
 
83
- def select_movie(movies)
84
- return movies.values.first || [] if movies.length <= 1
85
-
86
- puts "D'oh! You stumbled upon a hash conflict, please resolve it:"
87
- puts
88
- ask_user_to_identify_movie(movies)
89
- end
90
-
91
- def select_format(subs)
92
- return subs unless @options[:formats]
93
- subs.select{ |s| @options[:formats].include?(s.format) }
94
- end
95
-
96
- def select_sub(subs)
97
- subs = select_format(subs)
98
- movies = group_by_movie_name(subs)
99
- subs = select_movie(movies)
100
- subs.max_by(&:score)
101
- end
102
-
103
- def download_sub!(sub, movie)
104
- sub_path = movie.sub_path(sub.format)
105
- download!(sub.url, sub_path)
106
- end
49
+ def run!(files)
50
+ @parser.parse!
51
+
52
+ movie_files = files.map{ |path| OSDb::MovieFile.new(path) }
53
+
54
+ movie_files.each do |movie_file|
55
+ begin
56
+ puts "* Search subtitles for #{movie_file.name}"
57
+ if movie_file.has_sub? && !@options[:force]
58
+ puts "* Sub already there. To override it use --force"
59
+ puts
60
+ next
61
+ end
62
+
63
+ if sub = subtitle_finder.find_sub_for(movie_file, @options[:language])
64
+ download_sub!(sub, movie_file)
65
+ else
66
+ puts "* No sub found"
67
+ end
68
+ puts
69
+ rescue Exception => e
70
+ report_exception(e)
71
+ end
72
+ end
73
+ end
107
74
 
108
- def arg_files
109
- return ARGV unless @options[:dir]
110
- Dir.glob(File.join(@options[:dir], '**', "*.{#{OSDb::Movie::EXTENSIONS.join(',')}}"))
111
- end
75
+ def report_exception(exception)
76
+ puts "Something crashed."
77
+ puts "Feel free to report the error here: https://github.com/byroot/ruby-osdb/issues"
78
+ puts "With the following debug informations:"
79
+ puts
80
+ puts "#{exception.class.name}: #{exception.message}:"
81
+ puts exception.backtrace
82
+ puts
83
+ end
112
84
 
113
- movies = arg_files.map{ |path| OSDb::Movie.new(path) }
114
- movies.reject!(&:has_sub?) unless @options[:force]
115
-
116
- server = OSDb::Server.new(
117
- :host => 'api.opensubtitles.org',
118
- :path => '/xml-rpc',
119
- :timeout => 90,
120
- :useragent => "SubDownloader 2.0.10" # register useragent ? WTF ? too boring.
121
- )
122
- STDOUT.sync = true
123
-
124
- if movies.empty?
125
- puts "No file provided"
126
- puts @parser.help
127
- exit 1
128
- end
85
+ def subtitle_finder
86
+ @subtitle_finder ||= OSDb::SubtitleFinder.new(search_engines, finders, selectors)
87
+ end
129
88
 
130
- def search_by_hash(server, movie)
131
- server.search_subtitles(:moviehash => movie.hash, :moviebytesize => movie.size.to_s, :sublanguageid => @options[:language])
132
- end
89
+ SEARCH_ENGINES = {
90
+ 'h' => OSDb::Search::MovieHash,
91
+ 'i' => OSDb::Search::IMDB,
92
+ 'n' => OSDb::Search::Name,
93
+ 'p' => OSDb::Search::Path
94
+ }
133
95
 
134
- def search_by_path(server, movie)
135
- server.search_subtitles(:sublanguageid => @options[:language], :tag => movie.path)
136
- end
96
+ def search_engines
97
+ @options[:methods].to_s.each_char.to_a.uniq.map do |char|
98
+ SEARCH_ENGINES[char.to_s].new(server)
99
+ end
100
+ end
137
101
 
138
- def search_by_name(server, movie)
139
- subs = server.search_subtitles(:sublanguageid => @options[:language], :query => movie.name)
140
- normalized_movie_name = normalize_name(movie.name)
141
- subs.select! do |sub|
142
- normalize_name(sub.filename).index(normalized_movie_name) # MAYBE: Levenshtein ?
102
+ def finders
103
+ [OSDb::Finder::Score.new]
143
104
  end
144
- subs if subs.any?
145
- end
146
105
 
147
- def search_by_imdb(server, movie)
148
- imdb_results = server.search_imdb(:query => movie.name)
149
- if imdb_results.any?
150
- if imdb_results.length == 1
151
- imdb_result = imdb_results.first
152
- puts "* found on IMDB with ID: #{imdb_result.imdbid}"
106
+ def selectors
107
+ movie_finder = if @options[:auto]
108
+ OSDb::Finder::First.new # TODO: try to match subtitle movie name with filename
153
109
  else
154
- movies = Hash[imdb_results.map{ |r| [r.title, r] }]
155
- imdb_result = ask_user_to_identify_movie(movies)
110
+ OSDb::Finder::Interactive.new
156
111
  end
157
- server.search_subtitles(:sublanguageid => @options[:language], :imdbid => imdb_result.imdbid)
112
+
113
+ selectors = [
114
+ OSDb::Selector::Movie.new(movie_finder)
115
+ ]
116
+ selectors << OSDb::Selector::Format.new(@options[:formats]) if @options[:formats]
117
+ selectors
158
118
  end
159
- end
160
119
 
161
- movies.each do |movie|
162
- begin
163
- puts "* search subs by hash for: #{movie.path}"
164
- sub = select_sub search_by_hash(server, movie)
120
+ def server
121
+ @server ||= OSDb::Server.new(
122
+ :host => 'api.opensubtitles.org',
123
+ :path => '/xml-rpc',
124
+ :timeout => 90,
125
+ :useragent => "SubDownloader 2.0.10" # register useragent ? WTF ? too boring.
126
+ )
127
+ end
165
128
 
166
- unless sub
167
- puts "* could not find sub by hash, trying IMDB"
168
- sub = select_sub search_by_imdb(server, movie)
169
- end
129
+ def download_sub!(sub, movie_file)
130
+ sub_path = movie_file.sub_path(sub.format)
131
+ download!(sub.url, sub_path)
132
+ end
170
133
 
171
- unless sub
172
- puts "* no matches"
173
- puts "* search subs by path"
174
- sub = select_sub search_by_path(server, movie)
175
- end
134
+ def curl_available?
135
+ %x{ curl --version 2> /dev/null > /dev/null }
136
+ $?.success?
137
+ end
176
138
 
177
- unless sub
178
- puts "* still no matches"
179
- puts "* search subs by filename"
180
- sub = select_sub search_by_name(server, movie)
181
- end
139
+ def wget_available?
140
+ %x{ wget --version 2> /dev/null > /dev/null }
141
+ $?.success?
142
+ end
182
143
 
183
- if sub
184
- download_sub!(sub, movie)
144
+ def download!(url, local_path)
145
+ # TODO: use Net::HTTP and ZLib to avoid shell dependency
146
+ puts "* download #{url} to #{local_path}"
147
+ if curl_available?
148
+ %x{ curl -s '#{url}' | gunzip > "#{local_path}" }
149
+ elsif wget_available?
150
+ %x{ wget -O - -q '#{url}' | gunzip > "#{local_path}"}
185
151
  else
186
- puts "Nothing worked, you are very unlucky :'("
152
+ puts "Can't found any curl or wget please install one of them or manualy download your sub"
153
+ puts url
187
154
  end
188
- puts
189
- rescue Exception => e
190
- puts "Something crashed."
191
- puts "Feel free to report the error here: https://github.com/byroot/ruby-osdb/issues"
192
- puts "With the following debug informations:"
193
- puts
194
- puts "#{e.class.name}: #{e.message}:"
195
- puts e.backtrace
196
155
  end
156
+
197
157
  end
158
+
159
+ GetSub.new.run!(ARGV)
data/lib/osdb.rb CHANGED
@@ -2,8 +2,13 @@ require 'xmlrpc/client'
2
2
 
3
3
  module OSDb
4
4
  base_path = File.expand_path(File.dirname(__FILE__) + '/osdb')
5
- autoload :Language, "#{base_path}/language"
6
- autoload :Movie, "#{base_path}/movie"
7
- autoload :Server, "#{base_path}/server"
8
- autoload :Sub, "#{base_path}/sub"
9
- end
5
+ autoload :Finder, "#{base_path}/finder"
6
+ autoload :Language, "#{base_path}/language"
7
+ autoload :Movie, "#{base_path}/movie"
8
+ autoload :MovieFile, "#{base_path}/movie_file"
9
+ autoload :Search, "#{base_path}/search"
10
+ autoload :Selector, "#{base_path}/selector"
11
+ autoload :Server, "#{base_path}/server"
12
+ autoload :Sub, "#{base_path}/sub"
13
+ autoload :SubtitleFinder, "#{base_path}/subtitle_finder"
14
+ end
@@ -0,0 +1,8 @@
1
+ module OSDb
2
+ module Finder
3
+ base_path = File.expand_path(File.dirname(__FILE__) + '/finder')
4
+ autoload :First, "#{base_path}/first"
5
+ autoload :Interactive, "#{base_path}/interactive"
6
+ autoload :Score, "#{base_path}/score"
7
+ end
8
+ end
@@ -0,0 +1,13 @@
1
+ module OSDb
2
+ module Finder
3
+
4
+ class First
5
+
6
+ def chose(items)
7
+ items.first
8
+ end
9
+
10
+ end
11
+
12
+ end
13
+ end
@@ -0,0 +1,21 @@
1
+ module OSDb
2
+ module Finder
3
+
4
+ class Interactive
5
+
6
+ def chose(items)
7
+ puts "D'oh! You stumbled upon a hash conflict, please resolve it:"
8
+ puts
9
+ items.each_with_index do |name, index|
10
+ puts " #{index} - #{name}"
11
+ end
12
+ print 'id: '
13
+ str = STDIN.gets # TODO: rule #1, never trust user input
14
+ puts
15
+ items[str.to_i]
16
+ end
17
+
18
+ end
19
+
20
+ end
21
+ end
@@ -0,0 +1,13 @@
1
+ module OSDb
2
+ module Finder
3
+
4
+ class Score
5
+
6
+ def chose(items)
7
+ items.max_by(&:score)
8
+ end
9
+
10
+ end
11
+
12
+ end
13
+ end
data/lib/osdb/movie.rb CHANGED
@@ -1,61 +1,16 @@
1
1
  module OSDb
2
2
  class Movie
3
-
4
- EXTENSIONS = %w(avi mpg m4v mkv mov ogv mp4)
5
-
6
- attr_reader :path
7
-
8
- def initialize(path)
9
- @path = path
10
- end
11
3
 
12
- def has_sub?
13
- exist = false
14
- %w(.srt .sub).each{ |ext| exist ||= File.exist?(path.gsub(File.extname(path), ext)) }
15
- exist
16
- end
4
+ attr_reader :id, :title, :year, :cover, :rating, :raw_data
17
5
 
18
- def sub_path(format)
19
- path.gsub(File.extname(path), ".#{format}")
20
- end
21
-
22
- def hash
23
- @hash ||= self.class.compute_hash(path)
24
- end
25
-
26
- def size
27
- @size ||= File.size(path)
6
+ def initialize(data)
7
+ @id = data['id']
8
+ @title = data['title']
9
+ @year = data['year'] && data['year'].to_i
10
+ @cover = data['cover']
11
+ @rating = data['rating'] && data['rating'].to_f
12
+ @raw_data = data
28
13
  end
29
14
 
30
- def name
31
- @name ||= File.basename(path, File.extname(path))
32
- end
33
-
34
- CHUNK_SIZE = 64 * 1024 # in bytes
35
-
36
- # from http://trac.opensubtitles.org/projects/opensubtitles/wiki/HashSourceCodes
37
- def self.compute_hash(path)
38
- filesize = File.size(path)
39
- hash = filesize
40
-
41
- # Read 64 kbytes, divide up into 64 bits and add each
42
- # to hash. Do for beginning and end of file.
43
- File.open(path, 'rb') do |f|
44
- # Q = unsigned long long = 64 bit
45
- f.read(CHUNK_SIZE).unpack("Q*").each do |n|
46
- hash = hash + n & 0xffffffffffffffff # to remain as 64 bit number
47
- end
48
-
49
- f.seek([0, filesize - CHUNK_SIZE].max, IO::SEEK_SET)
50
-
51
- # And again for the end of the file
52
- f.read(CHUNK_SIZE).unpack("Q*").each do |n|
53
- hash = hash + n & 0xffffffffffffffff
54
- end
55
- end
56
-
57
- sprintf("%016x", hash)
58
- end
59
-
60
15
  end
61
16
  end