diff --git a/CHANGELOG.md b/CHANGELOG.md index d6f546b..7c9c0fd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,3 +24,19 @@ - **CustomRegex:** User define regexp. - **SubtitleObject:** Store the subtitle file data and its format type. - Initial version, created by [MuhmdHsn313](https://twitter.com/MuhmdHsn313) + +## 0.1.1 + +- add merge method + +## 0.1.2 + +- refactor merge method + +## 0.1.3 + +- upgrade dependencies path + +## 0.1.4 + +- resolve dependencies issue diff --git a/example/merge_example.dart b/example/merge_example.dart new file mode 100644 index 0000000..7b8706d --- /dev/null +++ b/example/merge_example.dart @@ -0,0 +1,142 @@ +import 'package:subtitle/subtitle.dart'; + +const vttData = '''WEBVTT FILE + +1 +00:00:03.500 --> 00:00:05.000 D:vertical A:start +Everyone wants the most from life + +2 +00:00:06.000 --> 00:00:09.000 A:start +Like internet experiences that are rich and entertaining + +3 +00:00:11.000 --> 00:00:14.000 A:end +Phone conversations where people truly connect + +4 +00:00:14.500 --> 00:00:18.000 +Your favourite TV programmes ready to watch at the touch of a button + +5 +00:00:19.000 --> 00:00:24.000 +Which is why we are bringing TV, internet and phone together in one super package + +6 +00:00:24.500 --> 00:00:26.000 +One simple way to get everything + +7 +00:00:26.500 --> 00:00:27.500 L:12% +UPC + +8 +00:00:28.000 --> 00:00:30.000 L:75% +Simply for everyone'''; + +const ttmlText = ''' + + + + + Timed Text TTML Example + The Authors (c) 2006 + + + + + + + + + + + + + + + + + + + It seems a paradox, does it not, + + + that the image formed on + the Retina should be inverted? + + + It is puzzling, why is it + we do not see things upside-down? + + + ===== This line should be merged into somewhere ===== + + You have never heard the Theory,then, that the Brain also is inverted? + + + ##### No indeed! What a beautiful fact! ##### + + + A + + + B + + + C + + + D + + + + +'''; + +void main(List args) async { + //! By using controller + var vttController = SubtitleController( + provider: SubtitleProvider.fromString( + data: vttData, + type: SubtitleType.vtt, + )); + + await vttController.initial(); + print('======= subtitle vtt ======='); + printResult(vttController.subtitles); + + var ttmlController = SubtitleController( + provider: SubtitleProvider.fromString( + data: ttmlText, + type: SubtitleType.ttml, + )); + await ttmlController.initial(); + + print('\n\n======= subtitle ttml ======='); + printResult(ttmlController.subtitles); + + var mergedController = + await SubtitleController.merge(ttmlController, vttController, deltaMs: 100); + print('\n\n======= after merged ======='); + printResult(mergedController.subtitles); +} + +void printResult(List subtitles) { + subtitles.sort((s1, s2) => s1.compareTo(s2)); + for (var result in subtitles) { + print( + '(${result.index}) Start: ${result.start}, end: ${result.end} [${result.data}]'); + } +} diff --git a/lib/src/core/models.dart b/lib/src/core/models.dart index be13c17..ea25f1b 100644 --- a/lib/src/core/models.dart +++ b/lib/src/core/models.dart @@ -32,7 +32,9 @@ class Subtitle { start.inMilliseconds.compareTo(other.start.inMilliseconds); bool inRange(Duration duration) => start <= duration && end >= duration; + bool isLarg(Duration duration) => duration > end; + bool inSmall(Duration duration) => duration < start; @override @@ -56,4 +58,17 @@ class Subtitle { String toString() => '$start --> $end\n$data'; List get props => [start, end, data, index]; + + Subtitle copyWith({ + int? index, + String? data, + Duration? start, + Duration? end, + }) { + return Subtitle( + start: start ?? this.start, + end: end ?? this.end, + data: data ?? this.data, + index: index ?? this.index); + } } diff --git a/lib/src/utils/regexes.dart b/lib/src/utils/regexes.dart index 6e08729..c26b27b 100644 --- a/lib/src/utils/regexes.dart +++ b/lib/src/utils/regexes.dart @@ -21,10 +21,18 @@ abstract class SubtitleRegexObject { /// The regex class final String pattern; final SubtitleType type; + final int? cueIndexOffset; + final int textIndexOffset; + final int startTimeIndexOffset; + final int endTimeIndexOffset; const SubtitleRegexObject({ required this.pattern, required this.type, + required this.startTimeIndexOffset, + required this.endTimeIndexOffset, + required this.textIndexOffset, + this.cueIndexOffset, }); /// # WebVTT Regex @@ -49,7 +57,11 @@ abstract class SubtitleRegexObject { /// /// This is the user define regex. Used in [SubtitleParser] to parsing this subtitle format to /// dart code. - factory SubtitleRegexObject.custom(String pattern) => CustomRegex(pattern); + factory SubtitleRegexObject.custom(String pattern, int startTimeIndexOffset, + int endTimeIndexOffset, int textIndexOffset, {int? cueIndexOffset}) => + CustomRegex( + pattern, startTimeIndexOffset, endTimeIndexOffset, textIndexOffset, + cueIndexOffset: cueIndexOffset); @override bool operator ==(Object other) { @@ -81,6 +93,10 @@ class VttRegex extends SubtitleRegexObject { pattern: r'(\d+)?\n(\d{1,}:)?(\d{1,2}:)?(\d{1,2}).(\d+)\s?-->\s?(\d{1,}:)?(\d{1,2}:)?(\d{1,2}).(\d+)(.*(?:\r?(?!\r?).*)*)\n(.*(?:\r?\n(?!\r?\n).*)*)', type: SubtitleType.vtt, + cueIndexOffset: 1, + startTimeIndexOffset: 2, + endTimeIndexOffset: 6, + textIndexOffset: 11, ); } @@ -94,6 +110,10 @@ class SrtRegex extends SubtitleRegexObject { pattern: r'(\d+)?\n(\d{1,}:)?(\d{1,2}:)?(\d{1,2}).(\d+)\s?-->\s?(\d{1,}:)?(\d{1,2}:)?(\d{1,2}).(\d+)(.*(?:\r?(?!\r?).*)*)\n(.*(?:\r?\n(?!\r?\n).*)*)', type: SubtitleType.srt, + cueIndexOffset: 1, + startTimeIndexOffset: 2, + endTimeIndexOffset: 6, + textIndexOffset: 11, ); } @@ -107,6 +127,9 @@ class TtmlRegex extends SubtitleRegexObject { pattern: r'(\D+)<\/p>', type: SubtitleType.ttml, + startTimeIndexOffset: 2, + endTimeIndexOffset: 6, + textIndexOffset: 11, ); } @@ -115,9 +138,15 @@ class TtmlRegex extends SubtitleRegexObject { /// This is the user define regex. Used in [SubtitleParser] to parsing this subtitle format to /// dart code. class CustomRegex extends SubtitleRegexObject { - const CustomRegex(String pattern) + const CustomRegex(String pattern, int startTimeIndexOffset, + int endTimeIndexOffset, textIndexOffset, + {int? cueIndexOffset}) : super( pattern: pattern, type: SubtitleType.custom, + cueIndexOffset: cueIndexOffset, + startTimeIndexOffset: startTimeIndexOffset, + endTimeIndexOffset: endTimeIndexOffset, + textIndexOffset: textIndexOffset, ); } diff --git a/lib/src/utils/subtitle_controller.dart b/lib/src/utils/subtitle_controller.dart index 4f3b257..e9884ea 100644 --- a/lib/src/utils/subtitle_controller.dart +++ b/lib/src/utils/subtitle_controller.dart @@ -1,5 +1,7 @@ import 'dart:async'; +import 'package:subtitle/src/utils/types.dart'; + import '../core/exceptions.dart'; import '../core/models.dart'; import 'subtitle_parser.dart'; @@ -18,9 +20,8 @@ abstract class ISubtitleController { /// The parser class, maybe still null if you are not initial the controller. ISubtitleParser? _parser; - ISubtitleController({ - required SubtitleProvider provider, - }) : _provider = provider, + ISubtitleController({required SubtitleProvider provider}) + : _provider = provider, subtitles = List.empty(growable: true); //! Getters @@ -32,10 +33,10 @@ abstract class ISubtitleController { } /// Return the current subtitle provider - SubtitleProvider get provider => _provider; + SubtitleProvider? get provider => _provider; /// Check it the controller is initial or not. - bool get initialized => _parser != null; + bool get initialized => _parser != null || subtitles.isNotEmpty; //! Abstract methods /// Use this method to customize your search algorithm. @@ -48,6 +49,11 @@ abstract class ISubtitleController { Future initial() async { if (initialized) return; final providerObject = await _provider.getSubtitle(); + if (providerObject.type == SubtitleType.parsedData) { + subtitles.addAll(providerObject.subtitles ?? []); + return; + } + _parser = SubtitleParser(providerObject); subtitles.addAll(_parser!.parsing()); sort(); @@ -68,6 +74,60 @@ class SubtitleController extends ISubtitleController { required SubtitleProvider provider, }) : super(provider: provider); + static Future merge( + SubtitleController sc1, SubtitleController sc2, + {int deltaMs = 0, String joinWith = '\n'}) async { + var mergedSubtitles = List.empty(growable: true); + + var index = 0, targetIndex = 0; + var srcSubtitles = List.of(sc1.subtitles); + var targetSubtitles = List.of(sc2.subtitles); + + if (srcSubtitles.isEmpty) { + mergedSubtitles = targetSubtitles; + } else { + srcSubtitles.forEach((s1) { + var mergedS2 = ''; + for (targetIndex = 0; targetIndex < targetSubtitles.length; targetIndex++) { + var s2 = targetSubtitles.elementAt(targetIndex); + + if ((s2.end.inMilliseconds - deltaMs) < s1.start.inMilliseconds) { + continue; + } else if ((s1.start.inMilliseconds - deltaMs) <= + s2.start.inMilliseconds && + s2.start.inMilliseconds <= (s1.end.inMilliseconds + deltaMs) || + (s1.start.inMilliseconds - deltaMs) <= + s2.end.inMilliseconds && + s2.end.inMilliseconds <= (s1.end.inMilliseconds + deltaMs)) { + if (mergedS2.isEmpty) { + mergedS2 = '${s1.data}$joinWith${s2.data}'; + } else { + mergedS2 += ' ${s2.data}'; + } + } else if ((s2.start.inMilliseconds - deltaMs) <= + s1.start.inMilliseconds && + s1.end.inMilliseconds <= (s2.end.inMilliseconds + deltaMs)) { + mergedS2 = '${s1.data}$joinWith${s2.data}'; + } else if (s2.start.inMilliseconds > + (s1.end.inMilliseconds + deltaMs)) { + break; + } + } + + if (mergedS2.isEmpty) { + mergedSubtitles.add(s1); + } else { + mergedSubtitles.add(s1.copyWith(index: index++, data: mergedS2)); + } + }); + } + + var controller = SubtitleController( + provider: SubtitleProvider.parsedData(subtitles: mergedSubtitles)); + await controller.initial(); + return controller; + } + /// Fetch your current single subtitle value by providing the duration. @override Subtitle? durationSearch(Duration duration) { @@ -81,6 +141,7 @@ class SubtitleController extends ISubtitleController { if (index > -1) { return subtitles[index]; } + return null; } /// Perform binary search when search about subtitle by duration. diff --git a/lib/src/utils/subtitle_parser.dart b/lib/src/utils/subtitle_parser.dart index 4bce3b5..a83c0b8 100644 --- a/lib/src/utils/subtitle_parser.dart +++ b/lib/src/utils/subtitle_parser.dart @@ -77,17 +77,18 @@ class SubtitleParser extends ISubtitleParser { var matcher = matches.elementAt(i); var index = i + 1; - if (type == SubtitleType.vtt || type == SubtitleType.srt) { - index = int.parse(matcher.group(1) ?? '${i + 1}'); + if (regexObject.cueIndexOffset != null) { + index = + int.parse(matcher.group(regexObject.cueIndexOffset!) ?? '$index'); } final data = shouldNormalizeText - ? normalize(matcher.group(11)?.trim() ?? '') - : matcher.group(11)?.trim() ?? ''; + ? normalize(matcher.group(regexObject.textIndexOffset)?.trim() ?? '') + : matcher.group(regexObject.textIndexOffset)?.trim() ?? ''; subtitles.add(Subtitle( - start: _getStartDuration(matcher), - end: _getEndDuration(matcher), + start: _getDuration(matcher, regexObject.startTimeIndexOffset), + end: _getDuration(matcher, regexObject.endTimeIndexOffset), data: data, index: index, )); @@ -96,43 +97,17 @@ class SubtitleParser extends ISubtitleParser { return subtitles; } - /// Fetch the start duration of subtitle by decoding the group inside [matcher]. - Duration _getStartDuration(RegExpMatch matcher) { - var minutes = 0; - var hours = 0; - if (matcher.group(3) == null && matcher.group(2) != null) { - minutes = int.parse(matcher.group(2)?.replaceAll(':', '') ?? '0'); - } else { - minutes = int.parse(matcher.group(3)?.replaceAll(':', '') ?? '0'); - hours = int.parse(matcher.group(2)?.replaceAll(':', '') ?? '0'); - } - - return Duration( - seconds: int.parse(matcher.group(4)?.replaceAll(':', '') ?? '0'), - minutes: minutes, - hours: hours, - milliseconds: int.parse(matcher.group(5) ?? '0'), - ); - } - - /// Fetch the end duration of subtitle by decoding the group inside [matcher]. - Duration _getEndDuration(RegExpMatch matcher) { - var minutes = 0; - var hours = 0; - - if (matcher.group(7) == null && matcher.group(6) != null) { - minutes = int.parse(matcher.group(6)?.replaceAll(':', '') ?? '0'); - } else { - minutes = int.parse(matcher.group(7)?.replaceAll(':', '') ?? '0'); - hours = int.parse(matcher.group(6)?.replaceAll(':', '') ?? '0'); - } - return Duration( - seconds: int.parse(matcher.group(8)?.replaceAll(':', '') ?? '0'), - minutes: minutes, - hours: hours, - milliseconds: int.parse(matcher.group(9) ?? '0'), - ); - } + /// Fetch the duration of subtitle by decoding the group inside [matcher]. + Duration _getDuration(RegExpMatch matcher, int indexOffset) => Duration( + hours: + int.parse(matcher.group(indexOffset)?.replaceAll(':', '') ?? '0'), + minutes: int.parse( + matcher.group(indexOffset + 1)?.replaceAll(':', '') ?? '0'), + seconds: int.parse( + matcher.group(indexOffset + 2)?.replaceAll(':', '') ?? '0'), + milliseconds: int.parse( + matcher.group(indexOffset + 3)?.replaceAll(':', '').padRight(3, '0') ?? '0'), + ); } /// Used in [CustomSubtitleParser] to comstmize parsing of subtitles. @@ -144,6 +119,10 @@ typedef OnParsingSubtitle = List Function( class CustomSubtitleParser extends ISubtitleParser { /// Store the custom regexp of subtitle. final String pattern; + final int? cueIndexOffset; + final int startTimeIndexOffset; + final int endTimeIndexOffset; + final int textIndexOffset; /// Decoding the subtitles and return a list from result. final OnParsingSubtitle onParsing; @@ -152,6 +131,10 @@ class CustomSubtitleParser extends ISubtitleParser { required SubtitleObject object, required this.pattern, required this.onParsing, + required this.startTimeIndexOffset, + required this.endTimeIndexOffset, + required this.textIndexOffset, + this.cueIndexOffset, }) : super(object); @override @@ -162,5 +145,7 @@ class CustomSubtitleParser extends ISubtitleParser { } @override - SubtitleRegexObject get regexObject => SubtitleRegexObject.custom(pattern); + SubtitleRegexObject get regexObject => SubtitleRegexObject.custom( + pattern, startTimeIndexOffset, endTimeIndexOffset, textIndexOffset, + cueIndexOffset: cueIndexOffset); } diff --git a/lib/src/utils/subtitle_provider.dart b/lib/src/utils/subtitle_provider.dart index f380707..f974090 100644 --- a/lib/src/utils/subtitle_provider.dart +++ b/lib/src/utils/subtitle_provider.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'package:path/path.dart' show extension; +import 'package:subtitle/src/core/models.dart'; import 'package:universal_io/io.dart'; import '../core/exceptions.dart'; @@ -102,6 +103,11 @@ abstract class SubtitleProvider { type: type, ); + factory SubtitleProvider.parsedData({ + required List subtitles, + }) => + ParsedSubtitle(subtitles); + /// Abstract method return an instance of [SubtitleObject]. Future getSubtitle(); @@ -246,3 +252,14 @@ class StringSubtitle extends SubtitleProvider { Future getSubtitle() async => SubtitleObject(data: data, type: type); } + +class ParsedSubtitle extends SubtitleProvider { + /// The url of subtitle file on the internet. + final List subtitles; + + const ParsedSubtitle(this.subtitles); + + @override + Future getSubtitle() async => SubtitleObject( + data: '', type: SubtitleType.parsedData, subtitles: subtitles); +} diff --git a/lib/src/utils/subtitle_repository.dart b/lib/src/utils/subtitle_repository.dart index f919d18..cec06d7 100644 --- a/lib/src/utils/subtitle_repository.dart +++ b/lib/src/utils/subtitle_repository.dart @@ -1,6 +1,8 @@ import 'dart:async'; import 'dart:convert'; +import 'package:dio/dio.dart'; +import 'package:meta/meta.dart'; import 'package:universal_io/io.dart'; import '../core/exceptions.dart'; @@ -29,6 +31,9 @@ class Response { abstract class ISubtitleRepository { const ISubtitleRepository(); + @visibleForTesting + static Dio? dioInstance = Dio(); + /// Help to fetch subtitle file data from internet. Future fetchFromNetwork(Uri url); @@ -37,15 +42,13 @@ abstract class ISubtitleRepository { /// Simple method enable you to create a http GET request. Future get(Uri url) async { - final client = HttpClient(); - final request = await client.getUrl(url); - final response = await request.close(); - final bytes = await response.single; + final dio = dioInstance ?? Dio(); + final response = await dio.getUri(url); return Response( - statusCode: response.statusCode, - body: utf8.decode(bytes), - bodyBytes: bytes, + statusCode: response.statusCode ?? 200, + body: response.toString(), + bodyBytes: utf8.encode(response.data.toString()), ); } } diff --git a/lib/src/utils/types.dart b/lib/src/utils/types.dart index 8b3201b..4aa2c1d 100644 --- a/lib/src/utils/types.dart +++ b/lib/src/utils/types.dart @@ -1,3 +1,5 @@ +import 'package:subtitle/src/core/models.dart'; + /// Stored the subtitle file data and its format type. Each subtitle file present in /// one object or [SubtitleObject] class SubtitleObject { @@ -7,9 +9,12 @@ class SubtitleObject { /// The current subtitle format type of current file. final SubtitleType type; + final List? subtitles; + const SubtitleObject({ required this.data, required this.type, + this.subtitles, }); @override @@ -29,7 +34,7 @@ class SubtitleObject { @override int get hashCode => props.hashCode; - List get props => [data, type]; + List get props => [data, type, subtitles]; } /// ## Subtitle formats types @@ -115,4 +120,5 @@ enum SubtitleType { /// /// This is type used when user provide a custom subtitle format or not supported in this package. custom, + parsedData, } diff --git a/pubspec.yaml b/pubspec.yaml index e16cdec..1e6e3a2 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -2,18 +2,20 @@ name: subtitle description: -> A library that makes it easy to work with multiple subtitle/caption file formats, written with highly efficient code, highly customizable (90%), supports Null Safety. -version: 0.1.0-beta.2 +version: 0.1.4 homepage: https://github.com/dsc-uob/subtitle issue_tracker: https://github.com/dsc-uob/subtitle/issues documentation: https://github.com/dsc-uob/subtitle/wiki environment: - sdk: '>=2.12.2 <3.0.0' + sdk: '>=2.17.0 <4.0.0' dependencies: - path: ">=1.8.0 <1.9.0" - universal_io: ">=2.0.4 <2.1.0" + path: ^1.8.0 + universal_io: ^2.1.0 + dio: ^5.1.2 + meta: ^1.8.0 dev_dependencies: - pedantic: ^1.9.0 - test: ^1.14.4 + pedantic: ^1.11.1 + test: ^1.24.2
+ It seems a paradox, does it not, +
+ that the image formed on + the Retina should be inverted? +
+ It is puzzling, why is it + we do not see things upside-down? +
+ ===== This line should be merged into somewhere =====
+ You have never heard the Theory,then, that the Brain also is inverted? +
+ ##### No indeed! What a beautiful fact! ##### +
+ A +
+ B +
+ C +
+ D +
(\D+)<\/p>', type: SubtitleType.ttml, + startTimeIndexOffset: 2, + endTimeIndexOffset: 6, + textIndexOffset: 11, ); } @@ -115,9 +138,15 @@ class TtmlRegex extends SubtitleRegexObject { /// This is the user define regex. Used in [SubtitleParser] to parsing this subtitle format to /// dart code. class CustomRegex extends SubtitleRegexObject { - const CustomRegex(String pattern) + const CustomRegex(String pattern, int startTimeIndexOffset, + int endTimeIndexOffset, textIndexOffset, + {int? cueIndexOffset}) : super( pattern: pattern, type: SubtitleType.custom, + cueIndexOffset: cueIndexOffset, + startTimeIndexOffset: startTimeIndexOffset, + endTimeIndexOffset: endTimeIndexOffset, + textIndexOffset: textIndexOffset, ); } diff --git a/lib/src/utils/subtitle_controller.dart b/lib/src/utils/subtitle_controller.dart index 4f3b257..e9884ea 100644 --- a/lib/src/utils/subtitle_controller.dart +++ b/lib/src/utils/subtitle_controller.dart @@ -1,5 +1,7 @@ import 'dart:async'; +import 'package:subtitle/src/utils/types.dart'; + import '../core/exceptions.dart'; import '../core/models.dart'; import 'subtitle_parser.dart'; @@ -18,9 +20,8 @@ abstract class ISubtitleController { /// The parser class, maybe still null if you are not initial the controller. ISubtitleParser? _parser; - ISubtitleController({ - required SubtitleProvider provider, - }) : _provider = provider, + ISubtitleController({required SubtitleProvider provider}) + : _provider = provider, subtitles = List.empty(growable: true); //! Getters @@ -32,10 +33,10 @@ abstract class ISubtitleController { } /// Return the current subtitle provider - SubtitleProvider get provider => _provider; + SubtitleProvider? get provider => _provider; /// Check it the controller is initial or not. - bool get initialized => _parser != null; + bool get initialized => _parser != null || subtitles.isNotEmpty; //! Abstract methods /// Use this method to customize your search algorithm. @@ -48,6 +49,11 @@ abstract class ISubtitleController { Future initial() async { if (initialized) return; final providerObject = await _provider.getSubtitle(); + if (providerObject.type == SubtitleType.parsedData) { + subtitles.addAll(providerObject.subtitles ?? []); + return; + } + _parser = SubtitleParser(providerObject); subtitles.addAll(_parser!.parsing()); sort(); @@ -68,6 +74,60 @@ class SubtitleController extends ISubtitleController { required SubtitleProvider provider, }) : super(provider: provider); + static Future merge( + SubtitleController sc1, SubtitleController sc2, + {int deltaMs = 0, String joinWith = '\n'}) async { + var mergedSubtitles = List.empty(growable: true); + + var index = 0, targetIndex = 0; + var srcSubtitles = List.of(sc1.subtitles); + var targetSubtitles = List.of(sc2.subtitles); + + if (srcSubtitles.isEmpty) { + mergedSubtitles = targetSubtitles; + } else { + srcSubtitles.forEach((s1) { + var mergedS2 = ''; + for (targetIndex = 0; targetIndex < targetSubtitles.length; targetIndex++) { + var s2 = targetSubtitles.elementAt(targetIndex); + + if ((s2.end.inMilliseconds - deltaMs) < s1.start.inMilliseconds) { + continue; + } else if ((s1.start.inMilliseconds - deltaMs) <= + s2.start.inMilliseconds && + s2.start.inMilliseconds <= (s1.end.inMilliseconds + deltaMs) || + (s1.start.inMilliseconds - deltaMs) <= + s2.end.inMilliseconds && + s2.end.inMilliseconds <= (s1.end.inMilliseconds + deltaMs)) { + if (mergedS2.isEmpty) { + mergedS2 = '${s1.data}$joinWith${s2.data}'; + } else { + mergedS2 += ' ${s2.data}'; + } + } else if ((s2.start.inMilliseconds - deltaMs) <= + s1.start.inMilliseconds && + s1.end.inMilliseconds <= (s2.end.inMilliseconds + deltaMs)) { + mergedS2 = '${s1.data}$joinWith${s2.data}'; + } else if (s2.start.inMilliseconds > + (s1.end.inMilliseconds + deltaMs)) { + break; + } + } + + if (mergedS2.isEmpty) { + mergedSubtitles.add(s1); + } else { + mergedSubtitles.add(s1.copyWith(index: index++, data: mergedS2)); + } + }); + } + + var controller = SubtitleController( + provider: SubtitleProvider.parsedData(subtitles: mergedSubtitles)); + await controller.initial(); + return controller; + } + /// Fetch your current single subtitle value by providing the duration. @override Subtitle? durationSearch(Duration duration) { @@ -81,6 +141,7 @@ class SubtitleController extends ISubtitleController { if (index > -1) { return subtitles[index]; } + return null; } /// Perform binary search when search about subtitle by duration. diff --git a/lib/src/utils/subtitle_parser.dart b/lib/src/utils/subtitle_parser.dart index 4bce3b5..a83c0b8 100644 --- a/lib/src/utils/subtitle_parser.dart +++ b/lib/src/utils/subtitle_parser.dart @@ -77,17 +77,18 @@ class SubtitleParser extends ISubtitleParser { var matcher = matches.elementAt(i); var index = i + 1; - if (type == SubtitleType.vtt || type == SubtitleType.srt) { - index = int.parse(matcher.group(1) ?? '${i + 1}'); + if (regexObject.cueIndexOffset != null) { + index = + int.parse(matcher.group(regexObject.cueIndexOffset!) ?? '$index'); } final data = shouldNormalizeText - ? normalize(matcher.group(11)?.trim() ?? '') - : matcher.group(11)?.trim() ?? ''; + ? normalize(matcher.group(regexObject.textIndexOffset)?.trim() ?? '') + : matcher.group(regexObject.textIndexOffset)?.trim() ?? ''; subtitles.add(Subtitle( - start: _getStartDuration(matcher), - end: _getEndDuration(matcher), + start: _getDuration(matcher, regexObject.startTimeIndexOffset), + end: _getDuration(matcher, regexObject.endTimeIndexOffset), data: data, index: index, )); @@ -96,43 +97,17 @@ class SubtitleParser extends ISubtitleParser { return subtitles; } - /// Fetch the start duration of subtitle by decoding the group inside [matcher]. - Duration _getStartDuration(RegExpMatch matcher) { - var minutes = 0; - var hours = 0; - if (matcher.group(3) == null && matcher.group(2) != null) { - minutes = int.parse(matcher.group(2)?.replaceAll(':', '') ?? '0'); - } else { - minutes = int.parse(matcher.group(3)?.replaceAll(':', '') ?? '0'); - hours = int.parse(matcher.group(2)?.replaceAll(':', '') ?? '0'); - } - - return Duration( - seconds: int.parse(matcher.group(4)?.replaceAll(':', '') ?? '0'), - minutes: minutes, - hours: hours, - milliseconds: int.parse(matcher.group(5) ?? '0'), - ); - } - - /// Fetch the end duration of subtitle by decoding the group inside [matcher]. - Duration _getEndDuration(RegExpMatch matcher) { - var minutes = 0; - var hours = 0; - - if (matcher.group(7) == null && matcher.group(6) != null) { - minutes = int.parse(matcher.group(6)?.replaceAll(':', '') ?? '0'); - } else { - minutes = int.parse(matcher.group(7)?.replaceAll(':', '') ?? '0'); - hours = int.parse(matcher.group(6)?.replaceAll(':', '') ?? '0'); - } - return Duration( - seconds: int.parse(matcher.group(8)?.replaceAll(':', '') ?? '0'), - minutes: minutes, - hours: hours, - milliseconds: int.parse(matcher.group(9) ?? '0'), - ); - } + /// Fetch the duration of subtitle by decoding the group inside [matcher]. + Duration _getDuration(RegExpMatch matcher, int indexOffset) => Duration( + hours: + int.parse(matcher.group(indexOffset)?.replaceAll(':', '') ?? '0'), + minutes: int.parse( + matcher.group(indexOffset + 1)?.replaceAll(':', '') ?? '0'), + seconds: int.parse( + matcher.group(indexOffset + 2)?.replaceAll(':', '') ?? '0'), + milliseconds: int.parse( + matcher.group(indexOffset + 3)?.replaceAll(':', '').padRight(3, '0') ?? '0'), + ); } /// Used in [CustomSubtitleParser] to comstmize parsing of subtitles. @@ -144,6 +119,10 @@ typedef OnParsingSubtitle = List Function( class CustomSubtitleParser extends ISubtitleParser { /// Store the custom regexp of subtitle. final String pattern; + final int? cueIndexOffset; + final int startTimeIndexOffset; + final int endTimeIndexOffset; + final int textIndexOffset; /// Decoding the subtitles and return a list from result. final OnParsingSubtitle onParsing; @@ -152,6 +131,10 @@ class CustomSubtitleParser extends ISubtitleParser { required SubtitleObject object, required this.pattern, required this.onParsing, + required this.startTimeIndexOffset, + required this.endTimeIndexOffset, + required this.textIndexOffset, + this.cueIndexOffset, }) : super(object); @override @@ -162,5 +145,7 @@ class CustomSubtitleParser extends ISubtitleParser { } @override - SubtitleRegexObject get regexObject => SubtitleRegexObject.custom(pattern); + SubtitleRegexObject get regexObject => SubtitleRegexObject.custom( + pattern, startTimeIndexOffset, endTimeIndexOffset, textIndexOffset, + cueIndexOffset: cueIndexOffset); } diff --git a/lib/src/utils/subtitle_provider.dart b/lib/src/utils/subtitle_provider.dart index f380707..f974090 100644 --- a/lib/src/utils/subtitle_provider.dart +++ b/lib/src/utils/subtitle_provider.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'package:path/path.dart' show extension; +import 'package:subtitle/src/core/models.dart'; import 'package:universal_io/io.dart'; import '../core/exceptions.dart'; @@ -102,6 +103,11 @@ abstract class SubtitleProvider { type: type, ); + factory SubtitleProvider.parsedData({ + required List subtitles, + }) => + ParsedSubtitle(subtitles); + /// Abstract method return an instance of [SubtitleObject]. Future getSubtitle(); @@ -246,3 +252,14 @@ class StringSubtitle extends SubtitleProvider { Future getSubtitle() async => SubtitleObject(data: data, type: type); } + +class ParsedSubtitle extends SubtitleProvider { + /// The url of subtitle file on the internet. + final List subtitles; + + const ParsedSubtitle(this.subtitles); + + @override + Future getSubtitle() async => SubtitleObject( + data: '', type: SubtitleType.parsedData, subtitles: subtitles); +} diff --git a/lib/src/utils/subtitle_repository.dart b/lib/src/utils/subtitle_repository.dart index f919d18..cec06d7 100644 --- a/lib/src/utils/subtitle_repository.dart +++ b/lib/src/utils/subtitle_repository.dart @@ -1,6 +1,8 @@ import 'dart:async'; import 'dart:convert'; +import 'package:dio/dio.dart'; +import 'package:meta/meta.dart'; import 'package:universal_io/io.dart'; import '../core/exceptions.dart'; @@ -29,6 +31,9 @@ class Response { abstract class ISubtitleRepository { const ISubtitleRepository(); + @visibleForTesting + static Dio? dioInstance = Dio(); + /// Help to fetch subtitle file data from internet. Future fetchFromNetwork(Uri url); @@ -37,15 +42,13 @@ abstract class ISubtitleRepository { /// Simple method enable you to create a http GET request. Future get(Uri url) async { - final client = HttpClient(); - final request = await client.getUrl(url); - final response = await request.close(); - final bytes = await response.single; + final dio = dioInstance ?? Dio(); + final response = await dio.getUri(url); return Response( - statusCode: response.statusCode, - body: utf8.decode(bytes), - bodyBytes: bytes, + statusCode: response.statusCode ?? 200, + body: response.toString(), + bodyBytes: utf8.encode(response.data.toString()), ); } } diff --git a/lib/src/utils/types.dart b/lib/src/utils/types.dart index 8b3201b..4aa2c1d 100644 --- a/lib/src/utils/types.dart +++ b/lib/src/utils/types.dart @@ -1,3 +1,5 @@ +import 'package:subtitle/src/core/models.dart'; + /// Stored the subtitle file data and its format type. Each subtitle file present in /// one object or [SubtitleObject] class SubtitleObject { @@ -7,9 +9,12 @@ class SubtitleObject { /// The current subtitle format type of current file. final SubtitleType type; + final List? subtitles; + const SubtitleObject({ required this.data, required this.type, + this.subtitles, }); @override @@ -29,7 +34,7 @@ class SubtitleObject { @override int get hashCode => props.hashCode; - List get props => [data, type]; + List get props => [data, type, subtitles]; } /// ## Subtitle formats types @@ -115,4 +120,5 @@ enum SubtitleType { /// /// This is type used when user provide a custom subtitle format or not supported in this package. custom, + parsedData, } diff --git a/pubspec.yaml b/pubspec.yaml index e16cdec..1e6e3a2 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -2,18 +2,20 @@ name: subtitle description: -> A library that makes it easy to work with multiple subtitle/caption file formats, written with highly efficient code, highly customizable (90%), supports Null Safety. -version: 0.1.0-beta.2 +version: 0.1.4 homepage: https://github.com/dsc-uob/subtitle issue_tracker: https://github.com/dsc-uob/subtitle/issues documentation: https://github.com/dsc-uob/subtitle/wiki environment: - sdk: '>=2.12.2 <3.0.0' + sdk: '>=2.17.0 <4.0.0' dependencies: - path: ">=1.8.0 <1.9.0" - universal_io: ">=2.0.4 <2.1.0" + path: ^1.8.0 + universal_io: ^2.1.0 + dio: ^5.1.2 + meta: ^1.8.0 dev_dependencies: - pedantic: ^1.9.0 - test: ^1.14.4 + pedantic: ^1.11.1 + test: ^1.24.2