diff options
Diffstat (limited to 'editty/source.py')
-rw-r--r-- | editty/source.py | 177 |
1 files changed, 177 insertions, 0 deletions
diff --git a/editty/source.py b/editty/source.py new file mode 100644 index 0000000..1a26407 --- /dev/null +++ b/editty/source.py | |||
@@ -0,0 +1,177 @@ | |||
1 | # -*- coding: utf-8 -*- | ||
2 | # Copyright (C) 2019 James E. Blair <corvus@gnu.org> | ||
3 | # | ||
4 | # This program is free software: you can redistribute it and/or modify | ||
5 | # it under the terms of the GNU General Public License as published by | ||
6 | # the Free Software Foundation, either version 3 of the License, or | ||
7 | # (at your option) any later version. | ||
8 | # | ||
9 | # This program is distributed in the hope that it will be useful, | ||
10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
12 | # GNU General Public License for more details. | ||
13 | # | ||
14 | # You should have received a copy of the GNU General Public License | ||
15 | # along with this program. If not, see <http://www.gnu.org/licenses/>. | ||
16 | |||
17 | import os | ||
18 | import logging | ||
19 | import time | ||
20 | import struct | ||
21 | import uuid | ||
22 | |||
23 | import urwid | ||
24 | |||
25 | class Frame: | ||
26 | def __init__(self, term, timeline_color, content=None, cursor=None): | ||
27 | if content is not None: | ||
28 | self.content = content | ||
29 | else: | ||
30 | self.content = [line[:] for line in term.content()] | ||
31 | if cursor is not None: | ||
32 | self.cursor = cursor | ||
33 | else: | ||
34 | self.cursor = term.term_cursor[:] | ||
35 | self.timeline_color = timeline_color | ||
36 | # TODO: cursor visibility, current color | ||
37 | |||
38 | class SourceClip: | ||
39 | def __init__(self, size, title, file_type, stream_fn, timing_fn, timeline_color): | ||
40 | self.title = 'Untitled' | ||
41 | self.size = size | ||
42 | self.frames = [] | ||
43 | self.times = [] | ||
44 | self.length = 0.0 | ||
45 | self.file_type = file_type | ||
46 | self.stream_fn = stream_fn | ||
47 | self.timing_fn = timing_fn | ||
48 | self.uuid = str(uuid.uuid4()) | ||
49 | self.timeline_color = timeline_color | ||
50 | |||
51 | def addFrame(self, timecode, frame): | ||
52 | self.frames.append(frame) | ||
53 | self.times.append(timecode) | ||
54 | self.length = timecode | ||
55 | |||
56 | def toJSON(self): | ||
57 | return dict(type=self.file_type, | ||
58 | uuid=self.uuid, | ||
59 | size=self.size, | ||
60 | stream=self.stream_fn, | ||
61 | timing=self.timing_fn, | ||
62 | color=self.timeline_color) | ||
63 | |||
64 | @classmethod | ||
65 | def fromJSON(cls, data): | ||
66 | ft = getFileType(data['type']) | ||
67 | sc = ft.load(data['size'], data['stream'], data['timing'], data['color']) | ||
68 | sc.uuid = data['uuid'] | ||
69 | return sc | ||
70 | |||
71 | def getFrames(self, start, end): | ||
72 | # In case we need to supply the frame before the start: | ||
73 | prev = None | ||
74 | yielded = False | ||
75 | for fi in zip(self.times, self.frames): | ||
76 | if end is not None and fi[0] > end: | ||
77 | if not yielded and prev is not None: | ||
78 | yield (start, prev[1]) | ||
79 | return | ||
80 | if start is not None: | ||
81 | if fi[0] < start: | ||
82 | prev = fi | ||
83 | continue | ||
84 | if prev is not None and fi[0] > start: | ||
85 | yield (start, prev[1]) | ||
86 | yielded = True | ||
87 | start = None | ||
88 | yield fi | ||
89 | yielded = True | ||
90 | |||
91 | class FileType: | ||
92 | timing = False | ||
93 | |||
94 | def __init__(self): | ||
95 | self.log = logging.getLogger('file') | ||
96 | |||
97 | def _loadCanvas(self, size, stream_fn, timing_fn, timeline_color): | ||
98 | title = os.path.split(stream_fn)[-1] | ||
99 | source_clip = SourceClip(size, title, self.name, stream_fn, timing_fn, timeline_color) | ||
100 | class LoadWidget(urwid.Widget): | ||
101 | term_modes = urwid.TermModes() | ||
102 | def beep(self): pass | ||
103 | def set_title(self, title): pass | ||
104 | canv = urwid.TermCanvas(size[0], size[1], LoadWidget()) | ||
105 | canv.modes.main_charset = urwid.vterm.CHARSET_UTF8 | ||
106 | |||
107 | return (source_clip, canv) | ||
108 | |||
109 | class ScriptFile(FileType): | ||
110 | name = 'Script' | ||
111 | timing = True | ||
112 | |||
113 | def load(self, size, stream_fn, timing_fn, timeline_color): | ||
114 | self.log.debug('Loading %s %s', stream_fn, timing_fn) | ||
115 | source_clip, canvas = self._loadCanvas(size, stream_fn, timing_fn, timeline_color) | ||
116 | start = time.time() | ||
117 | buffer_pos = 0 | ||
118 | timecode = 0.0 | ||
119 | with open(stream_fn, 'rb') as s: | ||
120 | stream = s.read() | ||
121 | i = stream.find(b'\n') | ||
122 | stream = stream[i+1:] | ||
123 | with open(timing_fn) as f: | ||
124 | for i, line in enumerate(f): | ||
125 | delay, count = line.strip().split(' ') | ||
126 | delay = float(delay) | ||
127 | count = int(count) | ||
128 | timecode += delay | ||
129 | data = stream[buffer_pos:buffer_pos+count] | ||
130 | canvas.addstr(data) | ||
131 | buffer_pos += count | ||
132 | source_clip.addFrame(timecode, Frame(canvas, timeline_color)) | ||
133 | end = time.time() | ||
134 | self.log.debug('Finished loading %s', end-start) | ||
135 | return source_clip | ||
136 | |||
137 | class TtyrecFile(FileType): | ||
138 | name = 'Ttyrec' | ||
139 | |||
140 | def load(self, size, stream_fn, timing_fn, timeline_color): | ||
141 | self.log.debug('Loading %s %s', stream_fn, timing_fn) | ||
142 | source_clip, canvas = self._loadCanvas(size, stream_fn, timing_fn, timeline_color) | ||
143 | with open(stream_fn, 'rb') as ttyrec_in: | ||
144 | start_time = None | ||
145 | while True: | ||
146 | header = ttyrec_in.read(12) | ||
147 | if not header: | ||
148 | self.log.debug("no header") | ||
149 | break | ||
150 | tc_secs, tc_usecs, dlen = struct.unpack('<III', header) | ||
151 | timecode = tc_secs + tc_usecs/1000000.0 | ||
152 | if start_time is None: | ||
153 | # If the first frame is less than a year from the | ||
154 | # epoch, assume we are 0-based, otherwise, assume | ||
155 | # it's epoch-based and use the first timecode as | ||
156 | # the basis. | ||
157 | if timecode > 365*24*60*60: | ||
158 | start_time = timecode | ||
159 | else: | ||
160 | start_time = 0.0 | ||
161 | data = ttyrec_in.read(dlen) | ||
162 | if len(data) != dlen: | ||
163 | raise Exception("short read") | ||
164 | canvas.addstr(data) | ||
165 | self.log.debug("Frame %0.6f %i" % (timecode, dlen)) | ||
166 | source_clip.addFrame(timecode-start_time, Frame(canvas, timeline_color)) | ||
167 | return source_clip | ||
168 | |||
169 | all_types = [ | ||
170 | TtyrecFile(), | ||
171 | ScriptFile(), | ||
172 | ] | ||
173 | |||
174 | def getFileType(name): | ||
175 | for ft in all_types: | ||
176 | if ft.name == name: | ||
177 | return ft | ||