Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(controller): added merge subtitle controller #17

Open
wants to merge 9 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
142 changes: 142 additions & 0 deletions example/merge_example.dart
Original file line number Diff line number Diff line change
@@ -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 <b>and</b> entertaining

3
00:00:11.000 --> 00:00:14.000 A:end
Phone conversations where people truly <c.highlight>connect</c>

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 <c.highlight>one</c> super package

6
00:00:24.500 --> 00:00:26.000
<c.highlight>One</c> 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 <u>everyone</u>''';

const ttmlText = '''
<?xml version="1.0" encoding="UTF-8"?>
<tt xmlns="http://www.w3.org/ns/ttml">
<head>
<metadata xmlns:ttm="http://www.w3.org/ns/ttml#metadata">
<ttm:title>Timed Text TTML Example</ttm:title>
<ttm:copyright>The Authors (c) 2006</ttm:copyright>
</metadata>
<styling xmlns:tts="http://www.w3.org/ns/ttml#styling">
<!-- s1 specifies default color, font, and text alignment -->
<style xml:id="s1"
tts:color="white"
tts:fontFamily="proportionalSansSerif"
tts:fontSize="22px"
tts:textAlign="center"
/>
<!-- alternative using yellow text but otherwise the same as style s1 -->
<style xml:id="s2" style="s1" tts:color="yellow"/>
<!-- a style based on s1 but justified to the right -->
<style xml:id="s1Right" style="s1" tts:textAlign="end" />
<!-- a style based on s2 but justified to the left -->
<style xml:id="s2Left" style="s2" tts:textAlign="start" />
</styling>
<layout xmlns:tts="http://www.w3.org/ns/ttml#styling">
<region xml:id="subtitleArea"
style="s1"
tts:extent="560px 62px"
tts:padding="5px 3px"
tts:backgroundColor="black"
tts:displayAlign="after"
/>
</layout>
</head>
<body region="subtitleArea">
<div>
<p xml:id="subtitle1" begin="3.76s" end="4.45s">
It seems a paradox, does it not,
</p>
<p xml:id="subtitle2" begin="5.9s" end="10.0s">
that the image formed on<br/>
the Retina should be inverted?
</p>
<p xml:id="subtitle3" begin="10.0s" end="14.0s" style="s2">
It is puzzling, why is it<br/>
we do not see things upside-down?
</p>
<p xml:id="subtitle4" begin="14.550s" end="17.0s" style="s2">
===== This line should be merged into somewhere =====</p>
<p xml:id="subtitle5" begin="17.2s" end="23.0s">
You have never heard the Theory,<br/>then, that the Brain also is inverted?
</p>
<p xml:id="subtitle6" begin="24.5s" end="27.0s" style="s2">
##### No indeed! What a beautiful fact! #####
</p>
<p xml:id="subtitle7" begin="28.0s" end="28.2s" style="s2">
A
</p>
<p xml:id="subtitle8" begin="28.4s" end="28.8s" style="s2">
B
</p>
<p xml:id="subtitle8" begin="29.0s" end="29.2s" style="s2">
C
</p>
<p xml:id="subtitle9" begin="29.3s" end="29.8s" style="s2">
D
</p>
</div>
</body>
</tt>
''';

void main(List<String> 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<Subtitle> subtitles) {
subtitles.sort((s1, s2) => s1.compareTo(s2));
for (var result in subtitles) {
print(
'(${result.index}) Start: ${result.start}, end: ${result.end} [${result.data}]');
}
}
15 changes: 15 additions & 0 deletions lib/src/core/models.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -56,4 +58,17 @@ class Subtitle {
String toString() => '$start --> $end\n$data';

List<Object> 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);
}
}
33 changes: 31 additions & 2 deletions lib/src/utils/regexes.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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) {
Expand Down Expand Up @@ -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,
);
}

Expand All @@ -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,
);
}

Expand All @@ -107,6 +127,9 @@ class TtmlRegex extends SubtitleRegexObject {
pattern:
r'<p ([\w:]+="\w+".*)?begin="(\d{1,}:)?(\d{1,}:)?(\d{1,}).(\d{1,})s?" end="(\d{1,}:)?(\d{1,}:)?(\d{1,}).(\d{1,})s?"(\s\w+="\w+".*)?>(\D+)<\/p>',
type: SubtitleType.ttml,
startTimeIndexOffset: 2,
endTimeIndexOffset: 6,
textIndexOffset: 11,
);
}

Expand All @@ -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,
);
}
71 changes: 66 additions & 5 deletions lib/src/utils/subtitle_controller.dart
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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
Expand All @@ -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.
Expand All @@ -48,6 +49,11 @@ abstract class ISubtitleController {
Future<void> 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();
Expand All @@ -68,6 +74,60 @@ class SubtitleController extends ISubtitleController {
required SubtitleProvider provider,
}) : super(provider: provider);

static Future<SubtitleController> merge(
SubtitleController sc1, SubtitleController sc2,
{int deltaMs = 0, String joinWith = '\n'}) async {
var mergedSubtitles = List<Subtitle>.empty(growable: true);

var index = 0, targetIndex = 0;
var srcSubtitles = List<Subtitle>.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) {
Expand All @@ -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.
Expand Down
Loading