getvideo 0.0.4
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +20 -0
- data/.rspec +2 -0
- data/Gemfile +15 -0
- data/Guardfile +24 -0
- data/LICENSE +22 -0
- data/README.md +56 -0
- data/Rakefile +2 -0
- data/getvideo.gemspec +21 -0
- data/lib/getvideo.rb +38 -0
- data/lib/getvideo/56.rb +60 -0
- data/lib/getvideo/iask.rb +173 -0
- data/lib/getvideo/iqiyi.rb +80 -0
- data/lib/getvideo/ku6.rb +56 -0
- data/lib/getvideo/sohu.rb +77 -0
- data/lib/getvideo/tudou.rb +163 -0
- data/lib/getvideo/version.rb +3 -0
- data/lib/getvideo/video.rb +126 -0
- data/lib/getvideo/youku.rb +82 -0
- data/lib/getvideo/youtube.rb +65 -0
- data/spec/56_spec.rb +50 -0
- data/spec/getvideo_spec.rb +13 -0
- data/spec/iask_spec.rb +128 -0
- data/spec/iqiyi_spec.rb +42 -0
- data/spec/ku6_spec.rb +39 -0
- data/spec/sohu_spec.rb +62 -0
- data/spec/spec_helper.rb +20 -0
- data/spec/tudou_spec.rb +155 -0
- data/spec/youku_spec.rb +47 -0
- data/spec/youtube_spec.rb +49 -0
- metadata +126 -0
@@ -0,0 +1,80 @@
|
|
1
|
+
#coding:utf-8
|
2
|
+
|
3
|
+
module Getvideo
|
4
|
+
class Iqiyi < Video
|
5
|
+
set_api_uri { "http://cache.video.qiyi.com/v/#{id}" }
|
6
|
+
attr_reader :ipad_response
|
7
|
+
|
8
|
+
def initialize(url)
|
9
|
+
super
|
10
|
+
ipad_connection
|
11
|
+
end
|
12
|
+
|
13
|
+
def html_url
|
14
|
+
response.css("videoUrl").text
|
15
|
+
end
|
16
|
+
|
17
|
+
def id
|
18
|
+
if url =~ /\.html/
|
19
|
+
parse_vid[1]
|
20
|
+
elsif url =~ /swf/
|
21
|
+
url.scan(/com\/([^\/]+)/)[0][0]
|
22
|
+
else
|
23
|
+
url
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def title
|
28
|
+
response.css("title").text
|
29
|
+
end
|
30
|
+
|
31
|
+
def cover
|
32
|
+
response.css("albumImage").text
|
33
|
+
end
|
34
|
+
|
35
|
+
def m3u8
|
36
|
+
ipad_response["data"]["mtl"][1]["m3u"]
|
37
|
+
end
|
38
|
+
|
39
|
+
def flash
|
40
|
+
position = response.css("logoPosition").text
|
41
|
+
duration = response.css("totalDuration").text
|
42
|
+
video_url = response.css("videoUrl").text.gsub("http://www.iqiyi.com/","")
|
43
|
+
"http://player.video.qiyi.com/#{id}/#{position}/#{duration}/#{video_url}"[0..-5]+"swf"
|
44
|
+
end
|
45
|
+
|
46
|
+
def media
|
47
|
+
video_list = {}
|
48
|
+
video_list["mp4"] = []
|
49
|
+
video_list["mp4"] << parse_mp4
|
50
|
+
video_list["ts"] = []
|
51
|
+
file = response.css("file")
|
52
|
+
size = response.css("size")
|
53
|
+
file.zip(size).each do |f|
|
54
|
+
video_list["ts"] << f[0].content[0..-4] + "ts?start=0&end=99999999&hsize=" + f[1].text
|
55
|
+
end
|
56
|
+
return video_list
|
57
|
+
end
|
58
|
+
|
59
|
+
private
|
60
|
+
|
61
|
+
def ipad_connection
|
62
|
+
conn = Faraday.new
|
63
|
+
response= conn.get "http://cache.video.qiyi.com/m/#{id}/"
|
64
|
+
@ipad_response = MultiJson.load response.body.scan(/ipadUrl=([^*]+)/)[0][0]
|
65
|
+
end
|
66
|
+
|
67
|
+
def parse_mp4(url=nil)
|
68
|
+
site = ipad_response["data"]["mp4Url"]
|
69
|
+
conn = Faraday.new
|
70
|
+
res = conn.get(site)
|
71
|
+
res.body.scan(/"l":"([^,]+)"/)[0][0]
|
72
|
+
end
|
73
|
+
|
74
|
+
def parse_vid
|
75
|
+
conn = Faraday.new
|
76
|
+
page_res = conn.get(url)
|
77
|
+
return page_res.body.match(/data-player-videoid\s*[:=]\s*["']([^"']+)["']/)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
data/lib/getvideo/ku6.rb
ADDED
@@ -0,0 +1,56 @@
|
|
1
|
+
#coding:utf-8
|
2
|
+
|
3
|
+
module Getvideo
|
4
|
+
class Ku6 < Video
|
5
|
+
def html_url
|
6
|
+
if url =~ /\.html/
|
7
|
+
url
|
8
|
+
else
|
9
|
+
"http://v.ku6.com/show/#{id}.html"
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
def title
|
14
|
+
response["data"]["t"]
|
15
|
+
end
|
16
|
+
|
17
|
+
def id
|
18
|
+
if url =~ /\.html/
|
19
|
+
url.split("/").last.split(".html")[0]
|
20
|
+
elsif url =~ /\.swf/
|
21
|
+
url.split("/")[-2]
|
22
|
+
else
|
23
|
+
url
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def cover
|
28
|
+
response["data"]["bigpicpath"]
|
29
|
+
end
|
30
|
+
|
31
|
+
def flash
|
32
|
+
"http://player.ku6.com/refer/#{id}/v.swf"
|
33
|
+
end
|
34
|
+
|
35
|
+
def m3u8
|
36
|
+
"http://v.ku6.com/fetchwebm/#{id}.m3u8"
|
37
|
+
end
|
38
|
+
|
39
|
+
def media
|
40
|
+
video_list = {}
|
41
|
+
video_list["f4v"] = []
|
42
|
+
response["data"]["f"].split(",").each do |f|
|
43
|
+
video_list["f4v"] << f
|
44
|
+
end
|
45
|
+
return video_list
|
46
|
+
end
|
47
|
+
|
48
|
+
private
|
49
|
+
|
50
|
+
def connection
|
51
|
+
conn = Faraday.new
|
52
|
+
response= conn.post "http://v.ku6.com/fetch.htm", {"t" => "getVideo4Player", "vid" => id}
|
53
|
+
@response = Response.new(response).parsed
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,77 @@
|
|
1
|
+
#coding:utf-8
|
2
|
+
|
3
|
+
module Getvideo
|
4
|
+
class Sohu < Video
|
5
|
+
set_api_uri do
|
6
|
+
if url =~ /(my\.tv|my\/v\.swf|\|my)/
|
7
|
+
"http://my.tv.sohu.com/videinfo.jhtml?m=viewnew&vid=#{id}"
|
8
|
+
else
|
9
|
+
"http://hot.vrs.sohu.com/vrs_flash.action?vid=#{id}"
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
def html_url
|
14
|
+
response["url"]
|
15
|
+
end
|
16
|
+
|
17
|
+
def id
|
18
|
+
if url =~ /my\.tv.+\/(?:pl|us)\/(?:[^\D]+)\/([^\D]+)/
|
19
|
+
u = url.scan(/my\.tv.+\/(?:pl|us)\/(?:[^\D]+)\/([^\D]+)/)[0][0]
|
20
|
+
elsif url =~ /(\.shtml|my\.tv.+\/user\/detail)/
|
21
|
+
parse_vid[1]
|
22
|
+
elsif url =~ /v\.swf/
|
23
|
+
url.scan(/(sohu\.com\/([^\D]+)\/v.swf|id=([^\D]+))/)[0].compact[1]
|
24
|
+
elsif url =~ /\|my/
|
25
|
+
url.split("|")[0]
|
26
|
+
else
|
27
|
+
url
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def title
|
32
|
+
response["data"]["tvName"]
|
33
|
+
end
|
34
|
+
|
35
|
+
def cover
|
36
|
+
response["data"]["coverImg"]
|
37
|
+
end
|
38
|
+
|
39
|
+
def flash
|
40
|
+
if url =~ /(my\.tv)/
|
41
|
+
"http://share.vrs.sohu.com/my/v.swf&topBar=1&id=#{id}&autoplay=false"
|
42
|
+
else
|
43
|
+
"http://share.vrs.sohu.com/#{id}/v.swf&autoplay=false"
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def m3u8
|
48
|
+
if url =~ /(my\.tv)/
|
49
|
+
"http://my.tv.sohu.com/ipad/#{id}.m3u8"
|
50
|
+
else
|
51
|
+
"http://hot.vrs.sohu.com/ipad#{id}.m3u8"
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def media
|
56
|
+
host = response["allot"]
|
57
|
+
prot = response["prot"]
|
58
|
+
clips = response["data"]["clipsURL"]
|
59
|
+
new = response["data"]["su"]
|
60
|
+
clips_len = clips.length
|
61
|
+
new_len = new.length
|
62
|
+
video_list = {}
|
63
|
+
video_list["mp4"] = []
|
64
|
+
clips.zip(new).each do | c, n |
|
65
|
+
h, _, _, key = Net::HTTP.get(URI.parse("http://#{host}?prot=#{prot}&file=#{c}&new=#{n}")).split("|")
|
66
|
+
video_list["mp4"] << "#{h}#{n}?key=#{key}"
|
67
|
+
end
|
68
|
+
return video_list
|
69
|
+
end
|
70
|
+
|
71
|
+
def parse_vid
|
72
|
+
conn = Faraday.new
|
73
|
+
page_res = conn.get(url)
|
74
|
+
page_res.body.match(/vid[\s]*=[\s]*["|']?(\d+)["|']?/)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
@@ -0,0 +1,163 @@
|
|
1
|
+
#coding:utf-8
|
2
|
+
|
3
|
+
module Getvideo
|
4
|
+
class Tudou < Video
|
5
|
+
attr_reader :info
|
6
|
+
|
7
|
+
def initialize(url)
|
8
|
+
@url = url
|
9
|
+
parse_html
|
10
|
+
tudou_connect
|
11
|
+
@body = Nokogiri::XML(response)
|
12
|
+
end
|
13
|
+
|
14
|
+
def id
|
15
|
+
iid
|
16
|
+
end
|
17
|
+
|
18
|
+
def html_url
|
19
|
+
if url =~ /\/(a|o|l)\/.*.\.swf/
|
20
|
+
type = url.scan(/\/(a|o|l)\/.*.\.swf/)[0][0]
|
21
|
+
case type
|
22
|
+
when "a"
|
23
|
+
"http://www.tudou.com/albumplay/#{acode}#{"/" + lcode if lcode }.html"
|
24
|
+
when "o"
|
25
|
+
"http://www.tudou.com/oplay/#{acode}#{"/" + lcode if lcode}.html"
|
26
|
+
when "l"
|
27
|
+
"http://www.tudou.com/listplay/#{acode}#{"/" + lcode if lcode}.html"
|
28
|
+
end
|
29
|
+
elsif url =~ /dp\.tudou.com\/(a|o|l|v|albumplay|oplay|listplay)\//
|
30
|
+
type = url.scan(/dp\.tudou.com\/(a|o|l|v|albumplay|oplay|listplay)\/.*.\.html/)[0][0]
|
31
|
+
case type
|
32
|
+
when "a", "albumplay"
|
33
|
+
"http://www.tudou.com/albumplay/#{acode}#{"/" + lcode if lcode}.html"
|
34
|
+
when "o", "oplay"
|
35
|
+
"http://www.tudou.com/oplay/#{acode}#{"/" + lcode if lcode}.html"
|
36
|
+
when "l", "listplay"
|
37
|
+
"http://www.tudou.com/listplay/#{acode}#{"/" + lcode if lcode}.html"
|
38
|
+
when "v"
|
39
|
+
"http://www.tudou.com/programs/view/#{lcode}/"
|
40
|
+
end
|
41
|
+
elsif url =~ /www\.tudou.com\/(programs|albumplay|listplay|oplay)/
|
42
|
+
url
|
43
|
+
else
|
44
|
+
"http://www.tudou.com/programs/view/#{lcode}/"
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def title
|
49
|
+
@body.children()[0].attr("title")
|
50
|
+
end
|
51
|
+
|
52
|
+
def cover
|
53
|
+
@info.match(/pic\s*:\s*(\S+)/)[1].delete("\"").gsub(/["|']/,"")
|
54
|
+
end
|
55
|
+
|
56
|
+
def flash
|
57
|
+
if url =~ /\/(v|a|o|l)\/.*.\.swf/
|
58
|
+
url
|
59
|
+
elsif url =~ /\/(albumplay|listplay|oplay)/
|
60
|
+
url_type = url.scan(/\/((a)lbumplay|(l)istplay|(o)play)/)[0].compact()[1]
|
61
|
+
"http://www.tudou.com/#{url_type}/#{acode}/&rpid=116105338&resourceId=116105338_04_05_99&iid=#{iid}/v.swf"
|
62
|
+
elsif url =~ /dp\.tudou\.com\/(l|a|o)/
|
63
|
+
url_type = url.scan(/dp\.tudou.com\/(a|l|o)/)[0][0]
|
64
|
+
"http://www.tudou.com/#{url_type}/#{acode}/&rpid=116105338&resourceId=116105338_04_05_99&iid=#{iid}/v.swf"
|
65
|
+
elsif url =~ /dp\.tudou\.com\/v/
|
66
|
+
"http://www.tudou.com/v/#{lcode}/&rpid=116105338&resourceId=116105338_04_05_99/v.swf"
|
67
|
+
else
|
68
|
+
"http://www.tudou.com/v/#{lcode}/&rpid=116105338&resourceId=116105338_04_05_99/v.swf"
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def m3u8
|
73
|
+
"http://vr.tudou.com/v2proxy/v2.m3u8?it=#{iid}&st=2&pw="
|
74
|
+
end
|
75
|
+
|
76
|
+
def media
|
77
|
+
video_list = {}
|
78
|
+
video_list["f4v"] = []
|
79
|
+
video_list["flv"] = []
|
80
|
+
old_brt = ""
|
81
|
+
@body.css("f").each do | file |
|
82
|
+
brt = file.attr("brt")
|
83
|
+
if brt != old_brt
|
84
|
+
if file.content =~ /f4v/
|
85
|
+
video_list["f4v"] << file.content
|
86
|
+
elsif file.content =~ /flv/
|
87
|
+
video_list["flv"] << file.content
|
88
|
+
end
|
89
|
+
old_brt = brt
|
90
|
+
end
|
91
|
+
end
|
92
|
+
return video_list
|
93
|
+
end
|
94
|
+
|
95
|
+
private
|
96
|
+
|
97
|
+
def tudou_connect
|
98
|
+
uri = URI.parse "http://v2.tudou.com/v?it=#{iid}"
|
99
|
+
http = Net::HTTP.new uri.host, uri.port
|
100
|
+
req = Net::HTTP::Get.new(uri.request_uri,{"User-Agent"=> ""})
|
101
|
+
res = http.request req
|
102
|
+
@response = res.body
|
103
|
+
end
|
104
|
+
|
105
|
+
def lcode
|
106
|
+
if url =~ /\/v\/.*.\.swf/
|
107
|
+
url.scan(/\/v\/([^\/]+)\//)[0][0]
|
108
|
+
elsif url =~ /dp\.tudou\.com\/(l|a|o|listplay|albumplay|oplay)\/([^.]+)\/([^.]+)\.html/
|
109
|
+
url.scan(/dp\.tudou\.com\/(?:l|a|o|listplay|albumplay|oplay)\/([^.]+)\/([^.]+)\.html/)[0][1]
|
110
|
+
elsif url =~ /dp\.tudou\.com\/(l|a|o|listplay|albumplay|oplay)\/([^.]+)\.html/
|
111
|
+
nil
|
112
|
+
elsif url =~ /dp\.tudou\.com\/v\/([^.]+)\.html/
|
113
|
+
url.scan(/dp\.tudou\.com\/v\/([^.]+)\.html/)[0][0]
|
114
|
+
else
|
115
|
+
tudou_connect
|
116
|
+
Nokogiri::XML(response).children()[0].attr("code")
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
def acode
|
121
|
+
if url =~ /\/(a|o|l)\/.*.\.swf/
|
122
|
+
url.scan(/\/[a|o|l]\/([^\/]+)\//)[0][0]
|
123
|
+
elsif url =~ /\/(listplay|albumplay|oplay)\/([^.]+)\/([^.]+)\.html/
|
124
|
+
url.scan(/\/(?:listplay|albumplay|oplay)\/([^.]+)\/([^.]+)\.html/)[0][0]
|
125
|
+
elsif url =~ /dp\.tudou\.com\/(l|a|o)\/([^.]+)\/([^.]+)\.html/
|
126
|
+
url.scan(/dp\.tudou\.com\/(?:l|a|o)\/([^.]+)\/([^.]+)\.html/)[0][0]
|
127
|
+
elsif url =~ /dp\.tudou\.com\/(l|a|o)\/([^.]+)\.html/
|
128
|
+
url.scan(/dp\.tudou\.com\/(?:l|a|o)\/([^.]+)\.html/)[0][0]
|
129
|
+
|
130
|
+
elsif url =~ /\/(listplay|albumplay|oplay)\/([^.]+)\.html/
|
131
|
+
url.scan(/\/(?:listplay|albumplay|oplay)\/([^.]+)\.html/)[0][0]
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
def iid
|
136
|
+
if url =~ /\/(programs|albumplay|listplay|oplay)/
|
137
|
+
get_iid[0][0]
|
138
|
+
elsif url =~ /\/v\/.*.\.swf/
|
139
|
+
get_iid[0][0]
|
140
|
+
elsif url =~ /\/(a|o|l)\/.*.\.swf/
|
141
|
+
url.scan(/iid=([^\/]+)/)[0][0]
|
142
|
+
elsif url =~ /dp\.tudou\.com\/(a|o|l|v)\//
|
143
|
+
get_iid[0][0]
|
144
|
+
elsif url =~ /dp\.tudou\.com\/player\.php/
|
145
|
+
url.scan(/player\.php\?id=([^&]+)/)[0][0]
|
146
|
+
elsif url =~ /dp.tudou.com\/nplayer\.swf/
|
147
|
+
url.scan(/nplayer\.swf\?([^&]+)/)[0][0]
|
148
|
+
else
|
149
|
+
url
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
def get_iid
|
154
|
+
@info.scan(/iid\s*:\s*(\S+)/)
|
155
|
+
end
|
156
|
+
|
157
|
+
def parse_html
|
158
|
+
conn = Faraday.new
|
159
|
+
res = conn.get html_url
|
160
|
+
@info = res.body
|
161
|
+
end
|
162
|
+
end
|
163
|
+
end
|
@@ -0,0 +1,126 @@
|
|
1
|
+
#coding:utf-8
|
2
|
+
begin
|
3
|
+
require 'faraday'
|
4
|
+
rescue LoadError
|
5
|
+
raise "You don't have the 'faraday' gem installed"
|
6
|
+
end
|
7
|
+
|
8
|
+
module Getvideo
|
9
|
+
class Video
|
10
|
+
attr_reader :url, :response
|
11
|
+
|
12
|
+
def initialize(url)
|
13
|
+
@url = url
|
14
|
+
connection
|
15
|
+
end
|
16
|
+
|
17
|
+
def connection
|
18
|
+
api_url = self.class.get_api_uri(self)
|
19
|
+
conn = Faraday.new
|
20
|
+
response= conn.get api_url
|
21
|
+
@response = Response.new(response).parsed
|
22
|
+
end
|
23
|
+
|
24
|
+
def id; end
|
25
|
+
def html_url; end
|
26
|
+
def cover; end
|
27
|
+
def title; end
|
28
|
+
def flash; end
|
29
|
+
def m3u8; end
|
30
|
+
def media; end
|
31
|
+
|
32
|
+
def play_media
|
33
|
+
media["mp4"][0] if media["mp4"]
|
34
|
+
end
|
35
|
+
|
36
|
+
def json
|
37
|
+
{id: id,
|
38
|
+
url: html_url,
|
39
|
+
cover: cover,
|
40
|
+
title: title,
|
41
|
+
m3u8: m3u8,
|
42
|
+
flash: flash,
|
43
|
+
media: play_media}.to_json
|
44
|
+
end
|
45
|
+
|
46
|
+
class << self
|
47
|
+
def set_api_uri(&block)
|
48
|
+
return @api_uri unless block_given?
|
49
|
+
@api_uri = block
|
50
|
+
end
|
51
|
+
|
52
|
+
def get_api_uri(klass)
|
53
|
+
klass.instance_eval(&set_api_uri)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
class Response
|
59
|
+
attr_reader :response
|
60
|
+
|
61
|
+
CONTENT_TYPE = {
|
62
|
+
'application/json' => :json,
|
63
|
+
'application/x-www-form-urlencoded' => :html,
|
64
|
+
'text/html' => :html,
|
65
|
+
'text/javascript' => :json,
|
66
|
+
'text/xml' => :xml
|
67
|
+
}
|
68
|
+
|
69
|
+
PARSERS = {
|
70
|
+
:json => lambda{ |body| MultiJson.respond_to?(:adapter) ? MultiJson.load(body) : MultiJson.decode(body) rescue body },
|
71
|
+
:html => lambda{ |body| Nokogiri::HTML(body) },
|
72
|
+
:xml => lambda{ |body| Nokogiri::XML(body) }
|
73
|
+
}
|
74
|
+
|
75
|
+
def initialize(response)
|
76
|
+
@response = response
|
77
|
+
end
|
78
|
+
|
79
|
+
def headers
|
80
|
+
response.headers
|
81
|
+
end
|
82
|
+
|
83
|
+
def body
|
84
|
+
decode(response.body)
|
85
|
+
end
|
86
|
+
|
87
|
+
def decode(body)
|
88
|
+
return '' if !body
|
89
|
+
return body if json?
|
90
|
+
charset = body.match(/charset\s*=[\s|\W]*([\w-]+)/)
|
91
|
+
if charset[1].downcase != "utf-8"
|
92
|
+
begin
|
93
|
+
body.encode! "utf-8", charset[1], {:invalid => :replace}
|
94
|
+
rescue
|
95
|
+
body
|
96
|
+
end
|
97
|
+
else
|
98
|
+
body
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
def status
|
103
|
+
response.status
|
104
|
+
end
|
105
|
+
|
106
|
+
def content_type
|
107
|
+
((response.headers.values_at('content-type', 'Content-Type').compact.first || '').split(';').first || '').strip
|
108
|
+
end
|
109
|
+
|
110
|
+
def json?
|
111
|
+
CONTENT_TYPE[content_type] == :json || !response.body.match(/\<html/)
|
112
|
+
end
|
113
|
+
|
114
|
+
def parser
|
115
|
+
type = CONTENT_TYPE[content_type]
|
116
|
+
type = :json if type == :html && !response.body.match(/\<html/)
|
117
|
+
return type
|
118
|
+
end
|
119
|
+
|
120
|
+
def parsed
|
121
|
+
return nil unless CONTENT_TYPE.key?(content_type)
|
122
|
+
return nil unless PARSERS.key?(parser)
|
123
|
+
@parsed ||= PARSERS[parser].call(body)
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|