Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 116 |
|
0.00% |
0 / 9 |
CRAP | |
0.00% |
0 / 1 |
ApiTimedText | |
0.00% |
0 / 116 |
|
0.00% |
0 / 9 |
756 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
2 | |||
isInternal | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getCustomPrinter | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 | |||
execute | |
0.00% |
0 / 44 |
|
0.00% |
0 / 1 |
156 | |||
findTimedText | |
0.00% |
0 / 17 |
|
0.00% |
0 / 1 |
56 | |||
convertTimedText | |
0.00% |
0 / 22 |
|
0.00% |
0 / 1 |
6 | |||
getAllowedParams | |
0.00% |
0 / 19 |
|
0.00% |
0 / 1 |
2 | |||
getExamplesMessages | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
2 | |||
getHelpUrls | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 |
1 | <?php |
2 | /** |
3 | * Copyright © 2015 Derk-Jan Hartman "hartman.wiki@gmail.com" |
4 | * Updated 2017-2019 Brion Vibber <bvibber@wikimedia.org> |
5 | * |
6 | * This program is free software; you can redistribute it and/or modify |
7 | * it under the terms of the GNU General Public License as published by |
8 | * the Free Software Foundation; either version 2 of the License, or |
9 | * (at your option) any later version. |
10 | * |
11 | * This program is distributed in the hope that it will be useful, |
12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
14 | * GNU General Public License for more details. |
15 | * |
16 | * You should have received a copy of the GNU General Public License along |
17 | * with this program; if not, write to the Free Software Foundation, Inc., |
18 | * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
19 | * http://www.gnu.org/copyleft/gpl.html |
20 | * |
21 | * @file |
22 | * @since 1.33 |
23 | */ |
24 | |
25 | namespace MediaWiki\TimedMediaHandler; |
26 | |
27 | use ApiBase; |
28 | use ApiFormatRaw; |
29 | use ApiMain; |
30 | use ApiResult; |
31 | use ApiUsageException; |
32 | use File; |
33 | use LogicException; |
34 | use MediaWiki\Languages\LanguageNameUtils; |
35 | use MediaWiki\Page\WikiPageFactory; |
36 | use MediaWiki\TimedMediaHandler\Handlers\TextHandler\TextHandler; |
37 | use MediaWiki\Title\Title; |
38 | use RepoGroup; |
39 | use TextContent; |
40 | use WANObjectCache; |
41 | use Wikimedia\ParamValidator\ParamValidator; |
42 | use WikiPage; |
43 | |
44 | /** |
45 | * Implements the timedtext module that outputs subtitle files |
46 | * for consumption by <track> elements |
47 | * |
48 | * @ingroup API |
49 | * @emits error.code timedtext-notfound, invalidlang, invalid-title |
50 | */ |
51 | class ApiTimedText extends ApiBase { |
52 | /** @var LanguageNameUtils */ |
53 | private $languageNameUtils; |
54 | |
55 | /** @var RepoGroup */ |
56 | private $repoGroup; |
57 | |
58 | /** @var WANObjectCache */ |
59 | private $cache; |
60 | |
61 | /** @var WikiPageFactory */ |
62 | private $wikiPageFactory; |
63 | |
64 | /** @var int version of the cache format */ |
65 | private const CACHE_VERSION = 1; |
66 | |
67 | /** @var int default 24 hours */ |
68 | private const CACHE_TTL = 86400; |
69 | |
70 | /** |
71 | * @param ApiMain $main |
72 | * @param string $action |
73 | * @param LanguageNameUtils $languageNameUtils |
74 | * @param RepoGroup $repoGroup |
75 | * @param WANObjectCache $cache |
76 | * @param WikiPageFactory $wikiPageFactory |
77 | */ |
78 | public function __construct( |
79 | ApiMain $main, |
80 | $action, |
81 | LanguageNameUtils $languageNameUtils, |
82 | RepoGroup $repoGroup, |
83 | WANObjectCache $cache, |
84 | WikiPageFactory $wikiPageFactory |
85 | ) { |
86 | parent::__construct( $main, $action ); |
87 | $this->languageNameUtils = $languageNameUtils; |
88 | $this->repoGroup = $repoGroup; |
89 | $this->cache = $cache; |
90 | $this->wikiPageFactory = $wikiPageFactory; |
91 | } |
92 | |
93 | /** |
94 | * URLs to this API endpoint are intended to be created internally and provided |
95 | * opaquely in track lists. Not (yet) considered stable for external use. |
96 | * |
97 | * @return bool |
98 | */ |
99 | public function isInternal() { |
100 | return true; |
101 | } |
102 | |
103 | /** |
104 | * This module uses a raw printer to directly output SRT, VTT or other subtitle formats |
105 | * |
106 | * @return ApiFormatRaw |
107 | */ |
108 | public function getCustomPrinter(): ApiFormatRaw { |
109 | $printer = new ApiFormatRaw( $this->getMain(), null ); |
110 | $printer->setFailWithHTTPError( true ); |
111 | return $printer; |
112 | } |
113 | |
114 | public function execute() { |
115 | $params = $this->extractRequestParams(); |
116 | |
117 | if ( $params['lang'] === null ) { |
118 | $langCode = false; |
119 | } elseif ( !$this->languageNameUtils->isValidCode( $params['lang'] ) ) { |
120 | $this->dieWithError( |
121 | [ 'apierror-invalidlang', $this->encodeParamName( 'lang' ) ], 'invalidlang' |
122 | ); |
123 | } else { |
124 | $langCode = $params['lang']; |
125 | } |
126 | |
127 | $page = $this->getTitleOrPageId( $params ); |
128 | if ( !$page->exists() ) { |
129 | $this->dieWithError( 'apierror-missingtitle', 'timedtext-notfound' ); |
130 | } |
131 | |
132 | $ns = $page->getTitle()->getNamespace(); |
133 | if ( $ns !== NS_FILE ) { |
134 | $this->dieWithError( 'apierror-filedoesnotexist', 'invalidtitle' ); |
135 | } |
136 | $file = $this->repoGroup->findFile( $page->getTitle() ); |
137 | if ( !$file ) { |
138 | $this->dieWithError( 'apierror-filedoesnotexist', 'timedtext-notfound' ); |
139 | } |
140 | if ( !$file->isLocal() ) { |
141 | $this->dieWithError( 'apierror-timedmedia-notlocal', 'timedtext-notlocal' ); |
142 | } |
143 | |
144 | // Find subtitle [content] that goes with file |
145 | $page = $this->findTimedText( $file, $langCode, $params['trackformat'] ); |
146 | if ( !$page ) { |
147 | $this->dieWithError( 'apierror-timedmedia-lang-notfound', 'timedtext-notfound' ); |
148 | } |
149 | |
150 | // TODO factor out. dupe with TimedTextPage.php |
151 | $filename = $page->getTitle()->getDBkey(); |
152 | $titleParts = explode( '.', $filename ); |
153 | $timedTextExtension = array_pop( $titleParts ); |
154 | |
155 | if ( $timedTextExtension !== $params['trackformat'] ) { |
156 | $filename .= '.' . $params['trackformat']; |
157 | } |
158 | |
159 | $rawTimedText = $this->convertTimedText( |
160 | $timedTextExtension, |
161 | $params['trackformat'], |
162 | $page |
163 | ); |
164 | |
165 | // We want to cache our output |
166 | $this->getMain()->setCacheMode( 'public' ); |
167 | if ( !$this->getMain()->getParameter( 'smaxage' ) ) { |
168 | // cache at least 15 seconds. |
169 | $this->getMain()->setCacheMaxAge( 15 ); |
170 | } |
171 | |
172 | if ( $params['trackformat'] === TimedTextPage::SRT_SUBTITLE_FORMAT ) { |
173 | $mimeType = 'text/srt'; |
174 | } elseif ( $params['trackformat'] === TimedTextPage::VTT_SUBTITLE_FORMAT ) { |
175 | $mimeType = 'text/vtt'; |
176 | } else { |
177 | // Unreachable due to parameter validation, |
178 | // unless someone adds a new format and forgets. :D |
179 | throw new LogicException( 'Unsupported timedtext trackformat' ); |
180 | } |
181 | |
182 | $result = $this->getResult(); |
183 | $result->addValue( null, 'text', $rawTimedText, ApiResult::NO_SIZE_CHECK ); |
184 | $result->addValue( null, 'mime', $mimeType, ApiResult::NO_SIZE_CHECK ); |
185 | $result->addValue( null, 'filename', $filename, ApiResult::NO_SIZE_CHECK ); |
186 | } |
187 | |
188 | /** |
189 | * @param File $file |
190 | * @param string $langCode |
191 | * @param string $preferredFormat |
192 | * @return WikiPage|null |
193 | * @throws ApiUsageException |
194 | */ |
195 | protected function findTimedText( File $file, $langCode, $preferredFormat ) { |
196 | // In future, add TimedTextPage::VTT_SUBTITLE_FORMAT as a supported input format as well. |
197 | $sourceFormats = [ TimedTextPage::SRT_SUBTITLE_FORMAT ]; |
198 | |
199 | $textHandler = new TextHandler( $file, $sourceFormats ); |
200 | $ns = $textHandler->getTimedTextNamespace(); |
201 | if ( !$ns ) { |
202 | $this->dieWithError( 'apierror-timedmedia-no-timedtext-support', 'invalidconfig' ); |
203 | } |
204 | |
205 | foreach ( $sourceFormats as $format ) { |
206 | $dbkey = "{$file->getTitle()->getDbKey()}.$langCode.$format"; |
207 | $page = $this->wikiPageFactory->newFromTitle( Title::makeTitle( $ns, $dbkey ) ); |
208 | if ( $page->exists() ) { |
209 | if ( $page->isRedirect() ) { |
210 | $title = $page->getRedirectTarget(); |
211 | if ( $title ) { |
212 | $page = $this->wikiPageFactory->newFromTitle( $title ); |
213 | } else { |
214 | return null; |
215 | } |
216 | } |
217 | if ( $page->exists() ) { |
218 | return $page; |
219 | } |
220 | } |
221 | } |
222 | return null; |
223 | } |
224 | |
225 | /** |
226 | * Fetch and convert or normalize the given timetext source. |
227 | * |
228 | * Uses the main WAN cache storage for caching output; if cached |
229 | * data is available it will be used instead of fetching and |
230 | * converting the text anew. |
231 | * |
232 | * Cache items are auto-expired if the CACHE_VERSION constant |
233 | * changes or the page has been edited since last update. |
234 | * |
235 | * @param string $from one of TimedTextPage::SRT_SUBTITLE_FORMAT or TimedTextPage::VTT_SUBTITLE_FORMAT |
236 | * @param string $to one of TimedTextPage::VTT_SUBTITLE_FORMAT or TimedTextPage::SRT_SUBTITLE_FORMAT |
237 | * @param WikiPage $page the TimedText page being loaded |
238 | * @return string text of the output in desired format |
239 | */ |
240 | protected function convertTimedText( $from, $to, $page ) { |
241 | $revId = $page->getLatest(); |
242 | $key = $this->cache->makeKey( |
243 | 'apitimedtext', |
244 | self::CACHE_VERSION, |
245 | $page->getTitle()->getDbKey(), |
246 | $revId, |
247 | $from, |
248 | $to |
249 | ); |
250 | return $this->cache->getWithSetCallback( |
251 | $key, |
252 | self::CACHE_TTL, |
253 | static function ( $cached, &$ttl ) use ( $from, $to, $page ) { |
254 | // TODO convert to contentmodel |
255 | $content = $page->getContent(); |
256 | $rawTimedText = $content instanceof TextContent ? $content->getText() : ''; |
257 | return TextHandler::convertSubtitles( |
258 | $from, |
259 | $to, |
260 | $rawTimedText |
261 | ); |
262 | } |
263 | ); |
264 | } |
265 | |
266 | /** |
267 | * @param int $flags |
268 | * |
269 | * @return array |
270 | */ |
271 | public function getAllowedParams( $flags = 0 ) { |
272 | $ret = [ |
273 | 'title' => [ |
274 | ParamValidator::PARAM_TYPE => 'string', |
275 | ], |
276 | 'pageid' => [ |
277 | ParamValidator::PARAM_TYPE => 'integer' |
278 | ], |
279 | 'trackformat' => [ |
280 | ParamValidator::PARAM_TYPE => [ |
281 | TimedTextPage::SRT_SUBTITLE_FORMAT, |
282 | TimedTextPage::VTT_SUBTITLE_FORMAT, |
283 | ], |
284 | ParamValidator::PARAM_REQUIRED => true, |
285 | ], |
286 | // Note this is the target language of the track to load, |
287 | // and does not control things like the language of |
288 | // error messages. |
289 | // |
290 | // Should not default to user language or anything, since |
291 | // track URLs should be consistent and explicit. |
292 | 'lang' => [ |
293 | ParamValidator::PARAM_TYPE => 'string', |
294 | ], |
295 | ]; |
296 | return $ret; |
297 | } |
298 | |
299 | /** |
300 | * @see ApiBase::getExamplesMessages() |
301 | * @return array of examples |
302 | */ |
303 | protected function getExamplesMessages() { |
304 | return [ |
305 | 'action=timedtext&title=File:Example.ogv&lang=de&trackformat=vtt' |
306 | => 'apihelp-timedtext-example-1', |
307 | ]; |
308 | } |
309 | |
310 | /** @inheritDoc */ |
311 | public function getHelpUrls() { |
312 | return 'https://www.mediawiki.org/wiki/Special:MyLanguage/Extension:TimedMediaHandler'; |
313 | } |
314 | } |