opensubtitles 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (46) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +4 -0
  3. data/.rspec +1 -0
  4. data/.travis.yml +10 -0
  5. data/Gemfile +3 -0
  6. data/LICENSE.txt +22 -0
  7. data/README.md +50 -0
  8. data/Rakefile +9 -0
  9. data/bin/getsub +157 -0
  10. data/lib/opensubtitles.rb +21 -0
  11. data/lib/opensubtitles/finder.rb +8 -0
  12. data/lib/opensubtitles/finder/first.rb +13 -0
  13. data/lib/opensubtitles/finder/interactive.rb +21 -0
  14. data/lib/opensubtitles/finder/score.rb +13 -0
  15. data/lib/opensubtitles/language.rb +84 -0
  16. data/lib/opensubtitles/movie.rb +16 -0
  17. data/lib/opensubtitles/movie_file.rb +67 -0
  18. data/lib/opensubtitles/search.rb +9 -0
  19. data/lib/opensubtitles/search/imdb.rb +27 -0
  20. data/lib/opensubtitles/search/movie_hash.rb +21 -0
  21. data/lib/opensubtitles/search/name.rb +28 -0
  22. data/lib/opensubtitles/search/path.rb +20 -0
  23. data/lib/opensubtitles/selector.rb +7 -0
  24. data/lib/opensubtitles/selector/format.rb +17 -0
  25. data/lib/opensubtitles/selector/movie.rb +29 -0
  26. data/lib/opensubtitles/server.rb +70 -0
  27. data/lib/opensubtitles/sub.rb +55 -0
  28. data/lib/opensubtitles/subtitle_finder.rb +30 -0
  29. data/lib/opensubtitles/version.rb +3 -0
  30. data/lib/opensubtitles/xmlrpc_monkey_patch.rb +88 -0
  31. data/opensubtitles.gemspec +26 -0
  32. data/spec/fixtures/http/check_movie_hash.yml +225 -0
  33. data/spec/fixtures/http/get_imdb_movie_details.yml +342 -0
  34. data/spec/fixtures/http/log_in.yml +86 -0
  35. data/spec/fixtures/http/log_out.yml +68 -0
  36. data/spec/fixtures/http/search_imdb.yml +761 -0
  37. data/spec/fixtures/http/search_subtitles_for_himym.yml +1189 -0
  38. data/spec/fixtures/http/search_subtitles_for_the_rock.yml +4124 -0
  39. data/spec/fixtures/http/server_info.yml +492 -0
  40. data/spec/fixtures/somemovie.avi +0 -0
  41. data/spec/opensubtitles/language_spec.rb +57 -0
  42. data/spec/opensubtitles/movie_file_spec.rb +18 -0
  43. data/spec/opensubtitles/server_spec.rb +123 -0
  44. data/spec/opensubtitles/sub_spec.rb +33 -0
  45. data/spec/spec_helper.rb +15 -0
  46. metadata +159 -0
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 56636430f2a77e1d25513363f97d20621023c537
4
+ data.tar.gz: 879215ec0a742848a2c1c43b32684742f7761f1c
5
+ SHA512:
6
+ metadata.gz: 971eeb7d6fb8b304b6e687bfe24e39a19d3b463d242b2bd50e7138670a5c72c85c09b674ba39bf012bc392224f038b81f673fcf0956e5c28168064de78837101
7
+ data.tar.gz: 1c93a7f15b5f4eb5639eadf77a23c9c64e2c57507542af3609dbfcb822a1bc93e3bec4d8b2d385fc8aab0b32c648adcec51f94d898d6881bc822056c954916ea
@@ -0,0 +1,4 @@
1
+ *.gem
2
+ .bundle
3
+ Gemfile.lock
4
+ pkg/*
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --color
@@ -0,0 +1,10 @@
1
+ rvm:
2
+ - 1.9.3
3
+ # - 2.0.1
4
+ - 2.1.1
5
+ - 2.2.1
6
+ - 2.2.3
7
+ - rbx
8
+ - jruby
9
+ # - ree
10
+
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source "http://rubygems.org"
2
+
3
+ gemspec
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2013 Jean Boussier
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,50 @@
1
+ # Opensubtitles
2
+
3
+ Client library for the [Opensubtitles protocol](http://trac.opensubtitles.org/projects/opensubtitles/wiki/XMLRPC).
4
+ Currently the implentation is limited to movie identification and subtitles search
5
+
6
+ [![Build Status](https://secure.travis-ci.org/evandrojr/opensubtitles.png)](http://travis-ci.org/evandrojr/opensubtitles)
7
+
8
+ ## Examples
9
+
10
+ Just read the source of `bin/getsub` it is a typical example of Opensubtitles's capacities.
11
+
12
+ ## getsub
13
+
14
+ The opensubtitles gem provide a simple script to find and download the best subtitle on
15
+ [opensubtitles.org](http://www.opensubtitles.org/) for your video file.
16
+
17
+ ### Installation
18
+
19
+ $ gem install opensubtitles
20
+
21
+ ### Usage
22
+
23
+ getsub [options] DIRECTORY | VIDEO_FILE [VIDEO_FILE ...]
24
+
25
+ You just have to execute `getsub` with some video files in arguments:
26
+
27
+ $ getsub somemovie.avi othermovie.mkv
28
+
29
+ Or specify a directory to search recursively:
30
+
31
+ $ getsub ~/Movies
32
+
33
+ For options details just run:
34
+
35
+ $ getsub --help
36
+
37
+ Main options:
38
+
39
+ -a, --auto Do not ask user to resolve hash conflicts.
40
+
41
+ -l, --language LANGUAGE Sub language ISO 639-2 code like fre or eng. Default: env $LANG (your-lang)
42
+
43
+ -f, --force Download sub even if video already has one
44
+
45
+ -t, --type FORMATS Select only subtitles in specified formats. e.g -t srt,sub
46
+
47
+ -L, --language-extension Add the ISO 639-2 in the subtitle's file extension. e.g filename.eng.srt
48
+
49
+ -s, --search-by METHODS Ordered list of search methods. h: by movie hash, i: by name on IMDB, n: by name on Opensubtitles, p: by filename on Opensubtitles. e.g -s hi . Default: h
50
+
@@ -0,0 +1,9 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+ require 'rspec/core/rake_task'
4
+ require 'bundler'
5
+
6
+ Bundler::GemHelper.install_tasks
7
+ RSpec::Core::RakeTask.new(:spec)
8
+
9
+ task :default => :spec
@@ -0,0 +1,157 @@
1
+ #!/usr/bin/env ruby
2
+ require 'optparse'
3
+ require 'uri'
4
+ require 'rubygems'
5
+ require File.expand_path(File.join(File.dirname(__FILE__), '..', 'lib', 'opensubtitles'))
6
+
7
+
8
+ class GetSub
9
+
10
+ def initialize
11
+ @options = {:language => Opensubtitles.default_language.to_iso639_2b, :force => false, :dir => nil, :methods => 'h', :language_extension => false }
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:"
18
+
19
+ opts.on("-a", "--auto", "Do not ask user to resolve hash conflicts.") { @options[:auto] = true }
20
+
21
+ opts.on("-l", "--language LANGUAGE", "Sub language ISO 639-2 code like fre or eng. Default: env $LANG (#{Opensubtitles.default_language.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
28
+
29
+ opts.on("-f", "--force", "Download sub even if video already has one") { @options[:force] = true }
30
+
31
+ opts.on("-t", "--type FORMATS", "Select only subtitles in specified formats. e.g -t srt,sub") { |formats| @options[:formats] = formats.to_s.split(',') }
32
+
33
+ opts.on("-L", "--language-extension", "Add the ISO 639-2 in the subtitle's file extension. e.g filename.eng.srt" ) { @options[:language_extension] = true }
34
+
35
+ methods_help = "Ordered list of search methods. h: by movie hash, i: by name on IMDB, n: by name on Opensubtitles, p: by filename on Opensubtitles. e.g -s hi . Default: h"
36
+ opts.on("-s", "--search-by METHODS", methods_help) do |methods|
37
+ unless methods =~ /^[hinp]+$/
38
+ STDERR.puts "Invalid argument: Available search methods are: h, i, n and p."
39
+ exit 1
40
+ end
41
+ @options[:methods] = methods
42
+ end
43
+
44
+ end
45
+ end
46
+
47
+ def run!(files)
48
+ @parser.parse!
49
+ language = @options[:language] if @options[:language_extension]
50
+
51
+ movie_files = glob(files).map{ |path| Opensubtitles::MovieFile.new(path, language) }
52
+
53
+ movie_files.each do |movie_file|
54
+ begin
55
+ puts "* Search subtitles for #{movie_file.name}"
56
+ if movie_file.has_sub? && !@options[:force]
57
+ puts "* Sub already there. To override it use --force"
58
+ puts
59
+ next
60
+ end
61
+
62
+ if sub = subtitle_finder.find_sub_for(movie_file, @options[:language])
63
+ download_sub!(sub, movie_file)
64
+ else
65
+ puts "* No sub found"
66
+ end
67
+ puts
68
+ rescue Exception => e
69
+ report_exception(e)
70
+ end
71
+ end
72
+ end
73
+
74
+ def glob(files)
75
+ files.map do |path|
76
+ if File.directory?(path)
77
+ Dir.chdir(path) do # chdir to avoid escaping special chars in path
78
+ relative_paths = Dir.glob("**/*.{#{Opensubtitles::MovieFile::EXTENSIONS.join(',')}}")
79
+ return relative_paths.map{ |f| File.join(path, f) }
80
+ end
81
+ else
82
+ path
83
+ end
84
+ end.flatten
85
+ end
86
+
87
+ def report_exception(exception)
88
+ puts
89
+ puts "Something crashed."
90
+ puts "Feel free to report the error here: https://github.com/byroot/opensubtitles/issues"
91
+ puts "With the following debug informations:"
92
+ puts
93
+ puts "#{exception.class.name}: #{exception.message}:"
94
+ puts exception.backtrace
95
+ puts
96
+ end
97
+
98
+ def subtitle_finder
99
+ @subtitle_finder ||= Opensubtitles::SubtitleFinder.new(search_engines, finders, selectors)
100
+ end
101
+
102
+ SEARCH_ENGINES = {
103
+ 'h' => Opensubtitles::Search::MovieHash,
104
+ 'i' => Opensubtitles::Search::IMDB,
105
+ 'n' => Opensubtitles::Search::Name,
106
+ 'p' => Opensubtitles::Search::Path
107
+ }
108
+
109
+ def search_engines
110
+ @options[:methods].to_s.each_char.to_a.uniq.map do |char|
111
+ SEARCH_ENGINES[char.to_s].new(server)
112
+ end
113
+ end
114
+
115
+ def finders
116
+ [Opensubtitles::Finder::Score.new]
117
+ end
118
+
119
+ def selectors
120
+ movie_finder = if @options[:auto]
121
+ Opensubtitles::Finder::First.new # TODO: try to match subtitle movie name with filename
122
+ else
123
+ Opensubtitles::Finder::Interactive.new
124
+ end
125
+
126
+ selectors = [
127
+ Opensubtitles::Selector::Movie.new(movie_finder)
128
+ ]
129
+ selectors << Opensubtitles::Selector::Format.new(@options[:formats]) if @options[:formats]
130
+ selectors
131
+ end
132
+
133
+ def server
134
+ @server ||= Opensubtitles::Server.new(
135
+ :timeout => 90,
136
+ :useragent => "SubDownloader 2.0.10" # register useragent ? WTF ? too boring.
137
+ )
138
+ end
139
+
140
+ def download_sub!(sub, movie_file)
141
+ local_path = movie_file.sub_path(sub.format)
142
+ print "* download #{sub.url} to #{local_path} ... "
143
+ content = sub.body
144
+ unless content
145
+ puts "failed"
146
+ return
147
+ end
148
+
149
+ File.open(movie_file.sub_path(sub.format), 'w+') do |file|
150
+ file.write(content)
151
+ end
152
+ puts "done"
153
+ end
154
+
155
+ end
156
+
157
+ GetSub.new.run!(ARGV)
@@ -0,0 +1,21 @@
1
+ require 'xmlrpc/client'
2
+
3
+ module Opensubtitles
4
+ base_path = File.expand_path(File.dirname(__FILE__) + '/opensubtitles')
5
+ require "#{base_path}/xmlrpc_monkey_patch"
6
+
7
+ autoload :Finder, "#{base_path}/finder"
8
+ autoload :Language, "#{base_path}/language"
9
+ autoload :Movie, "#{base_path}/movie"
10
+ autoload :MovieFile, "#{base_path}/movie_file"
11
+ autoload :Search, "#{base_path}/search"
12
+ autoload :Selector, "#{base_path}/selector"
13
+ autoload :Server, "#{base_path}/server"
14
+ autoload :Sub, "#{base_path}/sub"
15
+ autoload :SubtitleFinder, "#{base_path}/subtitle_finder"
16
+
17
+ def self.default_language
18
+ Opensubtitles::Language.from_locale(ENV['LANG'] || 'en_US.UTF-8')
19
+ end
20
+
21
+ end
@@ -0,0 +1,8 @@
1
+ module Opensubtitles
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 Opensubtitles
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 Opensubtitles
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 Opensubtitles
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
@@ -0,0 +1,84 @@
1
+ module Opensubtitles
2
+
3
+ LANGUAGES = [
4
+ {:iso639_1 => 'sq', :iso639_2b => 'alb', :locale => 'sq', :name => 'Albanian'},
5
+ {:iso639_1 => 'ar', :iso639_2b => 'ara', :locale => 'ar', :name => 'Arabic'},
6
+ {:iso639_1 => 'hy', :iso639_2b => 'arm', :locale => 'hy', :name => 'Armenian'},
7
+ {:iso639_1 => 'ms', :iso639_2b => 'may', :locale => 'ms', :name => 'Malay'},
8
+ {:iso639_1 => 'bs', :iso639_2b => 'bos', :locale => 'bs', :name => 'Bosnian'},
9
+ {:iso639_1 => 'bg', :iso639_2b => 'bul', :locale => 'bg', :name => 'Bulgarian'},
10
+ {:iso639_1 => 'ca', :iso639_2b => 'cat', :locale => 'ca', :name => 'Catalan'},
11
+ {:iso639_1 => 'eu', :iso639_2b => 'eus', :locale => 'eu', :name => 'Basque'},
12
+ {:iso639_1 => 'zh', :iso639_2b => 'chi', :locale => 'zh_CN', :name => 'Chinese (China)'},
13
+ {:iso639_1 => 'hr', :iso639_2b => 'hrv', :locale => 'hr', :name => 'Croatian'},
14
+ {:iso639_1 => 'cs', :iso639_2b => 'cze', :locale => 'cs', :name => 'Czech'},
15
+ {:iso639_1 => 'da', :iso639_2b => 'dan', :locale => 'da', :name => 'Danish'},
16
+ {:iso639_1 => 'nl', :iso639_2b => 'dut', :locale => 'nl', :name => 'Dutch'},
17
+ {:iso639_1 => 'en', :iso639_2b => 'eng', :locale => 'en', :name => 'English (US)'},
18
+ {:iso639_1 => 'en', :iso639_2b => 'bre', :locale => 'en_GB', :name => 'English (UK)'},
19
+ {:iso639_1 => 'eo', :iso639_2b => 'epo', :locale => 'eo', :name => 'Esperanto'},
20
+ {:iso639_1 => 'et', :iso639_2b => 'est', :locale => 'et', :name => 'Estonian'},
21
+ {:iso639_1 => 'fi', :iso639_2b => 'fin', :locale => 'fi', :name => 'Finnish'},
22
+ {:iso639_1 => 'fr', :iso639_2b => 'fre', :locale => 'fr', :name => 'French'},
23
+ {:iso639_1 => 'gl', :iso639_2b => 'glg', :locale => 'gl', :name => 'Galician'},
24
+ {:iso639_1 => 'ka', :iso639_2b => 'geo', :locale => 'ka', :name => 'Georgian'},
25
+ {:iso639_1 => 'de', :iso639_2b => 'ger', :locale => 'de', :name => 'German'},
26
+ {:iso639_1 => 'el', :iso639_2b => 'ell', :locale => 'el', :name => 'Greek'},
27
+ {:iso639_1 => 'he', :iso639_2b => 'heb', :locale => 'he', :name => 'Hebrew'},
28
+ {:iso639_1 => 'hu', :iso639_2b => 'hun', :locale => 'hu', :name => 'Hungarian'},
29
+ {:iso639_1 => 'id', :iso639_2b => 'ind', :locale => 'id', :name => 'Indonesian'},
30
+ {:iso639_1 => 'it', :iso639_2b => 'ita', :locale => 'it', :name => 'Italian'},
31
+ {:iso639_1 => 'ja', :iso639_2b => 'jpn', :locale => 'ja', :name => 'Japanese'},
32
+ {:iso639_1 => 'kk', :iso639_2b => 'kaz', :locale => 'kk', :name => 'Kazakh'},
33
+ {:iso639_1 => 'ko', :iso639_2b => 'kor', :locale => 'ko', :name => 'Korean'},
34
+ {:iso639_1 => 'lv', :iso639_2b => 'lav', :locale => 'lv', :name => 'Latvian'},
35
+ {:iso639_1 => 'lt', :iso639_2b => 'lit', :locale => 'lt', :name => 'Lithuanian'},
36
+ {:iso639_1 => 'lb', :iso639_2b => 'ltz', :locale => 'lb', :name => 'Luxembourgish'},
37
+ {:iso639_1 => 'mk', :iso639_2b => 'mac', :locale => 'mk', :name => 'Macedonian'},
38
+ {:iso639_1 => 'no', :iso639_2b => 'nor', :locale => 'no', :name => 'Norwegian'},
39
+ {:iso639_1 => 'fa', :iso639_2b => 'per', :locale => 'fa', :name => 'Persian'},
40
+ {:iso639_1 => 'pl', :iso639_2b => 'pol', :locale => 'pl', :name => 'Polish'},
41
+ {:iso639_1 => 'pt', :iso639_2b => 'por', :locale => 'pt_PT', :name => 'Portuguese (Portugal)'},
42
+ {:iso639_1 => 'pb', :iso639_2b => 'pob', :locale => 'pt_BR', :name => 'Portuguese (Brazil)'},
43
+ {:iso639_1 => 'ro', :iso639_2b => 'rum', :locale => 'ro', :name => 'Romanian'},
44
+ {:iso639_1 => 'ru', :iso639_2b => 'rus', :locale => 'ru', :name => 'Russian'},
45
+ {:iso639_1 => 'sr', :iso639_2b => 'scc', :locale => 'sr', :name => 'Serbian'},
46
+ {:iso639_1 => 'sk', :iso639_2b => 'slo', :locale => 'sk', :name => 'Slovak'},
47
+ {:iso639_1 => 'sl', :iso639_2b => 'slv', :locale => 'sl', :name => 'Slovenian'},
48
+ {:iso639_1 => 'es', :iso639_2b => 'spa', :locale => 'es_ES', :name => 'Spanish (Spain)'},
49
+ {:iso639_1 => 'sv', :iso639_2b => 'swe', :locale => 'sv', :name => 'Swedish'},
50
+ {:iso639_1 => 'th', :iso639_2b => 'tha', :locale => 'th', :name => 'Thai'},
51
+ {:iso639_1 => 'tr', :iso639_2b => 'tur', :locale => 'tr', :name => 'Turkish'},
52
+ {:iso639_1 => 'uk', :iso639_2b => 'ukr', :locale => 'uk', :name => 'Ukrainian'},
53
+ {:iso639_1 => 'vi', :iso639_2b => 'vie', :locale => 'vi', :name => 'Vietnamese'}
54
+ ]
55
+
56
+ class Language
57
+
58
+ class << self
59
+
60
+ def from_locale(locale)
61
+ locale = locale.split('.').first
62
+ lang = LANGUAGES.find{ |lang| lang[:locale] == locale }
63
+ return from_locale(locale.split('_').first) if !lang && locale.include?('_')
64
+ new(lang)
65
+ end
66
+
67
+ def from_iso639_2b(code)
68
+ new(LANGUAGES.find{ |lang| lang[:iso639_2b] == code })
69
+ end
70
+
71
+ end
72
+
73
+ attr_reader :name, :to_iso639_1, :to_iso639_2b, :to_locale
74
+
75
+ def initialize(hash)
76
+ @name = hash[:name]
77
+ @to_iso639_1 = hash[:iso639_1]
78
+ @to_iso639_2b = hash[:iso639_2b]
79
+ @to_locale = hash[:locale]
80
+ end
81
+
82
+ end
83
+
84
+ end
@@ -0,0 +1,16 @@
1
+ module Opensubtitles
2
+ class Movie
3
+
4
+ attr_reader :id, :title, :year, :cover, :rating, :raw_data
5
+
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
13
+ end
14
+
15
+ end
16
+ end