|
9
|
1 /*
|
|
|
2 ** Copyright (c) 2014-2017, Eren Okka
|
|
|
3 **
|
|
|
4 ** This Source Code Form is subject to the terms of the Mozilla Public
|
|
|
5 ** License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
|
6 ** file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
|
7 */
|
|
|
8
|
|
|
9 #include <algorithm>
|
|
|
10 #include <regex>
|
|
|
11
|
|
|
12 #include "element.h"
|
|
|
13 #include "keyword.h"
|
|
|
14 #include "parser.h"
|
|
|
15 #include "string.h"
|
|
|
16
|
|
|
17 namespace anitomy {
|
|
|
18
|
|
|
19 bool Parser::IsValidEpisodeNumber(const string_t& number) {
|
|
|
20 return StringToInt(number) <= kEpisodeNumberMax;
|
|
|
21 }
|
|
|
22
|
|
|
23 bool Parser::SetEpisodeNumber(const string_t& number, Token& token, bool validate) {
|
|
|
24 if (validate && !IsValidEpisodeNumber(number))
|
|
|
25 return false;
|
|
|
26
|
|
|
27 token.category = kIdentifier;
|
|
|
28
|
|
|
29 auto category = kElementEpisodeNumber;
|
|
|
30
|
|
|
31 // Handle equivalent numbers
|
|
|
32 if (found_episode_keywords_) {
|
|
|
33 for (auto& element : elements_) {
|
|
|
34 if (element.first != kElementEpisodeNumber)
|
|
|
35 continue;
|
|
|
36 // The larger number gets to be the alternative one
|
|
|
37 const int comparison = StringToInt(number) - StringToInt(element.second);
|
|
|
38 if (comparison > 0) {
|
|
|
39 category = kElementEpisodeNumberAlt;
|
|
|
40 } else if (comparison < 0) {
|
|
|
41 element.first = kElementEpisodeNumberAlt;
|
|
|
42 } else {
|
|
|
43 return false; // No need to add the same number twice
|
|
|
44 }
|
|
|
45 break;
|
|
|
46 }
|
|
|
47 }
|
|
|
48
|
|
|
49 elements_.insert(category, number);
|
|
|
50 return true;
|
|
|
51 }
|
|
|
52
|
|
|
53 bool Parser::SetAlternativeEpisodeNumber(const string_t& number, Token& token) {
|
|
|
54 elements_.insert(kElementEpisodeNumberAlt, number);
|
|
|
55 token.category = kIdentifier;
|
|
|
56
|
|
|
57 return true;
|
|
|
58 }
|
|
|
59
|
|
|
60 ////////////////////////////////////////////////////////////////////////////////
|
|
|
61
|
|
|
62 bool Parser::IsValidVolumeNumber(const string_t& number) {
|
|
|
63 return StringToInt(number) <= kVolumeNumberMax;
|
|
|
64 }
|
|
|
65
|
|
|
66 bool Parser::SetVolumeNumber(const string_t& number, Token& token, bool validate) {
|
|
|
67 if (validate)
|
|
|
68 if (!IsValidVolumeNumber(number))
|
|
|
69 return false;
|
|
|
70
|
|
|
71 elements_.insert(kElementVolumeNumber, number);
|
|
|
72 token.category = kIdentifier;
|
|
|
73 return true;
|
|
|
74 }
|
|
|
75
|
|
|
76 ////////////////////////////////////////////////////////////////////////////////
|
|
|
77
|
|
|
78 bool Parser::NumberComesAfterPrefix(ElementCategory category, Token& token) {
|
|
|
79 size_t number_begin = FindNumberInString(token.content);
|
|
|
80 auto prefix = keyword_manager.Normalize(token.content.substr(0, number_begin));
|
|
|
81
|
|
|
82 if (keyword_manager.Find(category, prefix)) {
|
|
|
83 auto number = token.content.substr(number_begin, token.content.length() - number_begin);
|
|
|
84 switch (category) {
|
|
|
85 case kElementEpisodePrefix:
|
|
|
86 if (!MatchEpisodePatterns(number, token))
|
|
|
87 SetEpisodeNumber(number, token, false);
|
|
|
88 return true;
|
|
|
89 case kElementVolumePrefix:
|
|
|
90 if (!MatchVolumePatterns(number, token))
|
|
|
91 SetVolumeNumber(number, token, false);
|
|
|
92 return true;
|
|
|
93 default: break;
|
|
|
94 }
|
|
|
95 }
|
|
|
96
|
|
|
97 return false;
|
|
|
98 }
|
|
|
99
|
|
|
100 bool Parser::NumberComesBeforeAnotherNumber(const token_iterator_t token) {
|
|
|
101 auto separator_token = FindNextToken(tokens_, token, kFlagNotDelimiter);
|
|
|
102
|
|
|
103 if (separator_token != tokens_.end()) {
|
|
|
104 static const std::vector<std::pair<string_t, bool>> separators{
|
|
|
105 {L"&", true },
|
|
|
106 {L"of", false},
|
|
|
107 };
|
|
|
108 for (const auto& separator : separators) {
|
|
|
109 if (IsStringEqualTo(separator_token->content, separator.first)) {
|
|
|
110 auto other_token = FindNextToken(tokens_, separator_token, kFlagNotDelimiter);
|
|
|
111 if (other_token != tokens_.end() && IsNumericString(other_token->content)) {
|
|
|
112 SetEpisodeNumber(token->content, *token, false);
|
|
|
113 if (separator.second)
|
|
|
114 SetEpisodeNumber(other_token->content, *other_token, false);
|
|
|
115 separator_token->category = kIdentifier;
|
|
|
116 other_token->category = kIdentifier;
|
|
|
117 return true;
|
|
|
118 }
|
|
|
119 }
|
|
|
120 }
|
|
|
121 }
|
|
|
122
|
|
|
123 return false;
|
|
|
124 }
|
|
|
125
|
|
|
126 bool Parser::SearchForEpisodePatterns(std::vector<size_t>& tokens) {
|
|
|
127 for (const auto& token_index : tokens) {
|
|
|
128 auto token = tokens_.begin() + token_index;
|
|
|
129 bool numeric_front = IsNumericChar(token->content.front());
|
|
|
130
|
|
|
131 if (!numeric_front) {
|
|
|
132 // e.g. "EP.1", "Vol.1"
|
|
|
133 if (NumberComesAfterPrefix(kElementEpisodePrefix, *token))
|
|
|
134 return true;
|
|
|
135 if (NumberComesAfterPrefix(kElementVolumePrefix, *token))
|
|
|
136 continue;
|
|
|
137 } else {
|
|
|
138 // e.g. "8 & 10", "01 of 24"
|
|
|
139 if (NumberComesBeforeAnotherNumber(token))
|
|
|
140 return true;
|
|
|
141 }
|
|
|
142 // Look for other patterns
|
|
|
143 if (MatchEpisodePatterns(token->content, *token))
|
|
|
144 return true;
|
|
|
145 }
|
|
|
146
|
|
|
147 return false;
|
|
|
148 }
|
|
|
149
|
|
|
150 ////////////////////////////////////////////////////////////////////////////////
|
|
|
151
|
|
|
152 using regex_t = std::basic_regex<char_t>;
|
|
|
153 using regex_match_results_t = std::match_results<string_t::const_iterator>;
|
|
|
154
|
|
|
155 bool Parser::MatchSingleEpisodePattern(const string_t& word, Token& token) {
|
|
|
156 static const regex_t pattern(L"(\\d{1,4})[vV](\\d)");
|
|
|
157 regex_match_results_t match_results;
|
|
|
158
|
|
|
159 if (std::regex_match(word, match_results, pattern)) {
|
|
|
160 SetEpisodeNumber(match_results[1].str(), token, false);
|
|
|
161 elements_.insert(kElementReleaseVersion, match_results[2].str());
|
|
|
162 return true;
|
|
|
163 }
|
|
|
164
|
|
|
165 return false;
|
|
|
166 }
|
|
|
167
|
|
|
168 bool Parser::MatchMultiEpisodePattern(const string_t& word, Token& token) {
|
|
|
169 static const regex_t pattern(L"(\\d{1,4})(?:[vV](\\d))?[-~&+]"
|
|
|
170 L"(\\d{1,4})(?:[vV](\\d))?");
|
|
|
171 regex_match_results_t match_results;
|
|
|
172
|
|
|
173 if (std::regex_match(word, match_results, pattern)) {
|
|
|
174 auto lower_bound = match_results[1].str();
|
|
|
175 auto upper_bound = match_results[3].str();
|
|
|
176 // Avoid matching expressions such as "009-1" or "5-2"
|
|
|
177 if (StringToInt(lower_bound) < StringToInt(upper_bound)) {
|
|
|
178 if (SetEpisodeNumber(lower_bound, token, true)) {
|
|
|
179 SetEpisodeNumber(upper_bound, token, false);
|
|
|
180 if (match_results[2].matched)
|
|
|
181 elements_.insert(kElementReleaseVersion, match_results[2].str());
|
|
|
182 if (match_results[4].matched)
|
|
|
183 elements_.insert(kElementReleaseVersion, match_results[4].str());
|
|
|
184 return true;
|
|
|
185 }
|
|
|
186 }
|
|
|
187 }
|
|
|
188
|
|
|
189 return false;
|
|
|
190 }
|
|
|
191
|
|
|
192 bool Parser::MatchSeasonAndEpisodePattern(const string_t& word, Token& token) {
|
|
|
193 static const regex_t pattern(L"S?"
|
|
|
194 L"(\\d{1,2})(?:-S?(\\d{1,2}))?"
|
|
|
195 L"(?:x|[ ._-x]?E)"
|
|
|
196 L"(\\d{1,4})(?:-E?(\\d{1,4}))?"
|
|
|
197 L"(?:[vV](\\d))?",
|
|
|
198 std::regex_constants::icase);
|
|
|
199 regex_match_results_t match_results;
|
|
|
200
|
|
|
201 if (std::regex_match(word, match_results, pattern)) {
|
|
|
202 if (StringToInt(match_results[1]) == 0)
|
|
|
203 return false;
|
|
|
204 elements_.insert(kElementAnimeSeason, match_results[1]);
|
|
|
205 if (match_results[2].matched)
|
|
|
206 elements_.insert(kElementAnimeSeason, match_results[2]);
|
|
|
207 SetEpisodeNumber(match_results[3], token, false);
|
|
|
208 if (match_results[4].matched)
|
|
|
209 SetEpisodeNumber(match_results[4], token, false);
|
|
|
210 return true;
|
|
|
211 }
|
|
|
212
|
|
|
213 return false;
|
|
|
214 }
|
|
|
215
|
|
|
216 bool Parser::MatchTypeAndEpisodePattern(const string_t& word, Token& token) {
|
|
|
217 size_t number_begin = FindNumberInString(word);
|
|
|
218 auto prefix = word.substr(0, number_begin);
|
|
|
219
|
|
|
220 ElementCategory category = kElementAnimeType;
|
|
|
221 KeywordOptions options;
|
|
|
222
|
|
|
223 if (keyword_manager.Find(keyword_manager.Normalize(prefix), category, options)) {
|
|
|
224 elements_.insert(kElementAnimeType, prefix);
|
|
|
225 auto number = word.substr(number_begin);
|
|
|
226 if (MatchEpisodePatterns(number, token) || SetEpisodeNumber(number, token, true)) {
|
|
|
227 auto it = std::find(tokens_.begin(), tokens_.end(), token);
|
|
|
228 if (it != tokens_.end()) {
|
|
|
229 // Split token (we do this last in order to avoid invalidating our
|
|
|
230 // token reference earlier)
|
|
|
231 token.content = number;
|
|
|
232 tokens_.insert(it, Token(options.identifiable ? kIdentifier : kUnknown, prefix, token.enclosed));
|
|
|
233 }
|
|
|
234 return true;
|
|
|
235 }
|
|
|
236 }
|
|
|
237
|
|
|
238 return false;
|
|
|
239 }
|
|
|
240
|
|
|
241 bool Parser::MatchFractionalEpisodePattern(const string_t& word, Token& token) {
|
|
|
242 // We don't allow any fractional part other than ".5", because there are cases
|
|
|
243 // where such a number is a part of the anime title (e.g. "Evangelion: 1.11",
|
|
|
244 // "Tokyo Magnitude 8.0") or a keyword (e.g. "5.1").
|
|
|
245 static const regex_t pattern(L"\\d+\\.5");
|
|
|
246
|
|
|
247 if (std::regex_match(word, pattern))
|
|
|
248 if (SetEpisodeNumber(word, token, true))
|
|
|
249 return true;
|
|
|
250
|
|
|
251 return false;
|
|
|
252 }
|
|
|
253
|
|
|
254 bool Parser::MatchPartialEpisodePattern(const string_t& word, Token& token) {
|
|
|
255 auto it = std::find_if_not(word.begin(), word.end(), IsNumericChar);
|
|
|
256 auto suffix_length = std::distance(it, word.end());
|
|
|
257
|
|
|
258 auto is_valid_suffix = [](const char_t c) { return (c >= L'A' && c <= L'C') || (c >= L'a' && c <= L'c'); };
|
|
|
259
|
|
|
260 if (suffix_length == 1 && is_valid_suffix(*it))
|
|
|
261 if (SetEpisodeNumber(word, token, true))
|
|
|
262 return true;
|
|
|
263
|
|
|
264 return false;
|
|
|
265 }
|
|
|
266
|
|
|
267 bool Parser::MatchNumberSignPattern(const string_t& word, Token& token) {
|
|
|
268 if (word.front() != L'#')
|
|
|
269 return false;
|
|
|
270
|
|
|
271 static const regex_t pattern(L"#(\\d{1,4})(?:[-~&+](\\d{1,4}))?(?:[vV](\\d))?");
|
|
|
272 regex_match_results_t match_results;
|
|
|
273
|
|
|
274 if (std::regex_match(word, match_results, pattern)) {
|
|
|
275 if (SetEpisodeNumber(match_results[1].str(), token, true)) {
|
|
|
276 if (match_results[2].matched)
|
|
|
277 SetEpisodeNumber(match_results[2].str(), token, false);
|
|
|
278 if (match_results[3].matched)
|
|
|
279 elements_.insert(kElementReleaseVersion, match_results[3].str());
|
|
|
280 return true;
|
|
|
281 }
|
|
|
282 }
|
|
|
283
|
|
|
284 return false;
|
|
|
285 }
|
|
|
286
|
|
|
287 bool Parser::MatchJapaneseCounterPattern(const string_t& word, Token& token) {
|
|
|
288 if (word.back() != L'\u8A71')
|
|
|
289 return false;
|
|
|
290
|
|
|
291 static const regex_t pattern(L"(\\d{1,4})\u8A71");
|
|
|
292 regex_match_results_t match_results;
|
|
|
293
|
|
|
294 if (std::regex_match(word, match_results, pattern)) {
|
|
|
295 SetEpisodeNumber(match_results[1].str(), token, false);
|
|
|
296 return true;
|
|
|
297 }
|
|
|
298
|
|
|
299 return false;
|
|
|
300 }
|
|
|
301
|
|
|
302 bool Parser::MatchEpisodePatterns(string_t word, Token& token) {
|
|
|
303 // All patterns contain at least one non-numeric character
|
|
|
304 if (IsNumericString(word))
|
|
|
305 return false;
|
|
|
306
|
|
|
307 TrimString(word, L" -");
|
|
|
308
|
|
|
309 const bool numeric_front = IsNumericChar(word.front());
|
|
|
310 const bool numeric_back = IsNumericChar(word.back());
|
|
|
311
|
|
|
312 // e.g. "01v2"
|
|
|
313 if (numeric_front && numeric_back)
|
|
|
314 if (MatchSingleEpisodePattern(word, token))
|
|
|
315 return true;
|
|
|
316 // e.g. "01-02", "03-05v2"
|
|
|
317 if (numeric_front && numeric_back)
|
|
|
318 if (MatchMultiEpisodePattern(word, token))
|
|
|
319 return true;
|
|
|
320 // e.g. "2x01", "S01E03", "S01-02xE001-150"
|
|
|
321 if (numeric_back)
|
|
|
322 if (MatchSeasonAndEpisodePattern(word, token))
|
|
|
323 return true;
|
|
|
324 // e.g. "ED1", "OP4a", "OVA2"
|
|
|
325 if (!numeric_front)
|
|
|
326 if (MatchTypeAndEpisodePattern(word, token))
|
|
|
327 return true;
|
|
|
328 // e.g. "07.5"
|
|
|
329 if (numeric_front && numeric_back)
|
|
|
330 if (MatchFractionalEpisodePattern(word, token))
|
|
|
331 return true;
|
|
|
332 // e.g. "4a", "111C"
|
|
|
333 if (numeric_front && !numeric_back)
|
|
|
334 if (MatchPartialEpisodePattern(word, token))
|
|
|
335 return true;
|
|
|
336 // e.g. "#01", "#02-03v2"
|
|
|
337 if (numeric_back)
|
|
|
338 if (MatchNumberSignPattern(word, token))
|
|
|
339 return true;
|
|
|
340 // U+8A71 is used as counter for stories, episodes of TV series, etc.
|
|
|
341 if (numeric_front)
|
|
|
342 if (MatchJapaneseCounterPattern(word, token))
|
|
|
343 return true;
|
|
|
344
|
|
|
345 return false;
|
|
|
346 }
|
|
|
347
|
|
|
348 ////////////////////////////////////////////////////////////////////////////////
|
|
|
349
|
|
|
350 bool Parser::MatchSingleVolumePattern(const string_t& word, Token& token) {
|
|
|
351 static const regex_t pattern(L"(\\d{1,2})[vV](\\d)");
|
|
|
352 regex_match_results_t match_results;
|
|
|
353
|
|
|
354 if (std::regex_match(word, match_results, pattern)) {
|
|
|
355 SetVolumeNumber(match_results[1].str(), token, false);
|
|
|
356 elements_.insert(kElementReleaseVersion, match_results[2].str());
|
|
|
357 return true;
|
|
|
358 }
|
|
|
359
|
|
|
360 return false;
|
|
|
361 }
|
|
|
362
|
|
|
363 bool Parser::MatchMultiVolumePattern(const string_t& word, Token& token) {
|
|
|
364 static const regex_t pattern(L"(\\d{1,2})[-~&+](\\d{1,2})(?:[vV](\\d))?");
|
|
|
365 regex_match_results_t match_results;
|
|
|
366
|
|
|
367 if (std::regex_match(word, match_results, pattern)) {
|
|
|
368 auto lower_bound = match_results[1].str();
|
|
|
369 auto upper_bound = match_results[2].str();
|
|
|
370 if (StringToInt(lower_bound) < StringToInt(upper_bound)) {
|
|
|
371 if (SetVolumeNumber(lower_bound, token, true)) {
|
|
|
372 SetVolumeNumber(upper_bound, token, false);
|
|
|
373 if (match_results[3].matched)
|
|
|
374 elements_.insert(kElementReleaseVersion, match_results[3].str());
|
|
|
375 return true;
|
|
|
376 }
|
|
|
377 }
|
|
|
378 }
|
|
|
379
|
|
|
380 return false;
|
|
|
381 }
|
|
|
382
|
|
|
383 bool Parser::MatchVolumePatterns(string_t word, Token& token) {
|
|
|
384 // All patterns contain at least one non-numeric character
|
|
|
385 if (IsNumericString(word))
|
|
|
386 return false;
|
|
|
387
|
|
|
388 TrimString(word, L" -");
|
|
|
389
|
|
|
390 const bool numeric_front = IsNumericChar(word.front());
|
|
|
391 const bool numeric_back = IsNumericChar(word.back());
|
|
|
392
|
|
|
393 // e.g. "01v2"
|
|
|
394 if (numeric_front && numeric_back)
|
|
|
395 if (MatchSingleVolumePattern(word, token))
|
|
|
396 return true;
|
|
|
397 // e.g. "01-02", "03-05v2"
|
|
|
398 if (numeric_front && numeric_back)
|
|
|
399 if (MatchMultiVolumePattern(word, token))
|
|
|
400 return true;
|
|
|
401
|
|
|
402 return false;
|
|
|
403 }
|
|
|
404
|
|
|
405 ////////////////////////////////////////////////////////////////////////////////
|
|
|
406
|
|
|
407 bool Parser::SearchForEquivalentNumbers(std::vector<size_t>& tokens) {
|
|
|
408 for (auto token_index = tokens.begin(); token_index != tokens.end(); ++token_index) {
|
|
|
409 auto token = tokens_.begin() + *token_index;
|
|
|
410
|
|
|
411 if (IsTokenIsolated(token) || !IsValidEpisodeNumber(token->content))
|
|
|
412 continue;
|
|
|
413
|
|
|
414 // Find the first enclosed, non-delimiter token
|
|
|
415 auto next_token = FindNextToken(tokens_, token, kFlagNotDelimiter);
|
|
|
416 if (!CheckTokenCategory(next_token, kBracket))
|
|
|
417 continue;
|
|
|
418 next_token = FindNextToken(tokens_, next_token, kFlagEnclosed | kFlagNotDelimiter);
|
|
|
419 if (!CheckTokenCategory(next_token, kUnknown))
|
|
|
420 continue;
|
|
|
421
|
|
|
422 // Check if it's an isolated number
|
|
|
423 if (!IsTokenIsolated(next_token) || !IsNumericString(next_token->content) ||
|
|
|
424 !IsValidEpisodeNumber(next_token->content))
|
|
|
425 continue;
|
|
|
426
|
|
|
427 auto minmax = std::minmax(token, next_token, [](const token_iterator_t& a, const token_iterator_t& b) {
|
|
|
428 return StringToInt(a->content) < StringToInt(b->content);
|
|
|
429 });
|
|
|
430 SetEpisodeNumber(minmax.first->content, *minmax.first, false);
|
|
|
431 SetAlternativeEpisodeNumber(minmax.second->content, *minmax.second);
|
|
|
432
|
|
|
433 return true;
|
|
|
434 }
|
|
|
435
|
|
|
436 return false;
|
|
|
437 }
|
|
|
438
|
|
|
439 bool Parser::SearchForIsolatedNumbers(std::vector<size_t>& tokens) {
|
|
|
440 for (auto token_index = tokens.begin(); token_index != tokens.end(); ++token_index) {
|
|
|
441 auto token = tokens_.begin() + *token_index;
|
|
|
442
|
|
|
443 if (!token->enclosed || !IsTokenIsolated(token))
|
|
|
444 continue;
|
|
|
445
|
|
|
446 if (SetEpisodeNumber(token->content, *token, true))
|
|
|
447 return true;
|
|
|
448 }
|
|
|
449
|
|
|
450 return false;
|
|
|
451 }
|
|
|
452
|
|
|
453 bool Parser::SearchForSeparatedNumbers(std::vector<size_t>& tokens) {
|
|
|
454 for (auto token_index = tokens.begin(); token_index != tokens.end(); ++token_index) {
|
|
|
455 auto token = tokens_.begin() + *token_index;
|
|
|
456 auto previous_token = FindPreviousToken(tokens_, token, kFlagNotDelimiter);
|
|
|
457
|
|
|
458 // See if the number has a preceding "-" separator
|
|
|
459 if (CheckTokenCategory(previous_token, kUnknown) && IsDashCharacter(previous_token->content)) {
|
|
|
460 if (SetEpisodeNumber(token->content, *token, true)) {
|
|
|
461 previous_token->category = kIdentifier;
|
|
|
462 return true;
|
|
|
463 }
|
|
|
464 }
|
|
|
465 }
|
|
|
466
|
|
|
467 return false;
|
|
|
468 }
|
|
|
469
|
|
|
470 bool Parser::SearchForLastNumber(std::vector<size_t>& tokens) {
|
|
|
471 for (auto it = tokens.rbegin(); it != tokens.rend(); ++it) {
|
|
|
472 size_t token_index = *it;
|
|
|
473 auto token = tokens_.begin() + token_index;
|
|
|
474
|
|
|
475 // Assuming that episode number always comes after the title, first token
|
|
|
476 // cannot be what we're looking for
|
|
|
477 if (token_index == 0)
|
|
|
478 continue;
|
|
|
479
|
|
|
480 // An enclosed token is unlikely to be the episode number at this point
|
|
|
481 if (token->enclosed)
|
|
|
482 continue;
|
|
|
483
|
|
|
484 // Ignore if it's the first non-enclosed, non-delimiter token
|
|
|
485 if (std::all_of(tokens_.begin(), token,
|
|
|
486 [](const Token& token) { return token.enclosed || token.category == kDelimiter; }))
|
|
|
487 continue;
|
|
|
488
|
|
|
489 // Ignore if the previous token is "Movie" or "Part"
|
|
|
490 auto previous_token = FindPreviousToken(tokens_, token, kFlagNotDelimiter);
|
|
|
491 if (CheckTokenCategory(previous_token, kUnknown)) {
|
|
|
492 if (IsStringEqualTo(previous_token->content, L"Movie") ||
|
|
|
493 IsStringEqualTo(previous_token->content, L"Part")) {
|
|
|
494 continue;
|
|
|
495 }
|
|
|
496 }
|
|
|
497
|
|
|
498 // We'll use this number after all
|
|
|
499 if (SetEpisodeNumber(token->content, *token, true))
|
|
|
500 return true;
|
|
|
501 }
|
|
|
502
|
|
|
503 return false;
|
|
|
504 }
|
|
|
505
|
|
|
506 } // namespace anitomy
|