osdb 0.0.10 → 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.
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