subs 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,58 @@
1
+ module Subs
2
+
3
+ class Provider
4
+
5
+ attr_reader :name
6
+ attr_reader :uri
7
+ attr_reader :user_agent
8
+
9
+ def initialize(name, uri, user_agent)
10
+ @name = name
11
+ @uri = uri.is_a?(String) ? URI(uri) : uri
12
+ @user_agent = user_agent
13
+ end
14
+
15
+ def process_result(io, result)
16
+ Subs.log.debug { "Processing '#{result.name}'"}
17
+ unless self.is_a?(result.provider)
18
+ Subs.log.error { "#{@name} cannot process #{result.provider_name} result"}
19
+ return false
20
+ end
21
+ true
22
+ end
23
+ end
24
+
25
+ module CredentialProvider
26
+ end
27
+
28
+ module HashSearcher
29
+
30
+ def compute_hash(path)
31
+ end
32
+
33
+ def hash_search(path, *languages)
34
+ Array.new
35
+ end
36
+ end
37
+
38
+ module LoginProvider
39
+
40
+ def login(username, password)
41
+ false
42
+ end
43
+ end
44
+
45
+ module FilenameSearcher
46
+
47
+ def filename_search(path, *larnguages)
48
+ Array.new
49
+ end
50
+ end
51
+
52
+ module IMDbSearcher
53
+
54
+ def imdb_search(path, imdb_code, *languages)
55
+ Array.new
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,80 @@
1
+
2
+ module Subs
3
+
4
+ class SubDB < Provider
5
+
6
+ include HashSearcher
7
+
8
+ def initialize(&block)
9
+ agent = "SubDB/1.0 (subs/#{Subs::VERSION}; http://github.com/ForeverZer0/subs)"
10
+ super('TheSubDB.com', URI('http://api.thesubdb.com/'), agent)
11
+ yield self if block_given?
12
+ end
13
+
14
+ def compute_hash(path)
15
+ size = 64 * 1024
16
+ File.open(path, 'rb') do |io|
17
+ buffer = io.read(size)
18
+ io.seek(-size, IO::SEEK_END)
19
+ buffer << io.read
20
+ return Digest::MD5.hexdigest(buffer)
21
+ end
22
+ end
23
+
24
+ def process_result(io, result)
25
+ return false unless super
26
+ hash = result.data
27
+ lang = result.language.alpha2
28
+ request = Net::HTTP::Get.new(@uri.path + Subs.query_string(action: :download, hash: hash, language: lang ))
29
+ request['User-Agent'] = @user_agent
30
+ begin
31
+ Net::HTTP.start(@uri.host, @uri.port) do |net|
32
+ response = net.request(request)
33
+ return false unless response.code.to_i == 200
34
+ io.write(response.body)
35
+ Subs.log.debug { 'Success'.green }
36
+ return true
37
+ end
38
+ rescue
39
+ Subs.log.debug { 'Failed'.red }
40
+ return false
41
+ end
42
+ end
43
+
44
+ def hash_search(path, *languages)
45
+ results = []
46
+ unless File.exist?(path)
47
+ Subs.log.error { "Cannot search, cannot find '#{path}'" }
48
+ return results
49
+ end
50
+ hash = compute_hash(path)
51
+ Subs.log.debug { "Searching #{@name.blue} by hash using '#{hash}'" }
52
+ supported = supported_languages(hash)
53
+ languages.each do |language|
54
+ next unless supported.include?(language)
55
+ name = File.basename(Subs.build_subtitle_path(path, language))
56
+ results << Subs::SearchResult.new(@name, self.class, name, language, path, hash)
57
+ end
58
+ Subs.log.debug { "Found #{results.size.to_s.light_blue} result(s)." }
59
+ results
60
+ end
61
+
62
+ def supported_languages(hash)
63
+ request = Net::HTTP::Get.new(@uri.path + Subs.query_string(action: 'search', hash: hash))
64
+ request['User-Agent'] = @user_agent
65
+ Net::HTTP.start(@uri.host, @uri.port) do |net|
66
+ body = net.request(request).body
67
+ body.split(',').map { |value| Subs::Language.from_alpha2(value) }.compact
68
+ end
69
+ end
70
+
71
+ def all_supported_languages
72
+ request = Net::HTTP::Get.new(@uri.path + Subs.query_string(action: 'languages'))
73
+ request['User-Agent'] = @user_agent
74
+ Net::HTTP.start(@uri.host, @uri.port) do |net|
75
+ body = net.request(request).body
76
+ body.split(',').map { |value| Subs::Language.from_alpha2(value) }.compact
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,47 @@
1
+ module Subs
2
+ class SubRipTime
3
+
4
+ def initialize(hour, minute, second, millisecond)
5
+ @ms = millisecond
6
+ @ms += second * 1000
7
+ @ms += minute * 60 * 1000
8
+ @ms += hour * 60 * 60 * 1000
9
+ end
10
+
11
+ ZERO = SubRipTime.new(0, 0, 0, 0).freeze
12
+
13
+ def total_ms
14
+ @ms
15
+ end
16
+
17
+ def -(amount)
18
+ value = amount.is_a?(SubRipTime) ? amount.total_ms : Integer(amount)
19
+ self.class.new(0, 0, 0, [@ms - value, 0].max)
20
+ end
21
+
22
+ def +(amount)
23
+ value = amount.is_a?(SubRipTime) ? amount.total_ms : Integer(amount)
24
+ self.class.new(0, 0, 0, [@ms + value, 0].max)
25
+ end
26
+
27
+ def hours
28
+ @ms / (1000 * 60 * 60)
29
+ end
30
+
31
+ def minutes
32
+ (@ms / (1000 * 60)) % 60
33
+ end
34
+
35
+ def seconds
36
+ (@ms / 1000) % 60
37
+ end
38
+
39
+ def milliseconds
40
+ @ms % 1000
41
+ end
42
+
43
+ def to_s
44
+ "%02d:%02d:%02d,%03d" % [hours, minutes, seconds, milliseconds]
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,4 @@
1
+ module Subs
2
+
3
+ VERSION = "1.0.0"
4
+ end
data/lib/subs.rb ADDED
@@ -0,0 +1,192 @@
1
+ require 'open-uri'
2
+ require 'zlib'
3
+ require 'digest'
4
+ require 'net/http'
5
+ require 'cgi'
6
+ require 'set'
7
+
8
+ require_relative 'subs/version'
9
+ require_relative 'subs/language'
10
+ require_relative 'subs/sub_rip_time'
11
+ require_relative 'subs/providers/provider'
12
+ require_relative 'subs/providers/sub_db'
13
+ require_relative 'subs/providers/open_subtitles'
14
+
15
+ class String
16
+
17
+ # Defines methods for displaying colorized console output, unless already provided by another gem.
18
+
19
+ [:red, :green, :yellow, :blue, :pink, :light_blue].each_with_index do |color, i|
20
+ next if method_defined?(color)
21
+ class_eval("def #{color};\"\e[#{i + 31}m\#{self}\e[0m\";end", __FILE__ , __LINE__ )
22
+ end
23
+
24
+ ##
25
+ # @!method red
26
+ # @return [String]
27
+
28
+ ##
29
+ # @!method green
30
+ # @return [String]
31
+
32
+ ##
33
+ # @!method yellow
34
+ # @return [String]
35
+
36
+ ##
37
+ # @!method blue
38
+ # @return [String]
39
+
40
+ ##
41
+ # @!method pink
42
+ # @return [String]
43
+
44
+ ##
45
+ # @!method light_blue
46
+ # @return [String]
47
+ end
48
+
49
+ ##
50
+ # Top-level namespace for the gem.
51
+ module Subs
52
+
53
+ ##
54
+ # Represents a generic search result for a subtitle
55
+ SearchResult = Struct.new(:provider_name, :provider, :name, :language, :video, :data)
56
+
57
+ ##
58
+ # Represent a movie.
59
+ Movie = Struct.new(:name, :year, :imdb)
60
+
61
+ ##
62
+ # Video extensions to search for.
63
+ VIDEO_EXTENSIONS = %w(.avi .mkv .mp4 .mov .mpg .wmv .rm .rmvb .divx).freeze
64
+
65
+ ##
66
+ # Subtitle extensions to search for.
67
+ SUB_EXTENSIONS = %w(.srt .sub).freeze
68
+
69
+ ##
70
+ # Creates the logger with the specified output stream and verbosity level.
71
+ #
72
+ # @param io [IO] An IO instance that can be written to.
73
+ # @param verbosity [:info|:warn|:error|:fatal|:debug] The logger verbosity level.
74
+ #
75
+ # @return [Logger] the created Logger instance.
76
+ def self.create_log(io = STDOUT, verbosity = :info)
77
+ unless @log
78
+ require 'logger'
79
+ @log = Logger.new(io)
80
+ @log.formatter = proc do |severity, datetime, _, msg|
81
+ "[%s] %s -- %s\n" % [datetime.strftime('%Y-%m-%d %H:%M:%S'), severity, msg]
82
+ end
83
+ @log.level = case verbosity
84
+ when :warn then Logger::WARN
85
+ when :error then Logger::ERROR
86
+ when :fatal then Logger::FATAL
87
+ when :debug then Logger::DEBUG
88
+ else Logger::INFO
89
+ end
90
+ end
91
+ @log
92
+ end
93
+
94
+ ##
95
+ # @return [Logger] the logger instance for the module.
96
+ def self.log
97
+ @log ||= create_log(STDOUT)
98
+ end
99
+
100
+ ##
101
+ # Searches the specified directory for supported video files.
102
+ #
103
+ # @param directory [String] Path to a directory to search.
104
+ # @param recursive [Boolean] `true` to search recursively within nested directories, otherwise `false`.
105
+ #
106
+ # @return [Array<String>] paths to all found video files.
107
+ #
108
+ def self.video_search(directory, recursive)
109
+ VIDEO_EXTENSIONS.flat_map do |ext|
110
+ Dir.glob(File.join(directory, recursive ? "**/*#{ext}" : "*#{ext}"))
111
+ end
112
+ end
113
+
114
+ ##
115
+ # Checks the specified video file for the existence of a subtitles, using common naming conventions and optional
116
+ # language.
117
+ #
118
+ # @param video_path [String] The path to the video file to check.
119
+ # @param language [Language] A specific language to check.
120
+ #
121
+ # @return [Boolean] `true` if a matching subtitle was found, otherwise `false`.
122
+ #
123
+ def self.subtitle_exist?(video_path, language = nil)
124
+ dir = File.dirname(video_path)
125
+ # ex. '/home/me/Videos/MyFavoriteMovie.2019.mp4' => 'MyFavoriteMovie.2019'
126
+ base = File.basename(video_path, File.extname(video_path))
127
+ # Check each supported subtitle extension
128
+ SUB_EXTENSIONS.each do |ext|
129
+ # ex. MyFavoriteMovie.2019.srt
130
+ return true if File.exist?(File.join(dir, "#{base}#{ext}"))
131
+ next unless language
132
+ if language.alpha2
133
+ # ex. MyFavoriteMovie.2019.en.srt
134
+ return true if File.exist?(File.join(dir, "#{base}.#{language.alpha2}#{ext}"))
135
+ end
136
+ # ex. MyFavoriteMovie.2019.eng.srt
137
+ return true if File.exist?(File.join(dir, "#{base}.#{language.alpha3}#{ext}"))
138
+ end
139
+ # Not found
140
+ false
141
+ end
142
+
143
+ ##
144
+ # Creates a query string to be used within a URI based on specified parameters.
145
+ #
146
+ # @param params [Hash<Symbol, Object>] A hash of keyword arguments that are used to build the query.
147
+ #
148
+ # @return [String] The constructed query string.
149
+ def self.query_string(**params)
150
+ query = ''
151
+ params.each_pair do |key, value|
152
+ next unless value
153
+ query << (query.size.zero? ? '?' : '&')
154
+ query << CGI.escape(key.to_s)
155
+ query << '='
156
+ query << CGI.escape(value.to_s)
157
+ end
158
+ query
159
+ end
160
+
161
+ ##
162
+ # Convenience method to attempt getting basic movie information with the specified file.
163
+ #
164
+ # @param path [String] A path to a video file.
165
+ #
166
+ # @return [Movie?] Movie information with result, or `nil` if none could be found.
167
+ def self.fuzzy_search(path)
168
+ return nil unless File.exist?(path)
169
+ result = nil
170
+ OpenSubtitles.new { |provider| result = provider.fuzzy_search(File.basename(path)) }
171
+ result
172
+ end
173
+
174
+ ##
175
+ # Uses the path of a video file to create a path to a matching subtitle file.
176
+ #
177
+ # @param path [String] The path to the video file.
178
+ # @param language [Language] A language for applying a 3-letter language suffix to the file, or `nil` to omit suffix.
179
+ # @param ext [String] The language file extension, including leading dot.
180
+ #
181
+ # @return [String] The created subtitle path.
182
+ def self.build_subtitle_path(path, language = nil, ext = '.srt')
183
+ dir = File.dirname(path)
184
+ base = File.basename(path, File.extname(path))
185
+ if language
186
+ File.join(dir, "#{base}.#{language.alpha3}#{ext}")
187
+ else
188
+ File.join(dir, "#{base}#{ext}")
189
+ end
190
+ end
191
+ end
192
+
data/subs.gemspec ADDED
@@ -0,0 +1,32 @@
1
+
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'subs/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = 'subs'
8
+ spec.version = Subs::VERSION
9
+ spec.authors = ['ForeverZer0']
10
+ spec.email = ['efreed09@gmail.com']
11
+
12
+ spec.summary = %q{Simple and intuitive command-line program to automatically and accurately search and download subtitles for all of your favorite movies and television shows.}
13
+ spec.description = %q{Simple and intuitive command-line program to automatically and accurately search and download subtitles for all of your favorite movies and television shows. Utilizes multiple providers search algorithms ensure you always get the best result, and includes tools to resync SubRip files with ease.}
14
+ spec.homepage = 'https://github.com/ForeverZer0/subs'
15
+ spec.license = 'MIT'
16
+
17
+ # Specify which files should be added to the gem when it is released.
18
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
19
+ spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
20
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
21
+ end
22
+
23
+ spec.bindir = 'bin'
24
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
25
+ spec.require_paths = ['lib']
26
+
27
+ spec.add_runtime_dependency 'xmlrpc', '~> 0.3'
28
+ spec.add_runtime_dependency 'thor', '~> 0.20'
29
+
30
+ spec.add_development_dependency 'bundler', '~> 2.0'
31
+ spec.add_development_dependency 'rake', '~> 10.0'
32
+ end
metadata ADDED
@@ -0,0 +1,120 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: subs
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - ForeverZer0
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2019-05-10 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: xmlrpc
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '0.3'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '0.3'
27
+ - !ruby/object:Gem::Dependency
28
+ name: thor
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '0.20'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '0.20'
41
+ - !ruby/object:Gem::Dependency
42
+ name: bundler
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '2.0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '2.0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rake
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '10.0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '10.0'
69
+ description: Simple and intuitive command-line program to automatically and accurately
70
+ search and download subtitles for all of your favorite movies and television shows.
71
+ Utilizes multiple providers search algorithms ensure you always get the best result,
72
+ and includes tools to resync SubRip files with ease.
73
+ email:
74
+ - efreed09@gmail.com
75
+ executables:
76
+ - subs
77
+ extensions: []
78
+ extra_rdoc_files: []
79
+ files:
80
+ - ".gitignore"
81
+ - ".travis.yml"
82
+ - CODE_OF_CONDUCT.md
83
+ - Gemfile
84
+ - LICENSE.txt
85
+ - README.md
86
+ - Rakefile
87
+ - bin/subs
88
+ - lib/subs.rb
89
+ - lib/subs/language.rb
90
+ - lib/subs/providers/open_subtitles.rb
91
+ - lib/subs/providers/provider.rb
92
+ - lib/subs/providers/sub_db.rb
93
+ - lib/subs/sub_rip_time.rb
94
+ - lib/subs/version.rb
95
+ - subs.gemspec
96
+ homepage: https://github.com/ForeverZer0/subs
97
+ licenses:
98
+ - MIT
99
+ metadata: {}
100
+ post_install_message:
101
+ rdoc_options: []
102
+ require_paths:
103
+ - lib
104
+ required_ruby_version: !ruby/object:Gem::Requirement
105
+ requirements:
106
+ - - ">="
107
+ - !ruby/object:Gem::Version
108
+ version: '0'
109
+ required_rubygems_version: !ruby/object:Gem::Requirement
110
+ requirements:
111
+ - - ">="
112
+ - !ruby/object:Gem::Version
113
+ version: '0'
114
+ requirements: []
115
+ rubygems_version: 3.0.3
116
+ signing_key:
117
+ specification_version: 4
118
+ summary: Simple and intuitive command-line program to automatically and accurately
119
+ search and download subtitles for all of your favorite movies and television shows.
120
+ test_files: []