marcandre-flvedit 0.6.1
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/CHANGELOG.rdoc +5 -0
- data/LICENSE +24 -0
- data/README.rdoc +90 -0
- data/Rakefile +131 -0
- data/VERSION.yml +4 -0
- data/bin/flvedit +14 -0
- data/lib/flv.rb +24 -0
- data/lib/flv/audio.rb +66 -0
- data/lib/flv/base.rb +38 -0
- data/lib/flv/body.rb +57 -0
- data/lib/flv/edit.rb +20 -0
- data/lib/flv/edit/options.rb +162 -0
- data/lib/flv/edit/processor.rb +3 -0
- data/lib/flv/edit/processor/add.rb +67 -0
- data/lib/flv/edit/processor/base.rb +209 -0
- data/lib/flv/edit/processor/command_line.rb +23 -0
- data/lib/flv/edit/processor/cut.rb +27 -0
- data/lib/flv/edit/processor/debug.rb +30 -0
- data/lib/flv/edit/processor/head.rb +16 -0
- data/lib/flv/edit/processor/join.rb +52 -0
- data/lib/flv/edit/processor/meta_data_maker.rb +127 -0
- data/lib/flv/edit/processor/print.rb +13 -0
- data/lib/flv/edit/processor/printer.rb +27 -0
- data/lib/flv/edit/processor/reader.rb +30 -0
- data/lib/flv/edit/processor/save.rb +28 -0
- data/lib/flv/edit/processor/update.rb +27 -0
- data/lib/flv/edit/runner.rb +23 -0
- data/lib/flv/edit/version.rb +15 -0
- data/lib/flv/event.rb +40 -0
- data/lib/flv/file.rb +41 -0
- data/lib/flv/header.rb +37 -0
- data/lib/flv/packing.rb +140 -0
- data/lib/flv/tag.rb +62 -0
- data/lib/flv/timestamp.rb +124 -0
- data/lib/flv/util/double_check.rb +22 -0
- data/lib/flv/video.rb +73 -0
- data/test/fixtures/corrupted.flv +0 -0
- data/test/fixtures/short.flv +0 -0
- data/test/fixtures/tags.xml +39 -0
- data/test/test_flv.rb +145 -0
- data/test/test_flv_edit.rb +32 -0
- data/test/test_flv_edit_results.rb +27 -0
- data/test/test_helper.rb +9 -0
- data/test/text_flv_edit_results/add_tags.txt +132 -0
- data/test/text_flv_edit_results/cut_from.txt +114 -0
- data/test/text_flv_edit_results/cut_key.txt +20 -0
- data/test/text_flv_edit_results/debug.txt +132 -0
- data/test/text_flv_edit_results/debug_limited.txt +18 -0
- data/test/text_flv_edit_results/debug_range.txt +32 -0
- data/test/text_flv_edit_results/join.txt +237 -0
- data/test/text_flv_edit_results/print.txt +16 -0
- data/test/text_flv_edit_results/stop.txt +38 -0
- data/test/text_flv_edit_results/update.txt +33 -0
- metadata +134 -0
data/lib/flv/event.rb
ADDED
@@ -0,0 +1,40 @@
|
|
1
|
+
module FLV
|
2
|
+
# The body of a tag containing meta data, cue points or last second information
|
3
|
+
# These behave like a Hash. The keys should be symbols
|
4
|
+
# while the values can be about any type, including arrays and hashes.
|
5
|
+
class Event < Hash
|
6
|
+
include Packable
|
7
|
+
include Body
|
8
|
+
TYPICAL_EVENTS = [:onMetaData, :onCuePoint, :onCaption, :onCaptionInfo, :onLastSecond, :onEvent]
|
9
|
+
attr_accessor :event
|
10
|
+
|
11
|
+
def initialize(event = :onMetaData, h = {})
|
12
|
+
self.replace h
|
13
|
+
self.event = event.to_sym
|
14
|
+
end
|
15
|
+
|
16
|
+
def read_packed(io,options) #:nodoc:
|
17
|
+
len = io.pos_change do
|
18
|
+
evt, h = io >>:flv_value >>:flv_value
|
19
|
+
self.event = evt.to_sym
|
20
|
+
replace h
|
21
|
+
end
|
22
|
+
FLV::Util.double_check :size, options[:bytes], len
|
23
|
+
end
|
24
|
+
|
25
|
+
def write_packed(io,*) #:nodoc:
|
26
|
+
io << [event.to_s, :flv_value] << [self, :flv_value]
|
27
|
+
end
|
28
|
+
|
29
|
+
def debug(format, *)
|
30
|
+
format.values(:event => event)
|
31
|
+
format.values(self)
|
32
|
+
end
|
33
|
+
|
34
|
+
def is?(what)
|
35
|
+
event.to_s == what.to_s || super
|
36
|
+
end
|
37
|
+
|
38
|
+
alias_method :similar_to?, :==
|
39
|
+
end
|
40
|
+
end
|
data/lib/flv/file.rb
ADDED
@@ -0,0 +1,41 @@
|
|
1
|
+
module FLV
|
2
|
+
module File
|
3
|
+
def each(*arg, &block)
|
4
|
+
return super unless arg.empty?
|
5
|
+
return to_enum unless block_given?
|
6
|
+
h= read(Header)
|
7
|
+
yield h
|
8
|
+
super(Tag, &block)
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.open(*arg)
|
12
|
+
file = ::File.open(*arg)
|
13
|
+
begin
|
14
|
+
file = return_value = file.packed.extend(File)
|
15
|
+
rescue Exception
|
16
|
+
file.close
|
17
|
+
raise
|
18
|
+
end
|
19
|
+
begin
|
20
|
+
return_value = yield(file)
|
21
|
+
ensure
|
22
|
+
file.close
|
23
|
+
end if block_given?
|
24
|
+
return_value
|
25
|
+
end
|
26
|
+
|
27
|
+
def self.read(portname, *arg)
|
28
|
+
open(portname) do |f|
|
29
|
+
return f.to_a if arg.empty?
|
30
|
+
n, offset = arg.first, arg[1] || 0
|
31
|
+
f.each.first(n+offset)[offset, offset+n-1]
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def self.foreach(portname, *, &block)
|
36
|
+
open(portname) do |f|
|
37
|
+
f.each(&block)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
data/lib/flv/header.rb
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
module FLV
|
2
|
+
# Represents the header chunk present at the very beginning of any FLV file.
|
3
|
+
class Header
|
4
|
+
include Base
|
5
|
+
attr_accessor :version, :has_audio, :has_video, :extra, :path
|
6
|
+
|
7
|
+
FLV_SIGNATURE = [String, {:bytes => 3}].freeze
|
8
|
+
SIGNATURE = 'FLV'
|
9
|
+
MIN_OFFSET = 9
|
10
|
+
FLAGS = { :has_video => 1, :has_audio => 4 }.freeze
|
11
|
+
|
12
|
+
def read_packed(io, *) #:nodoc:
|
13
|
+
signature, self.version, type_flags, offset = \
|
14
|
+
io >> FLV_SIGNATURE >> :char >> :char >> :unsigned_long
|
15
|
+
|
16
|
+
raise RuntimeError.new("typeflags is #{signature}, #{version}, #{type_flags}, #{offset}") unless Fixnum === type_flags
|
17
|
+
|
18
|
+
FLAGS.each {|flag,mask| send("#{flag}=", type_flags & mask > 0) }
|
19
|
+
self.extra = io.read(offset - MIN_OFFSET)
|
20
|
+
ignore_PreviousTagSize0 = io.read :unsigned_long
|
21
|
+
self.path = io.try :path
|
22
|
+
raise IOError("Wrong Signature (#{signature} instead of #{SIGNATURE})") unless SIGNATURE == signature
|
23
|
+
end
|
24
|
+
|
25
|
+
def write_packed(io, *) #:nodoc:
|
26
|
+
self.extra ||= ""
|
27
|
+
type_flags = FLAGS.sum{|flag, mask | send(flag) ? mask : 0}
|
28
|
+
io << SIGNATURE << [self.version || 1, :char] << [type_flags, :char] <<
|
29
|
+
[MIN_OFFSET + self.extra.length, :unsigned_long] << self.extra << [0, :unsigned_long]
|
30
|
+
end
|
31
|
+
|
32
|
+
def debug(format, *)
|
33
|
+
format.header("Header", path)
|
34
|
+
format.values(to_h.tap{|h| h.delete(:path)})
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
data/lib/flv/packing.rb
ADDED
@@ -0,0 +1,140 @@
|
|
1
|
+
require 'singleton'
|
2
|
+
|
3
|
+
module FLV
|
4
|
+
class Event < Hash ; end
|
5
|
+
# FLV files can contain structured data. This modules makes it easy to (un)pack that data.
|
6
|
+
# The packing option +flv_value+ can (un)pack any kind of variable.
|
7
|
+
# It corresponds to +SCRIPTDATAVALUE+ in the official FLV file format spec.
|
8
|
+
# Implementation Note:
|
9
|
+
# +flv_value+ is actually simply a wrapper that will (un)pack the type of variable;
|
10
|
+
# the actual data is (un)packed with the format +flv+, which is independently defined for each type
|
11
|
+
module Packing
|
12
|
+
|
13
|
+
# A special kind of value used to signify the end of a list (implemented at the end)
|
14
|
+
class EndOfList # :nodoc:
|
15
|
+
end
|
16
|
+
|
17
|
+
#=== Top-level :flv_value filter:
|
18
|
+
|
19
|
+
TYPE_TO_CLASS = Hash.new do |h, type|
|
20
|
+
#EndOfList
|
21
|
+
raise IOError, "Invalid type for a flash variable. #{type.inspect} is not in #{h.keys.sort.inspect}" #todo: handle error for corrupted da
|
22
|
+
end.merge!(
|
23
|
+
0 => Numeric ,
|
24
|
+
1 => TrueClass , # There really should be a Boolean class!
|
25
|
+
2 => String ,
|
26
|
+
3 => Hash ,
|
27
|
+
8 => [Hash, :flv_with_size],
|
28
|
+
9 => EndOfList ,
|
29
|
+
10 => Array ,
|
30
|
+
11 => Time
|
31
|
+
).freeze
|
32
|
+
|
33
|
+
CLASS_TO_TYPE = Hash.new do |h, klass|
|
34
|
+
h[klass] = h[klass.superclass] # Makes it such that CLASS_TO_TYPE[Fixnum] = CLASS_TO_TYPE[Integer]
|
35
|
+
end.merge!(TYPE_TO_CLASS.invert).merge!(
|
36
|
+
Event => TYPE_TO_CLASS.key([Hash, :flv_with_size]), # Write Events as hashes with size
|
37
|
+
FalseClass => TYPE_TO_CLASS.key(TrueClass)
|
38
|
+
)
|
39
|
+
|
40
|
+
# Read/write the type and (un)pack the actual data with :flv
|
41
|
+
Object.packers.set(:flv_value) do |packer|
|
42
|
+
packer.write do |io|
|
43
|
+
type_nb = CLASS_TO_TYPE[self.class]
|
44
|
+
klass, format = TYPE_TO_CLASS[type_nb]
|
45
|
+
io << [type_nb, :char] << [self, format || :flv]
|
46
|
+
end
|
47
|
+
packer.read do |io|
|
48
|
+
klass, format = TYPE_TO_CLASS[io.read(:char)]
|
49
|
+
io.read([klass, format || :flv])
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
#=== For each basic type, the :flv filter (un)packs the actual data:
|
54
|
+
|
55
|
+
Numeric.packers.set(:flv) do |packer|
|
56
|
+
# Both Integers and Floats are packed as doubles
|
57
|
+
packer.write {|io| io << [to_f, :double] }
|
58
|
+
packer.read do |io|
|
59
|
+
n = io.read(:double)
|
60
|
+
n.to_i == n ? n.to_i : n
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
[TrueClass, FalseClass].each do |klass|
|
65
|
+
klass.packers.set(:flv) do |packer|
|
66
|
+
packer.write {|io| io << [self ? 1 : 0, :char] }
|
67
|
+
packer.read {|io| io.read(:char) != 0 }
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
String.packers.set(:flv) do |packer|
|
72
|
+
packer.write {|io| io << [length, :unsigned_short] << self }
|
73
|
+
packer.read {|io| io.read(io.read(:unsigned_short)) }
|
74
|
+
end
|
75
|
+
|
76
|
+
Array.packers.set(:flv) do |packer|
|
77
|
+
packer.write do |io|
|
78
|
+
io << [length, :unsigned_long]
|
79
|
+
each do |value|
|
80
|
+
io << [value, :flv_value]
|
81
|
+
end
|
82
|
+
end
|
83
|
+
packer.read do |io|
|
84
|
+
nb = io.read(:unsigned_long)
|
85
|
+
io.each(:flv_value).take(nb)
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
# The default format for hashes has a useless hint for the size
|
90
|
+
# This filter simply acts as a wrapper for the :flv_without_size filter
|
91
|
+
Hash.packers.set(:flv_with_size) do |packer|
|
92
|
+
packer.write do |io|
|
93
|
+
io << [length, :unsigned_long] << [self, :flv]
|
94
|
+
end
|
95
|
+
packer.read do |io|
|
96
|
+
ignore_length_hint = io >> :unsigned_long
|
97
|
+
io.read [Hash, :flv]
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
Hash.packers.set(:flv) do |packer|
|
102
|
+
packer.write do |io|
|
103
|
+
each do |key, value|
|
104
|
+
io << [key.to_s, :flv] << [value, :flv_value]
|
105
|
+
end
|
106
|
+
io << ["", :flv] << [EndOfList.instance, :flv_value]
|
107
|
+
end
|
108
|
+
packer.read do |io|
|
109
|
+
Hash[
|
110
|
+
io.each([String, :flv], :flv_value).
|
111
|
+
take_while {|str, val| val != EndOfList.instance }.
|
112
|
+
map{|k,v| [k.to_sym, v]}
|
113
|
+
]
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
Time.packers.set(:flv) do |packer|
|
118
|
+
packer.write {|io| io << [to_f * 1000, :double] << [Time.now.gmtoff / 60, :short] }
|
119
|
+
packer.read do |io|
|
120
|
+
seconds, zone = io >> :double >> :short
|
121
|
+
Time.at((seconds / 1000).to_i) + (zone * 60) - Time.now.gmtoff
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
class EndOfList # :nodoc:
|
126
|
+
include Singleton, Packable
|
127
|
+
packers.set :flv, {}
|
128
|
+
|
129
|
+
def write_packed(*)
|
130
|
+
# no data to write
|
131
|
+
end
|
132
|
+
|
133
|
+
def self.read_packed(*)
|
134
|
+
self.instance
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
Integer.packers.set :unsigned_24bits, :bytes => 3, :signed => false, :endian => :big
|
139
|
+
end #module Packing
|
140
|
+
end #module FLV
|
data/lib/flv/tag.rb
ADDED
@@ -0,0 +1,62 @@
|
|
1
|
+
module FLV
|
2
|
+
# FLV files consists of a single header and a collection of Tags.
|
3
|
+
# Tags all have a timestamp and a body; this body can be Audio, Video, or Event.
|
4
|
+
class Tag
|
5
|
+
include Base
|
6
|
+
attr_accessor :body, :timestamp
|
7
|
+
|
8
|
+
def timestamp=(t)
|
9
|
+
@timestamp = Timestamp.try_convert(t)
|
10
|
+
end
|
11
|
+
|
12
|
+
def initialize(timestamp = 0, body = nil)
|
13
|
+
self.body = body.instance_of?(Hash) ? Event.new(:onMetaData, body) : body
|
14
|
+
self.timestamp = timestamp
|
15
|
+
end
|
16
|
+
|
17
|
+
CLASS_CODE = {
|
18
|
+
8 => Audio,
|
19
|
+
9 => Video,
|
20
|
+
18 => Event
|
21
|
+
}.freeze
|
22
|
+
|
23
|
+
def write_packed(io, *) #:nodoc
|
24
|
+
packed_body = @body.pack
|
25
|
+
len = io.pos_change do
|
26
|
+
io << [CLASS_CODE.key(self.body.class), :char] \
|
27
|
+
<< [packed_body.length, :unsigned_24bits] \
|
28
|
+
<< [@timestamp.in_milliseconds, :unsigned_24bits] \
|
29
|
+
<< [@timestamp.in_milliseconds >>24, :char] \
|
30
|
+
<< [streamid=0, :unsigned_24bits] \
|
31
|
+
<< packed_body
|
32
|
+
end
|
33
|
+
io << [len, :unsigned_long]
|
34
|
+
end
|
35
|
+
|
36
|
+
def read_packed(io, options) #:nodoc
|
37
|
+
len = io.pos_change do
|
38
|
+
code, body_len, timestamp_in_ms, timestamp_in_ms_ext, streamid =
|
39
|
+
io >>:char >>:unsigned_24bits >>:unsigned_24bits >>:char >>:unsigned_24bits
|
40
|
+
@timestamp = Timestamp.in_milliseconds(timestamp_in_ms + (timestamp_in_ms_ext << 24))
|
41
|
+
@body = io.read CLASS_CODE[code] || Body, :bytes => body_len
|
42
|
+
end
|
43
|
+
FLV::Util.double_check :size, len, io.read(:unsigned_long) #todo
|
44
|
+
end
|
45
|
+
|
46
|
+
def debug(format, compared_with = nil) #:nodoc
|
47
|
+
format.header("#{timestamp} ", @body.title)
|
48
|
+
@body.debug(format) unless compared_with && @body.similar_to?(compared_with.body)
|
49
|
+
end
|
50
|
+
|
51
|
+
def is?(what) #:nodoc
|
52
|
+
super || body.is?(what)
|
53
|
+
end
|
54
|
+
|
55
|
+
def method_missing(*arg, &block)
|
56
|
+
super
|
57
|
+
rescue NoMethodError
|
58
|
+
body.send(*arg, &block)
|
59
|
+
end
|
60
|
+
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,124 @@
|
|
1
|
+
require 'delegate'
|
2
|
+
require 'optparse'
|
3
|
+
|
4
|
+
module FLV
|
5
|
+
class Timestamp < DelegateClass(Float)
|
6
|
+
def initialize(ms=0)
|
7
|
+
raise ArgumentError unless ms.is_a? Numeric
|
8
|
+
super
|
9
|
+
end
|
10
|
+
|
11
|
+
# Returns [hours, minutes, seconds, milliseconds]
|
12
|
+
def to_a
|
13
|
+
n = in_milliseconds
|
14
|
+
[1000,60,60].map do |div|
|
15
|
+
val = n % div
|
16
|
+
n /= div
|
17
|
+
val
|
18
|
+
end.reverse.unshift(n)
|
19
|
+
end
|
20
|
+
|
21
|
+
# Returns "HH:MM:SS.MMMM" like "1:23:45.678" or "1:00.000" for 1 minute.
|
22
|
+
def to_s
|
23
|
+
return "" if self == INFINITY
|
24
|
+
nbs = to_a.drop_while(&:zero?)
|
25
|
+
ms = nbs.pop || 0
|
26
|
+
first = nbs.shift || 0
|
27
|
+
sprintf("%d%s.%03d", first, nbs.map{|n| sprintf(":%02d",n)}.join, ms)
|
28
|
+
end
|
29
|
+
|
30
|
+
def in_seconds
|
31
|
+
to_f
|
32
|
+
end
|
33
|
+
|
34
|
+
def in_milliseconds
|
35
|
+
(self * 1000).round
|
36
|
+
end
|
37
|
+
|
38
|
+
def widen(amount)
|
39
|
+
TimestampRange.new(self,self).widen(amount)
|
40
|
+
end
|
41
|
+
|
42
|
+
def self.in_milliseconds(ms)
|
43
|
+
Timestamp.new(ms/1000.0)
|
44
|
+
end
|
45
|
+
|
46
|
+
def self.in_seconds(s)
|
47
|
+
new s
|
48
|
+
end
|
49
|
+
|
50
|
+
def self.try_convert(s, if_empty_string = 0)
|
51
|
+
case s
|
52
|
+
when Timestamp
|
53
|
+
s
|
54
|
+
when Numeric
|
55
|
+
new s
|
56
|
+
when ""
|
57
|
+
new if_empty_string
|
58
|
+
when REGEXP
|
59
|
+
h, m, s, ms = Regexp.last_match.captures
|
60
|
+
ms &&= ms.ljust(3,'0')[0,3] # e.g. 1.23 => 1.230
|
61
|
+
h, m = m, h if h and h.end_with?(":") and m.nil?
|
62
|
+
h, m, s, ms = [h, m, s, ms].map{|n| (n || 0).to_i}
|
63
|
+
in_seconds ((h*60+m)*60+s)+ms/1000.0
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
REGEXP = /^(\d*[h:])?(\d*[m:])?(\d*)\.?(\d*)$/.freeze
|
68
|
+
end # Timestamp
|
69
|
+
|
70
|
+
INFINITY = 1/0.0
|
71
|
+
class TimestampRange < Range
|
72
|
+
core = Timestamp::REGEXP.source.gsub(/[\$\^\(\)]/,"")
|
73
|
+
REGEXP = Regexp.new("^(#{core})-(#{core})$").freeze
|
74
|
+
|
75
|
+
def to_s
|
76
|
+
"#{self.begin}-#{self.end}"
|
77
|
+
end
|
78
|
+
|
79
|
+
def initialize(from, to, exclusive=false)
|
80
|
+
super(Timestamp.try_convert(from), Timestamp.try_convert(to, INFINITY), exclusive)
|
81
|
+
end
|
82
|
+
|
83
|
+
def in_seconds
|
84
|
+
Range.new(self.begin.in_seconds, self.end.in_seconds, self.exclude_end?)
|
85
|
+
end
|
86
|
+
|
87
|
+
def in_milliseconds
|
88
|
+
Range.new(self.begin.in_milliseconds, self.end.in_milliseconds, self.exclude_end?)
|
89
|
+
end
|
90
|
+
|
91
|
+
def self.try_convert(s)
|
92
|
+
case s
|
93
|
+
when Range
|
94
|
+
new s.begin, s.end
|
95
|
+
when TimestampRange
|
96
|
+
s
|
97
|
+
when REGEXP
|
98
|
+
new *Regexp.last_match.captures
|
99
|
+
else
|
100
|
+
p "Can't convert #{s}"
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
def widen(amount)
|
105
|
+
TimestampRange.new [self.begin - amount, 0].max, self.end + amount, self.exclude_end?
|
106
|
+
end
|
107
|
+
end # TimestampRange
|
108
|
+
OptionParser.accept(TimestampRange, TimestampRange::REGEXP) {|str,from,to| TimestampRange.try_convert(str)}
|
109
|
+
|
110
|
+
|
111
|
+
class TimestampOrTimestampRange # :nodoc:
|
112
|
+
core = Timestamp::REGEXP.source.gsub(/[\$\^\(\)]/,"")
|
113
|
+
REGEXP = Regexp.new("^(#{core})-(#{core})|(#{core})$").freeze
|
114
|
+
end # TimestampOrTimestampRange
|
115
|
+
OptionParser.accept(TimestampOrTimestampRange, TimestampOrTimestampRange::REGEXP) do |str,from, to, from_to|
|
116
|
+
(from_to ? Timestamp : TimestampRange).try_convert(str)
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
class Range # :nodoc:
|
121
|
+
def ==(r) # Override built-in == because of bug in Ruby 1.8 & 1.9, see http://redmine.ruby-lang.org/issues/show/1165
|
122
|
+
self.begin == r.begin && self.end == r.end && self.exclude_end? == r.exclude_end?
|
123
|
+
end
|
124
|
+
end
|