1
|
|
|
<?php namespace Done\Subtitles; |
2
|
|
|
|
3
|
|
|
interface SubtitleContract { |
4
|
|
|
|
5
|
|
|
public static function convert($from_file_path, $to_file_path); |
6
|
|
|
|
7
|
|
|
public static function load($file_name, $extension = null); // load file |
8
|
|
|
public function save($file_name); // save file |
9
|
|
|
public function content($format); // output file content (instead of saving to file) |
10
|
|
|
|
11
|
|
|
public function add($start, $end, $text); // add one line // @TODO ability to add multilines |
12
|
|
|
public function remove($from, $till); // delete test from subtitles |
13
|
|
|
public function time($seconds); // shift time |
14
|
|
|
|
15
|
|
|
|
16
|
|
|
|
17
|
|
|
|
18
|
|
|
|
19
|
|
|
|
20
|
|
|
|
21
|
|
|
|
22
|
|
|
|
23
|
|
|
|
24
|
|
|
|
25
|
|
|
|
26
|
|
|
|
27
|
|
|
|
28
|
|
|
|
29
|
|
|
|
30
|
|
|
|
31
|
|
|
|
32
|
|
|
|
33
|
|
|
|
34
|
|
|
// input |
35
|
|
|
public static function loadString($string, $extension); |
36
|
|
|
|
37
|
|
|
// chose format |
38
|
|
|
public function convertTo($format); |
39
|
|
|
|
40
|
|
|
// only text from file (without timestamps) |
41
|
|
|
public function getOnlyTextFromInput(); |
42
|
|
|
|
43
|
|
|
// output |
|
|
|
|
44
|
|
|
// public function download($filename); |
45
|
|
|
} |
46
|
|
|
|
47
|
|
|
|
48
|
|
|
class Subtitles implements SubtitleContract { |
|
|
|
|
49
|
|
|
|
50
|
|
|
protected $input; |
51
|
|
|
protected $input_format; |
52
|
|
|
|
53
|
|
|
protected $internal_format; // data in internal format (when file is converted) |
54
|
|
|
|
55
|
|
|
protected $converter; |
56
|
|
|
protected $output; |
57
|
|
|
|
58
|
3 |
|
public static function convert($from_file_path, $to_file_path) |
59
|
|
|
{ |
60
|
3 |
|
self::load($from_file_path)->save($to_file_path); |
61
|
3 |
|
} |
62
|
|
|
|
63
|
11 |
|
public static function load($file_name, $extension = null) |
64
|
|
|
{ |
65
|
11 |
|
if (strstr($file_name, "\n") === false) { |
66
|
7 |
|
return self::loadFile($file_name); |
67
|
|
|
} else { |
68
|
4 |
|
if (!$extension) { |
69
|
|
|
throw new \Exception('Specify extension'); |
70
|
|
|
} |
71
|
4 |
|
return self::loadString($file_name, $extension); |
72
|
|
|
} |
73
|
|
|
} |
74
|
|
|
|
75
|
3 |
|
public function save($path) |
76
|
|
|
{ |
77
|
3 |
|
$file_extension = self::fileExtension($path); |
78
|
3 |
|
$content = $this->content($file_extension); |
79
|
|
|
|
80
|
3 |
|
file_put_contents($path, $content); |
81
|
|
|
|
82
|
3 |
|
return $this; |
83
|
|
|
} |
84
|
|
|
|
85
|
11 |
|
public function add($start, $end, $text) |
86
|
|
|
{ |
87
|
|
|
// @TODO validation |
88
|
|
|
// @TODO check subtitles to not overlap |
89
|
11 |
|
$this->internal_format[] = [ |
90
|
11 |
|
'start' => $start, |
91
|
11 |
|
'end' => $end, |
92
|
11 |
|
'lines' => is_array($text) ? $text : [$text], |
93
|
|
|
]; |
94
|
11 |
|
usort($this->internal_format, function ($item1, $item2) { |
95
|
|
|
// return $item2['start'] <=> $item1['start']; // from PHP 7 |
|
|
|
|
96
|
4 |
|
if ($item2['start'] == $item1['start']) { |
97
|
|
|
return 0; |
98
|
4 |
|
} elseif ($item2['start'] < $item1['start']) { |
99
|
|
|
return 1; |
100
|
|
|
} else { |
101
|
4 |
|
return -1; |
102
|
|
|
} |
103
|
11 |
|
}); |
104
|
|
|
|
105
|
11 |
|
return $this; |
106
|
|
|
} |
107
|
|
|
|
108
|
3 |
|
public function remove($from, $till) |
109
|
|
|
{ |
110
|
3 |
|
foreach ($this->internal_format as $k => $block) { |
111
|
3 |
|
if (($from < $block['start'] && $block['start'] < $till) || ($from < $block['end'] && $block['end'] < $till)) { |
112
|
3 |
|
unset($this->internal_format[$k]); |
113
|
|
|
} |
114
|
|
|
} |
115
|
|
|
|
116
|
3 |
|
$this->internal_format = array_values($this->internal_format); // reorder keys |
117
|
|
|
|
118
|
3 |
|
return $this; |
119
|
|
|
} |
120
|
|
|
|
121
|
2 |
|
public function time($seconds) |
122
|
|
|
{ |
123
|
2 |
|
foreach ($this->internal_format as &$block) { |
124
|
2 |
|
$block['start'] += $seconds; |
125
|
2 |
|
$block['end'] += $seconds; |
126
|
|
|
} |
127
|
2 |
|
unset($block); |
128
|
|
|
|
129
|
2 |
|
return $this; |
130
|
|
|
} |
131
|
|
|
|
132
|
|
|
|
133
|
|
|
|
134
|
|
|
|
135
|
|
|
|
136
|
|
|
|
137
|
|
|
|
138
|
|
|
|
139
|
|
|
|
140
|
|
|
|
141
|
|
|
|
142
|
|
|
|
143
|
|
|
|
144
|
|
|
|
145
|
7 |
|
private static function loadFile($path, $extension = null) |
146
|
|
|
{ |
147
|
7 |
|
$string = file_get_contents($path); |
148
|
7 |
|
if (!$extension) { |
149
|
7 |
|
$extension = self::fileExtension($path); |
150
|
|
|
} |
151
|
|
|
|
152
|
7 |
|
return self::loadString($string, $extension); |
153
|
|
|
} |
154
|
|
|
|
155
|
11 |
|
public static function loadString($text, $extension) |
156
|
|
|
{ |
157
|
11 |
|
$converter = new self; |
158
|
11 |
|
$converter->input = self::normalizeNewLines(self::removeUtf8Bom($text)); |
159
|
|
|
|
160
|
11 |
|
$converter->input_format = $extension; |
161
|
|
|
|
162
|
11 |
|
$input_converter = self::getConverter($extension); |
163
|
11 |
|
$converter->internal_format = $input_converter->fileContentToInternalFormat($converter->input); |
164
|
|
|
|
165
|
11 |
|
return $converter; |
166
|
|
|
} |
167
|
|
|
|
168
|
|
|
// public function convertTo($extension) |
|
|
|
|
169
|
|
|
// { |
170
|
|
|
// $converter = self::getConverter($extension); |
171
|
|
|
// |
172
|
|
|
// $this->output = $converter->internalFormatToFileContent($this->internal_format); |
173
|
|
|
// |
174
|
|
|
// return $this; |
175
|
|
|
// } |
176
|
|
|
|
177
|
|
|
// public function download($filename) |
|
|
|
|
178
|
|
|
// { |
179
|
|
|
// return Response::make($this->output, '200', array( |
180
|
|
|
// 'Content-Type' => 'text/plain', |
181
|
|
|
// 'Content-Disposition' => 'attachment; filename="' . $filename . '"', |
182
|
|
|
// )); |
183
|
|
|
// } |
184
|
|
|
|
185
|
8 |
|
public function content($format) |
186
|
|
|
{ |
187
|
8 |
|
$format = strtolower(trim($format, '.')); |
188
|
|
|
|
189
|
8 |
|
$converter = self::getConverter($format); |
190
|
8 |
|
$content = $converter->internalFormatToFileContent($this->internal_format); |
191
|
|
|
|
192
|
8 |
|
return $content; |
193
|
|
|
} |
194
|
|
|
|
195
|
|
|
// public function getOnlyTextFromInput() |
|
|
|
|
196
|
|
|
// { |
197
|
|
|
// $text = ''; |
198
|
|
|
// $data = $this->internal_format; |
199
|
|
|
// foreach ($data as $row) { |
200
|
|
|
// foreach ($row['lines'] as $line) { |
201
|
|
|
// $text .= $line . "\n"; |
202
|
|
|
// } |
203
|
|
|
// } |
204
|
|
|
// |
205
|
|
|
// return $text; |
206
|
|
|
// } |
207
|
|
|
|
208
|
|
|
// for testing only |
209
|
14 |
|
public function getInternalFormat() |
210
|
|
|
{ |
211
|
14 |
|
return $this->internal_format; |
212
|
|
|
} |
213
|
|
|
|
214
|
|
|
// for testing only |
215
|
2 |
|
public function setInternalFormat(array $internal_format) |
216
|
|
|
{ |
217
|
2 |
|
$this->internal_format = $internal_format; |
218
|
|
|
|
219
|
2 |
|
return $this; |
220
|
|
|
} |
221
|
|
|
|
222
|
|
|
// -------------------------------------- private ------------------------------------------------------------------ |
223
|
|
|
|
224
|
11 |
|
public static function removeUtf8Bom($text) |
225
|
|
|
{ |
226
|
11 |
|
$bom = pack('H*','EFBBBF'); |
227
|
11 |
|
$text = preg_replace("/^$bom/", '', $text); |
228
|
|
|
|
229
|
11 |
|
return $text; |
230
|
|
|
} |
231
|
|
|
|
232
|
15 |
|
private static function getConverter($extension) |
233
|
|
|
{ |
234
|
15 |
|
if ($extension == 'stl') { |
235
|
2 |
|
return new StlConverter(); |
236
|
15 |
|
} elseif ($extension == 'vtt') { |
237
|
2 |
|
return new VttConverter(); |
238
|
13 |
|
} elseif ($extension == 'srt') { |
239
|
9 |
|
return new SrtConverter(); |
240
|
4 |
|
} elseif ($extension == 'sbv') { |
241
|
2 |
|
return new SbvConverter(); |
242
|
2 |
|
} elseif ($extension == 'sub') { |
243
|
2 |
|
return new SubConverter(); |
244
|
|
|
} |
245
|
|
|
|
246
|
|
|
throw new \Exception('unknown format'); |
247
|
|
|
} |
248
|
|
|
|
249
|
7 |
|
private static function fileExtension($filename) { |
250
|
7 |
|
$parts = explode('.', $filename); |
251
|
7 |
|
$extension = end($parts); |
252
|
7 |
|
$extension = strtolower($extension); |
253
|
|
|
|
254
|
7 |
|
return $extension; |
255
|
|
|
} |
256
|
|
|
|
257
|
11 |
|
private static function normalizeNewLines($file_content) |
258
|
|
|
{ |
259
|
11 |
|
$file_content = str_replace("\r\n", "\n", $file_content); |
260
|
11 |
|
$file_content = str_replace("\r", "\n", $file_content); |
261
|
|
|
|
262
|
11 |
|
return $file_content; |
263
|
|
|
} |
264
|
|
|
} |
265
|
|
|
|
266
|
|
|
// https://github.com/captioning/captioning has potential, but :( |
267
|
|
|
// https://github.com/snikch/captions-php too small |
268
|
|
|
|
269
|
|
|
/* |
270
|
|
|
**Other popular formats that are not implemented** |
271
|
|
|
Feel free to implement one if you can, or choose some other format if you need |
272
|
|
|
``` |
273
|
|
|
.sub, .sbv - similar to .srt |
274
|
|
|
.vtt - very similar to .srt |
275
|
|
|
[.scc](https://en.wikipedia.org/wiki/EIA-608) |
276
|
|
|
``` |
277
|
|
|
*/ |
Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.
The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.
This check looks for comments that seem to be mostly valid code and reports them.