get-addic7ed 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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 9d5ff8f9da39da43045c9cad1deb440fa6a54d8c
4
+ data.tar.gz: bad8feb10529aebe17e1507fd30b7db2f77d7c01
5
+ SHA512:
6
+ metadata.gz: f76e8571f9d431a8ce5fbaa6b958ae334e6fb2a88210ba5fe3a5964f0e07723fa32648aa557507e35ee9e8e44c0100a7b1c6b66d7b6f89ac6c6bf63792074b80
7
+ data.tar.gz: 5d5241b0f9a27a839eff5f1cbbf47112fd83932e136bfa9d629b9cfad1ea62dc89c1ef432158281b21927b31b9a7063a71509c85af7e86c7b596accb0aec105f
data/bin/get-addic7ed ADDED
@@ -0,0 +1,141 @@
1
+ #!/usr/bin/env ruby
2
+ require "bundler/setup"
3
+ require "optparse"
4
+ require "get-addic7ed"
5
+
6
+ ARGV << '-h' if ARGV.empty?
7
+
8
+ # Options
9
+ options = {}
10
+
11
+ optparse = OptionParser.new do |opts|
12
+ opts.banner = "Usage :\tget-addic7ed [options] <file1> [<file2>, <file3>, ...]\n"
13
+
14
+ # Help
15
+ opts.on_tail("-h", "--help", "Display this actual help page") do
16
+ puts opts
17
+ exit
18
+ end
19
+
20
+ # All Languages
21
+ opts.on_tail("-L", "--all-languages", "List all available languages") do
22
+ puts "All available languages :"
23
+ GetAddic7ed::LANGUAGES.each do |lang, infos|
24
+ puts "#{lang}:\t#{infos[:name]}"
25
+ end
26
+ exit
27
+ end
28
+
29
+ # Version
30
+ opts.on_tail("-V", "--version", "Get get-addic7ed version") do
31
+ puts "v#{GetAddic7ed::VERSION}"
32
+ exit
33
+ end
34
+
35
+ # Language
36
+ opts.on("-l LANGUAGE", "--language LANGUAGE", "Language code for your subtitle (French by default)") do |l|
37
+ options[:language] = l
38
+ end
39
+
40
+ # Choose manually
41
+ opts.on("-c", "--choose", "Choose your subitle manually") do |c|
42
+ options[:choose] = c
43
+ end
44
+
45
+ #Include Tag
46
+ opts.on("-t", "--tagged", "Include language tag in filename") do |t|
47
+ options[:tagged] = t
48
+ end
49
+
50
+ # Verbose
51
+ opts.on("-v", "--verbose", "Display more details") do |v|
52
+ options[:verbose] = v
53
+ end
54
+
55
+ # Quiet
56
+ opts.on("-q", "--quiet", "Run without output (cron-mode)") do |q|
57
+ options[:quiet] = q
58
+ end
59
+
60
+ # Do not download
61
+ opts.on("-n", "--do-not-download", "Do not download the subtitle") do |n|
62
+ options[:nodownload] = n
63
+ end
64
+
65
+ # Debbug mode
66
+ =begin
67
+ opts.on("-d", "--debug", "Debug mode [do not use]") do |d|
68
+ options[:debug] = d
69
+ end
70
+ =end
71
+
72
+ end
73
+
74
+ optparse.parse!
75
+
76
+ # By default
77
+ options[:filenames] = ARGV
78
+ options[:language] ||= 'fr'
79
+ options[:choose] ||= false
80
+ options[:tagged] ||= false
81
+ options[:verbose] ||= false
82
+ options[:quiet] ||= false
83
+ options[:nodownload] ||= false
84
+
85
+ # Set option in module GetAddic7ed
86
+ GetAddic7ed::OPT_CHOOSE = options[:choose]
87
+ GetAddic7ed::OPT_TAGGED = options[:tagged]
88
+ GetAddic7ed::OPT_VERBOSE = options[:verbose]
89
+ GetAddic7ed::OPT_QUIET = options[:quiet]
90
+ GetAddic7ed::OPT_NODOWNLOAD = options[:nodownload]
91
+
92
+ options[:filenames].each do |filename|
93
+ unless File.file? filename
94
+ puts "File Error : #{filename}\nSkipping..." unless options[:quiet]
95
+ next
96
+ end
97
+
98
+ #TODO if it's a directory explore videos files
99
+
100
+ begin
101
+
102
+ puts "===\tFile\t===" if options[:verbose]
103
+ file = GetAddic7ed::VideoFile.new(ARGV[0])
104
+ file.inspect if options[:verbose]
105
+
106
+ puts "\n===\tShow\t===" if options[:verbose]
107
+ episode = GetAddic7ed::Episode.new( file.filepath )
108
+ episode.inspect if options[:verbose]
109
+
110
+ puts "\n===\tSubtitle\t===" if options[:verbose]
111
+ subtitle = GetAddic7ed::Subtitle.new( episode, options[:language] )
112
+ subtitle.inspect if options[:verbose]
113
+
114
+ unless options[:nodownload]
115
+ puts "\nDownloading..." if options[:verbose]
116
+ subtitle.download_sub
117
+ else
118
+ puts "[Not Downloaded]"
119
+ end
120
+
121
+ rescue GetAddic7ed::InvalidFile
122
+ puts "Error : Argument Is Not A File"
123
+ rescue GetAddic7ed::ConnectionError
124
+ puts "Error : Failed To Reach Addic7ed.com"
125
+ rescue GetAddic7ed::ShowNotFound
126
+ puts "Error : Show Not Found"
127
+ rescue GetAddic7ed::NoSubtitleFound
128
+ puts "Error : No Subtitle Found"
129
+ rescue GetAddic7ed::DownloadError
130
+ puts "Error : Download Error"
131
+ rescue GetAddic7ed::ConnectionError
132
+ puts "Error : Connection Error"
133
+ rescue GetAddic7ed::DownloadLimitReached
134
+ puts "Error : You Have Reach The Download Limit On Addic7ed.com"
135
+ rescue GetAddic7ed::SubtitleCannotBeSaved
136
+ puts "Error : Subtitle Can't Be Save"
137
+ end
138
+
139
+ puts "Done." unless options[:quiet]
140
+ end
141
+
@@ -0,0 +1,53 @@
1
+ module GetAddic7ed
2
+ =begin
3
+ From addic7ed-ruby gem
4
+ =end
5
+
6
+ LANGUAGES = {
7
+ 'ar' => {name: 'Arabic', id: 38},
8
+ 'az' => {name: 'Azerbaijani', id: 48},
9
+ 'bn' => {name: 'Bengali', id: 47},
10
+ 'bs' => {name: 'Bosnian', id: 44},
11
+ 'bg' => {name: 'Bulgarian', id: 35},
12
+ 'ca' => {name: 'Català', id: 12},
13
+ 'cn' => {name: 'Chinese (Simplified)', id: 41},
14
+ 'zh' => {name: 'Chinese (Traditional)', id: 24},
15
+ 'hr' => {name: 'Croatian', id: 31},
16
+ 'cs' => {name: 'Czech', id: 14},
17
+ 'da' => {name: 'Danish', id: 30},
18
+ 'nl' => {name: 'Dutch', id: 17},
19
+ 'en' => {name: 'English', id: 1},
20
+ 'eu' => {name: 'Euskera', id: 13},
21
+ 'fi' => {name: 'Finnish', id: 28},
22
+ 'fr' => {name: 'French', id: 8},
23
+ 'gl' => {name: 'Galego', id: 15},
24
+ 'de' => {name: 'German', id: 11},
25
+ 'el' => {name: 'Greek', id: 27},
26
+ 'he' => {name: 'Hebrew', id: 23},
27
+ 'hu' => {name: 'Hungarian', id: 20},
28
+ 'id' => {name: 'Indonesian', id: 37},
29
+ 'it' => {name: 'Italian', id: 7},
30
+ 'ja' => {name: 'Japanese', id: 32},
31
+ 'ko' => {name: 'Korean', id: 42},
32
+ 'mk' => {name: 'Macedonian', id: 49},
33
+ 'ms' => {name: 'Malay', id: 40},
34
+ 'no' => {name: 'Norwegian', id: 29},
35
+ 'fa' => {name: 'Persian', id: 43},
36
+ 'pl' => {name: 'Polish', id: 21},
37
+ 'pt' => {name: 'Portuguese', id: 9},
38
+ 'pt-br' => {name: 'Portuguese (Brazilian)', id: 10},
39
+ 'ro' => {name: 'Romanian', id: 26},
40
+ 'ru' => {name: 'Russian', id: 19},
41
+ 'sr' => {name: 'Serbian (Cyrillic)', id: 39},
42
+ 'sr-la' => {name: 'Serbian (Latin)', id: 36},
43
+ 'sk' => {name: 'Slovak', id: 25},
44
+ 'sl' => {name: 'Slovenian', id: 22},
45
+ 'es' => {name: 'Spanish', id: 4},
46
+ 'es-la' => {name: 'Spanish (Latin America)', id: 6},
47
+ 'es-es' => {name: 'Spanish (Spain)', id: 5},
48
+ 'sv' => {name: 'Swedish', id: 18},
49
+ 'th' => {name: 'Thai', id: 46},
50
+ 'tr' => {name: 'Turkish', id: 16},
51
+ 'vi' => {name: 'Vietnamese', id: 45}
52
+ }
53
+ end
@@ -0,0 +1,94 @@
1
+ require "nokogiri"
2
+ require "fuzzystringmatch"
3
+ require "open-uri"
4
+
5
+ module GetAddic7ed
6
+ class Episode
7
+
8
+ REGEX_SEASON_EPISODE = /\WS?(?<season>\d{1,2})[XE](?<episode>\d{2})\W|\W(?<seasonepisode>[0-9]{3})\W/i
9
+ attr_reader :id, :title, :season, :episode, :filename, :filepath
10
+
11
+ def initialize filepath
12
+ @filename = File.basename filepath
13
+ @filepath = filepath
14
+
15
+ if results = REGEX_SEASON_EPISODE.match(filename)
16
+ pos = filename.rindex(REGEX_SEASON_EPISODE)
17
+ @title = filename[0, pos].gsub(/[.]|\s/i, ' ')
18
+
19
+ unless results[:seasonepisode]
20
+ @season = results[:season].to_i
21
+ @episode = results[:episode].to_i
22
+ else
23
+ @season = results[:seasonepisode][0].to_i
24
+ @episode = results[:seasonepisode][1,2].to_i
25
+ end
26
+ else
27
+ raise InvalidFilename
28
+ end
29
+
30
+ @id = self.class.get_show_id @title
31
+ raise ShowNotFound if @id == nil
32
+ end
33
+
34
+ #Get all show from addic7ed.com
35
+ def self.get_all_shows
36
+ uri = URI('http://www.addic7ed.com/')
37
+ all_shows = {}
38
+
39
+
40
+ puts "Try to reach Addic7ed.com, please wait..." unless GetAddic7ed::OPT_QUIET
41
+
42
+ begin
43
+ body = open(uri)
44
+ rescue
45
+ raise ConnectionError
46
+ end
47
+
48
+ html = Nokogiri::HTML(body)
49
+
50
+ html.css("#qsShow option").each do |d|
51
+ all_shows[d.text.downcase] = d.attr("value").to_i
52
+ end
53
+
54
+ return all_shows
55
+ end
56
+
57
+ # Get the show id from the list of all shows
58
+ def self.get_show_id show_title
59
+ all_shows = self.get_all_shows
60
+ # Looking in the show list correponding show_title
61
+ id = all_shows[show_title.to_s.downcase]
62
+ # In case we don't have the exact show name (ex: You're The Worst <> You.are.The.Worst)
63
+ if id == nil
64
+ best_result = 0
65
+ best_show_id = 0
66
+ best_show_title = nil
67
+
68
+ jarow = FuzzyStringMatch::JaroWinkler.create( :pure )
69
+ all_shows.each do |show, key|
70
+ matching = jarow.getDistance( show.downcase , show_title.to_s.downcase )
71
+ if matching > best_result
72
+ best_result = matching
73
+ best_show_id = key
74
+ best_show_title = show
75
+ end
76
+ end
77
+
78
+ id = best_show_id
79
+ end
80
+
81
+ return id
82
+ end
83
+
84
+ def inspect
85
+ puts "Filename".ljust(10) + ": #{@filename}"
86
+ puts "Filepath".ljust(10) + ": #{@filepath}"
87
+ puts "Id".ljust(10) + ": #{@id}"
88
+ puts "Title".ljust(10) + ": #{@title}"
89
+ puts "Season".ljust(10) + ": #{@season}"
90
+ puts "Episode".ljust(10) + ": #{@episode}"
91
+ end
92
+
93
+ end
94
+ end
@@ -0,0 +1,14 @@
1
+ module GetAddic7ed
2
+ exceptions = [
3
+ :InvalidFile,
4
+ :InvalidFilename,
5
+ :ConnectionError,
6
+ :ShowNotFound,
7
+ :NoSubtitleFound,
8
+ :DownloadError,
9
+ :DownloadLimitReached,
10
+ :SubtitleCannotBeSaved
11
+ ]
12
+
13
+ exceptions.each { |e| const_set(e, Class.new(StandardError)) }
14
+ end
@@ -0,0 +1,158 @@
1
+ require "nokogiri"
2
+ require "fuzzystringmatch"
3
+ require "open-uri"
4
+ require 'net/http'
5
+
6
+ module GetAddic7ed
7
+ class Subtitle
8
+ attr_reader :episode, :lang, :link, :page_link
9
+
10
+ def initialize episode, lang = 'fr'
11
+ @episode = episode # Episode instance
12
+ @lang = lang
13
+ @page = get_page_link
14
+ @link = get_subtitle_link
15
+ end
16
+
17
+ def inspect
18
+ puts "Sub Lang".ljust(20) + ": #{GetAddic7ed::LANGUAGES[@lang][:name]}"
19
+ puts "Hearing Impaired".ljust(20) + ": #{@hi}"
20
+ puts "Page Link".ljust(20) + ": #{@page}"
21
+ puts "Download Link".ljust(20) + ": #{@link}"
22
+ end
23
+
24
+ def get_page_link
25
+ return "http://www.addic7ed.com/ajax_loadShow.php?show=#{@episode.id}&season=#{@episode.season}&langs=&hd=undeENDed&hi=#{@hi}"
26
+ end
27
+
28
+ def get_subtitle_link
29
+ puts "Searching for <#{GetAddic7ed::LANGUAGES[@lang][:name]}> subtitles..." unless GetAddic7ed::OPT_QUIET
30
+
31
+ # sub list
32
+ sub_list = get_sub_list
33
+
34
+ if sub_list.length == 1
35
+ return sub_list.first[:link]
36
+ elsif sub_list.length == 0
37
+ raise NoSubtitleFound
38
+ else
39
+ return sub_list[choose_sub(sub_list)][:link]
40
+ end
41
+ end
42
+
43
+ def get_sub_list
44
+ sub_list = []
45
+ all_sub_list = []
46
+
47
+ link = get_page_link
48
+ uri = URI(link)
49
+
50
+ begin
51
+ body = open(uri)
52
+ rescue
53
+ raise ConnectionError
54
+ end
55
+
56
+ html = Nokogiri::HTML(body)
57
+
58
+ html.css("#season table tbody tr.epeven").each do |sub|
59
+
60
+ current_sub = {
61
+ season: sub.children[0].text,
62
+ episode: sub.children[1].text,
63
+ lang: sub.children[3].text,
64
+ group: sub.children[4].text,
65
+ completed: sub.children[6].text,
66
+ link: "http://www.addic7ed.com#{sub.children[10].children.first['href']}"
67
+ }
68
+
69
+ sub_list.push (current_sub) if (
70
+ sub.children[0].text.to_i == @episode.season &&
71
+ sub.children[1].text.to_i == @episode.episode &&
72
+ sub.children[3].text.downcase == LANGUAGES[@lang][:name].downcase &&
73
+ sub.children[6].text == 'Completed'
74
+ )
75
+
76
+ all_sub_list.push ( current_sub ) if (
77
+ sub.children[0].text.to_i == @episode.season &&
78
+ sub.children[1].text.to_i == @episode.episode
79
+ )
80
+
81
+ next
82
+ end
83
+
84
+ if GetAddic7ed::OPT_CHOOSE
85
+ return all_sub_list
86
+ end
87
+
88
+ if sub_list.length > 0
89
+ # Looking for the corresponding subtitle sub group in filename
90
+ sub_list.each_index do |i|
91
+ if (@episode.filename.downcase =~ /#{Regexp.escape( sub_list[i][:group].downcase )}/ ) != nil
92
+ puts "> #{@episode.title} : S#{sub_list[i][:season].rjust(2, '0')}E#{sub_list[i][:episode].rjust(2, '0')}, #{sub_list[i][:lang]}, #{sub_list[i][:group]}" unless GetAddic7ed::OPT_QUIET
93
+
94
+ return Array.new.push( sub_list[i] )
95
+ end
96
+ end
97
+ end
98
+
99
+ return sub_list.length > 0 ? sub_list : all_sub_list
100
+ end
101
+
102
+ def choose_sub sub_list
103
+ if GetAddic7ed::OPT_CHOOSE
104
+ puts "Choose a subtitle :"
105
+ else
106
+ puts "No perfect match found.\nBut you can choose one of those instead :"
107
+ end
108
+
109
+ sub_list.each_index do |i|
110
+ print "\n#{i+1} -".ljust(6)
111
+ print "#{sub_list[i][:group]}".ljust(25)
112
+ print "#{sub_list[i][:lang]}".ljust(25)
113
+ print "#{sub_list[i][:completed]}".ljust(20)
114
+ end
115
+
116
+ puts "\n\nType 'exit' or '0' or 'q' to cancel.\n\n"
117
+
118
+ choice = -1
119
+
120
+ until (choice.to_i >= 1 && choice.to_i <= sub_list.length)
121
+ puts "* Which subtitle do you want ?"
122
+ print "> "
123
+ choice = STDIN.gets.chomp
124
+
125
+ if choice === "exit" || choice === "0" || choice === "q"
126
+ puts "Done."
127
+ exit
128
+ else
129
+ choice = choice.to_i
130
+ end
131
+ end
132
+
133
+ return choice-1
134
+ end
135
+
136
+ def download_sub
137
+ dir_path = File.dirname(@episode.filepath)
138
+ sub_name = File.basename(@episode.filepath, '.*')
139
+
140
+ begin
141
+ srt_filename = "#{dir_path}/#{sub_name}#{('.' + @lang) if GetAddic7ed::OPT_TAGGED}.srt"
142
+
143
+ #TODO check download limit /^\/downloadexceeded.php/.match
144
+
145
+ File.open(srt_filename, "wb") do |saved_file|
146
+ open("#{@link}", "rb", "Referer" => @page) do |read_file|
147
+ saved_file.write(read_file.read)
148
+ end
149
+ end
150
+ puts "Subtitle downloaded !" unless GetAddic7ed::OPT_QUIET
151
+ puts srt_filename unless GetAddic7ed::OPT_QUIET
152
+ rescue
153
+ raise DownloadError
154
+ end
155
+ end
156
+
157
+ end
158
+ end
@@ -0,0 +1,3 @@
1
+ module GetAddic7ed
2
+ VERSION = "0.1.0"
3
+ end
@@ -0,0 +1,40 @@
1
+ module GetAddic7ed
2
+ class VideoFile
3
+ attr_reader :filename, :filepath
4
+
5
+ def initialize(file)
6
+ @filepath = File.expand_path(file)
7
+
8
+ if self.class.file? (@filepath)
9
+ @filename = File.basename(@filepath)
10
+ else
11
+ raise InvalidVideoFile
12
+ end
13
+ end
14
+
15
+ def inspect
16
+ puts "Filename".ljust(10) + ": #{@filename}"
17
+ puts "Filepath".ljust(10) + ": #{@filepath}"
18
+ end
19
+
20
+ def self.file? filepath
21
+ if File.file? (filepath)
22
+ return true
23
+ else
24
+ raise InvalidFile
25
+ end
26
+ end
27
+
28
+ def self.video? filepath
29
+ if File.file? (filepath)
30
+ =begin
31
+ TODO : Find a way to make it work on windows, mac os x and linux
32
+ =end
33
+ return mimetype(filepath).start_with?("video") || File.extname(filepath) == ".mkv"
34
+ else
35
+ raise WrongArgument
36
+ end
37
+ end
38
+
39
+ end
40
+ end
@@ -0,0 +1,6 @@
1
+ require "addic7ed/version"
2
+ require 'addic7ed/attr'
3
+ require 'addic7ed/errors'
4
+ require 'addic7ed/videofile'
5
+ require 'addic7ed/episode'
6
+ require 'addic7ed/subtitle'
metadata ADDED
@@ -0,0 +1,109 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: get-addic7ed
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Nicolas Vergoz
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2016-01-27 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.11'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.11'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '10.0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '10.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: nokogiri
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '1.6'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '1.6'
55
+ - !ruby/object:Gem::Dependency
56
+ name: fuzzy-string-match
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ description: Ruby tool to download subtitles from Addic7d based on video file name
70
+ email:
71
+ - nicolas.vergoz@gmail.com
72
+ executables:
73
+ - get-addic7ed
74
+ extensions: []
75
+ extra_rdoc_files: []
76
+ files:
77
+ - bin/get-addic7ed
78
+ - lib/addic7ed/attr.rb
79
+ - lib/addic7ed/episode.rb
80
+ - lib/addic7ed/errors.rb
81
+ - lib/addic7ed/subtitle.rb
82
+ - lib/addic7ed/version.rb
83
+ - lib/addic7ed/videofile.rb
84
+ - lib/get-addic7ed.rb
85
+ homepage: http://rubygems.org/gems/get-addic7ed
86
+ licenses:
87
+ - MIT
88
+ metadata: {}
89
+ post_install_message:
90
+ rdoc_options: []
91
+ require_paths:
92
+ - lib
93
+ required_ruby_version: !ruby/object:Gem::Requirement
94
+ requirements:
95
+ - - ">="
96
+ - !ruby/object:Gem::Version
97
+ version: '0'
98
+ required_rubygems_version: !ruby/object:Gem::Requirement
99
+ requirements:
100
+ - - ">="
101
+ - !ruby/object:Gem::Version
102
+ version: '0'
103
+ requirements: []
104
+ rubyforge_project:
105
+ rubygems_version: 2.4.5.1
106
+ signing_key:
107
+ specification_version: 4
108
+ summary: Get subtitles from Addic7ed
109
+ test_files: []