Init Git-hub/ea
This commit is contained in:
commit
b2e0341f60
43 files changed
+2062
No files matched your search
@@ -0,0 +1,20 @@
|
|||||||
|
name: CI
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
pull_request:
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
- uses: actions/setup-java@v4
|
||||||
|
with:
|
||||||
|
distribution: temurin
|
||||||
|
java-version: '21'
|
||||||
|
- name: Run tests and build artifacts
|
||||||
|
run: ./gradlew --no-daemon clean test build
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
name: Publish to Reposilite
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags: ['v*', '[0-9]*']
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
publish:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
- uses: actions/setup-java@v4
|
||||||
|
with:
|
||||||
|
distribution: temurin
|
||||||
|
java-version: '21'
|
||||||
|
- name: Require Reposilite credentials
|
||||||
|
env:
|
||||||
|
REPOSILITE_USERNAME: ${{ secrets.REPOSILITE_USERNAME }}
|
||||||
|
REPOSILITE_PASSWORD: ${{ secrets.REPOSILITE_PASSWORD }}
|
||||||
|
run: |
|
||||||
|
test -n "$REPOSILITE_USERNAME" || { echo 'Missing REPOSILITE_USERNAME secret'; exit 1; }
|
||||||
|
test -n "$REPOSILITE_PASSWORD" || { echo 'Missing REPOSILITE_PASSWORD secret'; exit 1; }
|
||||||
|
- name: Resolve release version from tag
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
case "$GITHUB_REF" in
|
||||||
|
refs/tags/*) echo "RELEASE_VERSION=${GITHUB_REF#refs/tags/}" >> "$GITHUB_ENV" ;;
|
||||||
|
*) echo 'Publishing must be run from a release tag (for example v1.2.3)' >&2; exit 1 ;;
|
||||||
|
esac
|
||||||
|
- name: Publish release
|
||||||
|
env:
|
||||||
|
REPOSILITE_USERNAME: ${{ secrets.REPOSILITE_USERNAME }}
|
||||||
|
REPOSILITE_PASSWORD: ${{ secrets.REPOSILITE_PASSWORD }}
|
||||||
|
run: ./gradlew --no-daemon clean test publish
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
name: CI
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
pull_request:
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
- uses: actions/setup-java@v4
|
||||||
|
with:
|
||||||
|
distribution: temurin
|
||||||
|
java-version: '21'
|
||||||
|
- name: Run tests and build artifacts
|
||||||
|
run: ./gradlew --no-daemon clean test build
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
name: Publish to Reposilite
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags: ['v*', '[0-9]*']
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
publish:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
- uses: actions/setup-java@v4
|
||||||
|
with:
|
||||||
|
distribution: temurin
|
||||||
|
java-version: '21'
|
||||||
|
- name: Require Reposilite credentials
|
||||||
|
env:
|
||||||
|
REPOSILITE_USERNAME: ${{ secrets.REPOSILITE_USERNAME }}
|
||||||
|
REPOSILITE_PASSWORD: ${{ secrets.REPOSILITE_PASSWORD }}
|
||||||
|
run: |
|
||||||
|
test -n "$REPOSILITE_USERNAME" || { echo 'Missing REPOSILITE_USERNAME secret'; exit 1; }
|
||||||
|
test -n "$REPOSILITE_PASSWORD" || { echo 'Missing REPOSILITE_PASSWORD secret'; exit 1; }
|
||||||
|
- name: Resolve release version from tag
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
case "$GITHUB_REF" in
|
||||||
|
refs/tags/*) echo "RELEASE_VERSION=${GITHUB_REF#refs/tags/}" >> "$GITHUB_ENV" ;;
|
||||||
|
*) echo 'Publishing must be run from a release tag (for example v1.2.3)' >&2; exit 1 ;;
|
||||||
|
esac
|
||||||
|
- name: Publish release
|
||||||
|
env:
|
||||||
|
REPOSILITE_USERNAME: ${{ secrets.REPOSILITE_USERNAME }}
|
||||||
|
REPOSILITE_PASSWORD: ${{ secrets.REPOSILITE_PASSWORD }}
|
||||||
|
run: ./gradlew --no-daemon clean test publish
|
||||||
Executable
+39
@@ -0,0 +1,39 @@
|
|||||||
|
HELP.md
|
||||||
|
.gradle
|
||||||
|
build/
|
||||||
|
!gradle/wrapper/gradle-wrapper.jar
|
||||||
|
!**/src/main/**/build/
|
||||||
|
!**/src/test/**/build/
|
||||||
|
|
||||||
|
### STS ###
|
||||||
|
.apt_generated
|
||||||
|
.classpath
|
||||||
|
.factorypath
|
||||||
|
.project
|
||||||
|
.settings
|
||||||
|
.springBeans
|
||||||
|
.sts4-cache
|
||||||
|
bin/
|
||||||
|
!**/src/main/**/bin/
|
||||||
|
!**/src/test/**/bin/
|
||||||
|
|
||||||
|
### IntelliJ IDEA ###
|
||||||
|
.idea
|
||||||
|
*.iws
|
||||||
|
*.iml
|
||||||
|
*.ipr
|
||||||
|
out/
|
||||||
|
!**/src/main/**/out/
|
||||||
|
!**/src/test/**/out/
|
||||||
|
|
||||||
|
### NetBeans ###
|
||||||
|
/nbproject/private/
|
||||||
|
/nbbuild/
|
||||||
|
/dist/
|
||||||
|
/nbdist/
|
||||||
|
/.nb-gradle/
|
||||||
|
|
||||||
|
### VS Code ###
|
||||||
|
.vscode/
|
||||||
|
|
||||||
|
/src/test/resources/subtitle/
|
||||||
Executable
+147
@@ -0,0 +1,147 @@
|
|||||||
|
import java.io.ByteArrayOutputStream
|
||||||
|
import org.gradle.api.tasks.bundling.Jar
|
||||||
|
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
||||||
|
|
||||||
|
plugins {
|
||||||
|
kotlin("jvm") version "2.2.0"
|
||||||
|
`java-library`
|
||||||
|
`maven-publish`
|
||||||
|
}
|
||||||
|
|
||||||
|
val groupId = "no.iktdev.library"
|
||||||
|
val artifactName = "subtitle"
|
||||||
|
val fallbackVersion = "1.8.1-SNAPSHOT"
|
||||||
|
|
||||||
|
group = groupId
|
||||||
|
version = resolveProjectVersion()
|
||||||
|
|
||||||
|
repositories {
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation("com.google.code.gson:gson:2.9.0")
|
||||||
|
implementation("org.json:json:20220320")
|
||||||
|
|
||||||
|
testImplementation("junit:junit:4.13.2")
|
||||||
|
testImplementation(platform("org.junit:junit-bom:5.11.4"))
|
||||||
|
testImplementation("org.junit.jupiter:junit-jupiter")
|
||||||
|
testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine")
|
||||||
|
testRuntimeOnly("org.junit.vintage:junit-vintage-engine")
|
||||||
|
testImplementation("org.assertj:assertj-core:3.27.7")
|
||||||
|
}
|
||||||
|
|
||||||
|
kotlin {
|
||||||
|
jvmToolchain(21)
|
||||||
|
compilerOptions {
|
||||||
|
jvmTarget.set(JvmTarget.JVM_21)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
java {
|
||||||
|
sourceCompatibility = JavaVersion.VERSION_21
|
||||||
|
targetCompatibility = JavaVersion.VERSION_21
|
||||||
|
withSourcesJar()
|
||||||
|
withJavadocJar()
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.test {
|
||||||
|
useJUnitPlatform()
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.withType<Jar>().configureEach {
|
||||||
|
manifest {
|
||||||
|
attributes(
|
||||||
|
mapOf(
|
||||||
|
"Implementation-Title" to artifactName,
|
||||||
|
"Implementation-Version" to project.version.toString(),
|
||||||
|
"Implementation-Vendor" to "no.iktdev"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val snapshotRepositoryUrl = providers.environmentVariable("REPOSILITE_SNAPSHOT_URL")
|
||||||
|
.orElse("https://reposilite.iktdev.no/snapshots").get()
|
||||||
|
val releaseRepositoryUrl = providers.environmentVariable("REPOSILITE_RELEASE_URL")
|
||||||
|
.orElse("https://reposilite.iktdev.no/releases").get()
|
||||||
|
val publicationRepositoryUrl = if (version.toString().endsWith("-SNAPSHOT")) {
|
||||||
|
snapshotRepositoryUrl
|
||||||
|
} else {
|
||||||
|
releaseRepositoryUrl
|
||||||
|
}
|
||||||
|
|
||||||
|
publishing {
|
||||||
|
publications {
|
||||||
|
create<MavenPublication>("reposilite") {
|
||||||
|
groupId = project.group.toString()
|
||||||
|
artifactId = artifactName
|
||||||
|
version = project.version.toString()
|
||||||
|
from(components["java"])
|
||||||
|
|
||||||
|
pom {
|
||||||
|
name.set(artifactName)
|
||||||
|
description.set("Streamit Subtitle Convert Library")
|
||||||
|
url.set("https://reposilite.iktdev.no")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
repositories {
|
||||||
|
mavenLocal()
|
||||||
|
maven {
|
||||||
|
name = "reposilite"
|
||||||
|
url = uri(publicationRepositoryUrl)
|
||||||
|
credentials {
|
||||||
|
username = providers.environmentVariable("REPOSILITE_USERNAME")
|
||||||
|
.orElse(providers.environmentVariable("reposiliteUsername")).orNull
|
||||||
|
password = providers.environmentVariable("REPOSILITE_PASSWORD")
|
||||||
|
.orElse(providers.environmentVariable("reposilitePassword")).orNull
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun resolveProjectVersion(): String {
|
||||||
|
providers.gradleProperty("releaseVersion").orNull?.let { return normalizeTagVersion(it) }
|
||||||
|
providers.environmentVariable("RELEASE_VERSION").orNull?.let { return normalizeTagVersion(it) }
|
||||||
|
|
||||||
|
val ref = sequenceOf("GITHUB_REF", "GITEA_REF", "CI_COMMIT_TAG")
|
||||||
|
.mapNotNull { providers.environmentVariable(it).orNull }
|
||||||
|
.firstOrNull { it.isNotBlank() }
|
||||||
|
if (ref?.startsWith("refs/tags/") == true) {
|
||||||
|
return normalizeTagVersion(ref.removePrefix("refs/tags/"))
|
||||||
|
}
|
||||||
|
|
||||||
|
val latestTag = gitOutput("describe", "--tags", "--abbrev=0")
|
||||||
|
return latestTag?.let(::nextSnapshotVersion) ?: fallbackVersion
|
||||||
|
}
|
||||||
|
|
||||||
|
fun normalizeTagVersion(tag: String): String {
|
||||||
|
val normalized = tag.trim().removePrefix("v")
|
||||||
|
require(Regex("\\d+\\.\\d+\\.\\d+(?:[-+][0-9A-Za-z.-]+)?").matches(normalized)) {
|
||||||
|
"Release tag '$tag' must be a semantic version such as v1.2.3"
|
||||||
|
}
|
||||||
|
return normalized
|
||||||
|
}
|
||||||
|
|
||||||
|
fun nextSnapshotVersion(tag: String): String {
|
||||||
|
val match = Regex("^(?:v)?(\\d+)\\.(\\d+)\\.(\\d+)(?:[-+].*)?$").matchEntire(tag.trim())
|
||||||
|
?: return fallbackVersion
|
||||||
|
val major = match.groupValues[1]
|
||||||
|
val minor = match.groupValues[2]
|
||||||
|
val patch = match.groupValues[3].toInt() + 1
|
||||||
|
return "$major.$minor.$patch-SNAPSHOT"
|
||||||
|
}
|
||||||
|
|
||||||
|
fun gitOutput(vararg arguments: String): String? = try {
|
||||||
|
val output = ByteArrayOutputStream()
|
||||||
|
val result = exec {
|
||||||
|
commandLine = listOf("git", *arguments)
|
||||||
|
standardOutput = output
|
||||||
|
isIgnoreExitValue = true
|
||||||
|
}
|
||||||
|
output.toString().trim().takeIf { result.exitValue == 0 && it.isNotBlank() }
|
||||||
|
} catch (_: Exception) {
|
||||||
|
null
|
||||||
|
}
|
||||||
Executable
+1
@@ -0,0 +1 @@
|
|||||||
|
kotlin.code.style=official
|
||||||
BIN
Binary file not shown.
+5
@@ -0,0 +1,5 @@
|
|||||||
|
distributionBase=GRADLE_USER_HOME
|
||||||
|
distributionPath=wrapper/dists
|
||||||
|
distributionUrl=https\://services.gradle.org/distributions/gradle-8.10-bin.zip
|
||||||
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
|
zipStorePath=wrapper/dists
|
||||||
@@ -0,0 +1,234 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
#
|
||||||
|
# Copyright © 2015-2021 the original authors.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
#
|
||||||
|
|
||||||
|
##############################################################################
|
||||||
|
#
|
||||||
|
# Gradle start up script for POSIX generated by Gradle.
|
||||||
|
#
|
||||||
|
# Important for running:
|
||||||
|
#
|
||||||
|
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
|
||||||
|
# noncompliant, but you have some other compliant shell such as ksh or
|
||||||
|
# bash, then to run this script, type that shell name before the whole
|
||||||
|
# command line, like:
|
||||||
|
#
|
||||||
|
# ksh Gradle
|
||||||
|
#
|
||||||
|
# Busybox and similar reduced shells will NOT work, because this script
|
||||||
|
# requires all of these POSIX shell features:
|
||||||
|
# * functions;
|
||||||
|
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
|
||||||
|
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
|
||||||
|
# * compound commands having a testable exit status, especially «case»;
|
||||||
|
# * various built-in commands including «command», «set», and «ulimit».
|
||||||
|
#
|
||||||
|
# Important for patching:
|
||||||
|
#
|
||||||
|
# (2) This script targets any POSIX shell, so it avoids extensions provided
|
||||||
|
# by Bash, Ksh, etc; in particular arrays are avoided.
|
||||||
|
#
|
||||||
|
# The "traditional" practice of packing multiple parameters into a
|
||||||
|
# space-separated string is a well documented source of bugs and security
|
||||||
|
# problems, so this is (mostly) avoided, by progressively accumulating
|
||||||
|
# options in "$@", and eventually passing that to Java.
|
||||||
|
#
|
||||||
|
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
|
||||||
|
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
|
||||||
|
# see the in-line comments for details.
|
||||||
|
#
|
||||||
|
# There are tweaks for specific operating systems such as AIX, CygWin,
|
||||||
|
# Darwin, MinGW, and NonStop.
|
||||||
|
#
|
||||||
|
# (3) This script is generated from the Groovy template
|
||||||
|
# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||||
|
# within the Gradle project.
|
||||||
|
#
|
||||||
|
# You can find Gradle at https://github.com/gradle/gradle/.
|
||||||
|
#
|
||||||
|
##############################################################################
|
||||||
|
|
||||||
|
# Attempt to set APP_HOME
|
||||||
|
|
||||||
|
# Resolve links: $0 may be a link
|
||||||
|
app_path=$0
|
||||||
|
|
||||||
|
# Need this for daisy-chained symlinks.
|
||||||
|
while
|
||||||
|
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
|
||||||
|
[ -h "$app_path" ]
|
||||||
|
do
|
||||||
|
ls=$( ls -ld "$app_path" )
|
||||||
|
link=${ls#*' -> '}
|
||||||
|
case $link in #(
|
||||||
|
/*) app_path=$link ;; #(
|
||||||
|
*) app_path=$APP_HOME$link ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
|
||||||
|
|
||||||
|
APP_NAME="Gradle"
|
||||||
|
APP_BASE_NAME=${0##*/}
|
||||||
|
|
||||||
|
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||||
|
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||||
|
|
||||||
|
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||||
|
MAX_FD=maximum
|
||||||
|
|
||||||
|
warn () {
|
||||||
|
echo "$*"
|
||||||
|
} >&2
|
||||||
|
|
||||||
|
die () {
|
||||||
|
echo
|
||||||
|
echo "$*"
|
||||||
|
echo
|
||||||
|
exit 1
|
||||||
|
} >&2
|
||||||
|
|
||||||
|
# OS specific support (must be 'true' or 'false').
|
||||||
|
cygwin=false
|
||||||
|
msys=false
|
||||||
|
darwin=false
|
||||||
|
nonstop=false
|
||||||
|
case "$( uname )" in #(
|
||||||
|
CYGWIN* ) cygwin=true ;; #(
|
||||||
|
Darwin* ) darwin=true ;; #(
|
||||||
|
MSYS* | MINGW* ) msys=true ;; #(
|
||||||
|
NONSTOP* ) nonstop=true ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||||
|
|
||||||
|
|
||||||
|
# Determine the Java command to use to start the JVM.
|
||||||
|
if [ -n "$JAVA_HOME" ] ; then
|
||||||
|
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||||
|
# IBM's JDK on AIX uses strange locations for the executables
|
||||||
|
JAVACMD=$JAVA_HOME/jre/sh/java
|
||||||
|
else
|
||||||
|
JAVACMD=$JAVA_HOME/bin/java
|
||||||
|
fi
|
||||||
|
if [ ! -x "$JAVACMD" ] ; then
|
||||||
|
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
||||||
|
|
||||||
|
Please set the JAVA_HOME variable in your environment to match the
|
||||||
|
location of your Java installation."
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
JAVACMD=java
|
||||||
|
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||||
|
|
||||||
|
Please set the JAVA_HOME variable in your environment to match the
|
||||||
|
location of your Java installation."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Increase the maximum file descriptors if we can.
|
||||||
|
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
||||||
|
case $MAX_FD in #(
|
||||||
|
max*)
|
||||||
|
MAX_FD=$( ulimit -H -n ) ||
|
||||||
|
warn "Could not query maximum file descriptor limit"
|
||||||
|
esac
|
||||||
|
case $MAX_FD in #(
|
||||||
|
'' | soft) :;; #(
|
||||||
|
*)
|
||||||
|
ulimit -n "$MAX_FD" ||
|
||||||
|
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
||||||
|
esac
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Collect all arguments for the java command, stacking in reverse order:
|
||||||
|
# * args from the command line
|
||||||
|
# * the main class name
|
||||||
|
# * -classpath
|
||||||
|
# * -D...appname settings
|
||||||
|
# * --module-path (only if needed)
|
||||||
|
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
|
||||||
|
|
||||||
|
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||||
|
if "$cygwin" || "$msys" ; then
|
||||||
|
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
|
||||||
|
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
|
||||||
|
|
||||||
|
JAVACMD=$( cygpath --unix "$JAVACMD" )
|
||||||
|
|
||||||
|
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||||
|
for arg do
|
||||||
|
if
|
||||||
|
case $arg in #(
|
||||||
|
-*) false ;; # don't mess with options #(
|
||||||
|
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
|
||||||
|
[ -e "$t" ] ;; #(
|
||||||
|
*) false ;;
|
||||||
|
esac
|
||||||
|
then
|
||||||
|
arg=$( cygpath --path --ignore --mixed "$arg" )
|
||||||
|
fi
|
||||||
|
# Roll the args list around exactly as many times as the number of
|
||||||
|
# args, so each arg winds up back in the position where it started, but
|
||||||
|
# possibly modified.
|
||||||
|
#
|
||||||
|
# NB: a `for` loop captures its iteration list before it begins, so
|
||||||
|
# changing the positional parameters here affects neither the number of
|
||||||
|
# iterations, nor the values presented in `arg`.
|
||||||
|
shift # remove old arg
|
||||||
|
set -- "$@" "$arg" # push replacement arg
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Collect all arguments for the java command;
|
||||||
|
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
|
||||||
|
# shell script including quotes and variable substitutions, so put them in
|
||||||
|
# double quotes to make sure that they get re-expanded; and
|
||||||
|
# * put everything else in single quotes, so that it's not re-expanded.
|
||||||
|
|
||||||
|
set -- \
|
||||||
|
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
||||||
|
-classpath "$CLASSPATH" \
|
||||||
|
org.gradle.wrapper.GradleWrapperMain \
|
||||||
|
"$@"
|
||||||
|
|
||||||
|
# Use "xargs" to parse quoted args.
|
||||||
|
#
|
||||||
|
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
|
||||||
|
#
|
||||||
|
# In Bash we could simply go:
|
||||||
|
#
|
||||||
|
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
|
||||||
|
# set -- "${ARGS[@]}" "$@"
|
||||||
|
#
|
||||||
|
# but POSIX shell has neither arrays nor command substitution, so instead we
|
||||||
|
# post-process each arg (as a line of input to sed) to backslash-escape any
|
||||||
|
# character that might be a shell metacharacter, then use eval to reverse
|
||||||
|
# that process (while maintaining the separation between arguments), and wrap
|
||||||
|
# the whole thing up as a single "set" statement.
|
||||||
|
#
|
||||||
|
# This will of course break if any of these variables contains a newline or
|
||||||
|
# an unmatched quote.
|
||||||
|
#
|
||||||
|
|
||||||
|
eval "set -- $(
|
||||||
|
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
|
||||||
|
xargs -n1 |
|
||||||
|
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
|
||||||
|
tr '\n' ' '
|
||||||
|
)" '"$@"'
|
||||||
|
|
||||||
|
exec "$JAVACMD" "$@"
|
||||||
+89
@@ -0,0 +1,89 @@
|
|||||||
|
@rem
|
||||||
|
@rem Copyright 2015 the original author or authors.
|
||||||
|
@rem
|
||||||
|
@rem Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
@rem you may not use this file except in compliance with the License.
|
||||||
|
@rem You may obtain a copy of the License at
|
||||||
|
@rem
|
||||||
|
@rem https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
@rem
|
||||||
|
@rem Unless required by applicable law or agreed to in writing, software
|
||||||
|
@rem distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
@rem See the License for the specific language governing permissions and
|
||||||
|
@rem limitations under the License.
|
||||||
|
@rem
|
||||||
|
|
||||||
|
@if "%DEBUG%" == "" @echo off
|
||||||
|
@rem ##########################################################################
|
||||||
|
@rem
|
||||||
|
@rem Gradle startup script for Windows
|
||||||
|
@rem
|
||||||
|
@rem ##########################################################################
|
||||||
|
|
||||||
|
@rem Set local scope for the variables with windows NT shell
|
||||||
|
if "%OS%"=="Windows_NT" setlocal
|
||||||
|
|
||||||
|
set DIRNAME=%~dp0
|
||||||
|
if "%DIRNAME%" == "" set DIRNAME=.
|
||||||
|
set APP_BASE_NAME=%~n0
|
||||||
|
set APP_HOME=%DIRNAME%
|
||||||
|
|
||||||
|
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
|
||||||
|
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
|
||||||
|
|
||||||
|
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||||
|
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
|
||||||
|
|
||||||
|
@rem Find java.exe
|
||||||
|
if defined JAVA_HOME goto findJavaFromJavaHome
|
||||||
|
|
||||||
|
set JAVA_EXE=java.exe
|
||||||
|
%JAVA_EXE% -version >NUL 2>&1
|
||||||
|
if "%ERRORLEVEL%" == "0" goto execute
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||||
|
echo.
|
||||||
|
echo Please set the JAVA_HOME variable in your environment to match the
|
||||||
|
echo location of your Java installation.
|
||||||
|
|
||||||
|
goto fail
|
||||||
|
|
||||||
|
:findJavaFromJavaHome
|
||||||
|
set JAVA_HOME=%JAVA_HOME:"=%
|
||||||
|
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||||
|
|
||||||
|
if exist "%JAVA_EXE%" goto execute
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
||||||
|
echo.
|
||||||
|
echo Please set the JAVA_HOME variable in your environment to match the
|
||||||
|
echo location of your Java installation.
|
||||||
|
|
||||||
|
goto fail
|
||||||
|
|
||||||
|
:execute
|
||||||
|
@rem Setup the command line
|
||||||
|
|
||||||
|
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||||
|
|
||||||
|
|
||||||
|
@rem Execute Gradle
|
||||||
|
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
|
||||||
|
|
||||||
|
:end
|
||||||
|
@rem End local scope for the variables with windows NT shell
|
||||||
|
if "%ERRORLEVEL%"=="0" goto mainEnd
|
||||||
|
|
||||||
|
:fail
|
||||||
|
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||||
|
rem the _cmd.exe /c_ return code!
|
||||||
|
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
|
||||||
|
exit /b 1
|
||||||
|
|
||||||
|
:mainEnd
|
||||||
|
if "%OS%"=="Windows_NT" endlocal
|
||||||
|
|
||||||
|
:omega
|
||||||
Executable
+2
@@ -0,0 +1,2 @@
|
|||||||
|
rootProject.name = "subtitle"
|
||||||
|
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package no.iktdev.library.subtitle
|
||||||
|
|
||||||
|
class Configuration {
|
||||||
|
companion object {
|
||||||
|
var allowOverwrite: Boolean = false
|
||||||
|
var exportJson: Boolean = false
|
||||||
|
val carriageReturn = "\r\n"
|
||||||
|
}
|
||||||
|
}
|
||||||
+171
@@ -0,0 +1,171 @@
|
|||||||
|
package no.iktdev.library.subtitle
|
||||||
|
|
||||||
|
import no.iktdev.library.subtitle.classes.Dialog
|
||||||
|
import no.iktdev.library.subtitle.classes.DialogType
|
||||||
|
import no.iktdev.library.subtitle.classes.Time
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used to merge and handle overlapping dialogs
|
||||||
|
* It will aslo exclude SIGN_SONG from dialog if overlapping (if enabled)
|
||||||
|
*/
|
||||||
|
open class Syncro() {
|
||||||
|
|
||||||
|
|
||||||
|
fun isInConflictOfRef(ref: Dialog, inq: Dialog): Boolean {
|
||||||
|
return !isEndChange(ref, inq) && isAppendedInTimelineOfRef(ref, inq) || isTrackInRefAndEndsAfter(ref, inq)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the change is normal
|
||||||
|
*
|
||||||
|
* [--------------------------------------]
|
||||||
|
* [------------]
|
||||||
|
*/
|
||||||
|
fun isEndChange(ref: Dialog, check: Dialog): Boolean {
|
||||||
|
return ref.end.inMilli() <= check.start.inMilli()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* Track 1: [-------------------------]
|
||||||
|
* Tracl 2: [---------------]
|
||||||
|
*
|
||||||
|
* Track 2.A: [-------------------------]
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
fun isAppendedInTimelineOfRef(ref: Dialog, inq: Dialog): Boolean {
|
||||||
|
return inq.start.inMilli() >= ref.start.inMilli() && ref.end.inMilli() >= inq.end.inMilli()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* [------------------] (Ref)
|
||||||
|
*
|
||||||
|
* [------------------------------------] (A)
|
||||||
|
* [--------------------------------] (B)
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
fun isTrackInRefAndEndsAfter(ref: Dialog, inq: Dialog): Boolean {
|
||||||
|
return (inq.start.inMilli() >= ref.start.inMilli()) && inq.end.inMilli() > ref.end.inMilli()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This function will return dialogs that are directly overlapping the previous one
|
||||||
|
* In case a dialog is overlapping more than one dialog, this will need to be ran again
|
||||||
|
* @return List<String> of overlapping dialogs
|
||||||
|
*/
|
||||||
|
protected fun getOverlapping(items: List<Dialog>): List<Pair<Dialog, List<Dialog>>> {
|
||||||
|
/* In order to ensure that we don't get any edge cases where the dialog
|
||||||
|
that starts first comes later than an overlapping dialog
|
||||||
|
*/
|
||||||
|
val _items = items.sortedBy { it.start.inMilli() }
|
||||||
|
|
||||||
|
val overlapping: MutableList<Pair<Dialog, List<Dialog>>> = mutableListOf()
|
||||||
|
|
||||||
|
_items.forEach { ref ->
|
||||||
|
if (!overlapping.any { it.first == ref || it.second.contains(ref) }) {
|
||||||
|
if (ref.type == DialogType.SIGN_SONG) {
|
||||||
|
ref.ignore = true
|
||||||
|
} else {
|
||||||
|
val overlappingRef = items.filter { ref != it }.filter { isInConflictOfRef(ref, it ) }
|
||||||
|
overlapping.add(Pair(ref, overlappingRef))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return overlapping
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param List<Dialog> list of overlapping
|
||||||
|
* @return List<Dialog> of newly created dialogs based on overlapping
|
||||||
|
*/
|
||||||
|
fun sync(items: List<Dialog>): List<Dialog> {
|
||||||
|
// val returnable = items.filter { c -> overlapping.find { q -> q.first == c || q.second.contains(c) } != null }.toMutableList() // This list should now not contain any item that is overlapping
|
||||||
|
val overlapping = getOverlapping(items)
|
||||||
|
|
||||||
|
val returnable = tracks(items )
|
||||||
|
|
||||||
|
return (returnable).sortedBy { it.start.inMilli() }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Deprecated("Not in use")
|
||||||
|
protected fun hasOverlapping(items: List<Dialog>): Boolean {
|
||||||
|
items.forEach { c ->
|
||||||
|
val isConflict = items.any { isInConflictOfRef(c, it) }
|
||||||
|
if (isConflict)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
protected fun trackSegments(items: List<Dialog>): List<Dialog> {
|
||||||
|
val segments = mutableListOf<Dialog>()
|
||||||
|
for (c: Dialog in items) {
|
||||||
|
val followingDialog = items.after(c).minByOrNull { it.start.inMilli() } ?: continue
|
||||||
|
if (followingDialog.start.inMilli() < c.end.inMilli() && c.end.inMilli() > followingDialog.end.inMilli()) {
|
||||||
|
|
||||||
|
segments.add(Dialog(c.id, followingDialog.start, followingDialog.end, c.text, c.type))
|
||||||
|
segments.add(Dialog(c.id, followingDialog.end, c.end, c.text, c.type))
|
||||||
|
c.end = followingDialog.start // Sets the current end to the start of the overlapping
|
||||||
|
|
||||||
|
}
|
||||||
|
else if (followingDialog.start.inMilli() < c.end.inMilli()) {
|
||||||
|
segments.add(Dialog(c.id, followingDialog.start, c.end, c.text, c.type))
|
||||||
|
c.end = followingDialog.start
|
||||||
|
} else {
|
||||||
|
// SKIP
|
||||||
|
c.end
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
return segments
|
||||||
|
}
|
||||||
|
|
||||||
|
protected fun tracks(items: List<Dialog>): List<Dialog> {
|
||||||
|
var dialogs = items.sortedBy { it.start.inMilli() }.toMutableList()
|
||||||
|
val segments = trackSegments(dialogs)
|
||||||
|
dialogs.addAll(segments)
|
||||||
|
dialogs = dialogs.sortedBy { it.start.inMilli() }.toMutableList()
|
||||||
|
|
||||||
|
val copy = dialogs.toList()
|
||||||
|
for (c in copy) {
|
||||||
|
if (!dialogs.contains(c))
|
||||||
|
continue
|
||||||
|
|
||||||
|
val conflict = dialogs.filter { isAppendedInTimelineOfRef(c, it) && it != c }
|
||||||
|
conflict.forEach { dialogs.remove(it) }
|
||||||
|
if (conflict.isNotEmpty()) {
|
||||||
|
dialogs.remove(c)
|
||||||
|
val lines = conflict.map { it.text }.distinctBy { it }
|
||||||
|
dialogs.add(newTrackInTimeline(listOf(c.text) + lines, c.start, c.end))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return dialogs.sortedBy { it.start.inMilli() }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
protected fun newTrackInTimeline(texts: List<String>, start: Time, end: Time): Dialog {
|
||||||
|
return Dialog(
|
||||||
|
start = start,
|
||||||
|
end = end,
|
||||||
|
text = texts.joinToString(Configuration.carriageReturn),
|
||||||
|
type = DialogType.DIALOG_NORMAL
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
fun <T> List<T>.after(item: T): List<T> {
|
||||||
|
if (!this.contains(item))
|
||||||
|
throw RuntimeException("Item does not exist in list!")
|
||||||
|
return if (this.indexOf(item) == this.lastIndex)
|
||||||
|
emptyList<T>()
|
||||||
|
else
|
||||||
|
this.subList(this.indexOf(item)+1, this.size)
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package no.iktdev.library.subtitle.classes
|
||||||
|
|
||||||
|
import org.jetbrains.annotations.NotNull
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
data class Dialog(
|
||||||
|
val id: String = UUID.randomUUID().toString(),
|
||||||
|
@NotNull var start: Time,
|
||||||
|
@NotNull var end: Time,
|
||||||
|
var text: String,
|
||||||
|
val type: DialogType,
|
||||||
|
var ignore: Boolean = false
|
||||||
|
)
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package no.iktdev.library.subtitle.classes
|
||||||
|
|
||||||
|
enum class DialogType {
|
||||||
|
DIALOG_NORMAL,
|
||||||
|
DIALOG_ITALIC,
|
||||||
|
DIALOG_BOLD,
|
||||||
|
SIGN_SONG,
|
||||||
|
CAPTION
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package no.iktdev.library.subtitle.classes
|
||||||
|
|
||||||
|
data class SubripData(
|
||||||
|
val number: String,
|
||||||
|
val time: String,
|
||||||
|
val text: MutableList<String>
|
||||||
|
)
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package no.iktdev.library.subtitle.classes
|
||||||
|
|
||||||
|
data class SubstationAlphaDialogData(
|
||||||
|
val layer: String,
|
||||||
|
val start: String,
|
||||||
|
val end: String,
|
||||||
|
val style: String,
|
||||||
|
val name: String,
|
||||||
|
val marginL: String,
|
||||||
|
val marginR: String,
|
||||||
|
val marginV: String,
|
||||||
|
val effect: String,
|
||||||
|
val text: String
|
||||||
|
)
|
||||||
+102
@@ -0,0 +1,102 @@
|
|||||||
|
package no.iktdev.library.subtitle.classes
|
||||||
|
|
||||||
|
import java.util.*
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
|
data class Time(val hour: Int, val minute: Int, val seconds: Int, val milliSeconds: Int) {
|
||||||
|
companion object {
|
||||||
|
private val cDotFormat = arrayOf(":",".")
|
||||||
|
private val cCFormat = arrayOf(":", ",")
|
||||||
|
|
||||||
|
private fun fromSplatter(value: List<String>): Array<String> {
|
||||||
|
return arrayOf<String>(
|
||||||
|
value[0],
|
||||||
|
value[1],
|
||||||
|
value[2],
|
||||||
|
value[3]
|
||||||
|
)
|
||||||
|
/*val h = value[0].toInt()
|
||||||
|
val m = value[1].toInt()
|
||||||
|
val s = if (value.size > 2) value[2].toInt() else 0
|
||||||
|
val f = if (value.size > 3) value[3].toInt() else 0*/
|
||||||
|
//return Time(h, m, s, f)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun toTimeFromSplatter(timeBreak: Array<String>): Time {
|
||||||
|
val h = timeBreak[0].toInt()
|
||||||
|
val m = timeBreak[1].toInt()
|
||||||
|
val s = when (timeBreak[2].length) {
|
||||||
|
2 -> timeBreak[2].toInt()
|
||||||
|
1 -> "0${timeBreak[2]}".toInt()
|
||||||
|
else -> 0
|
||||||
|
}
|
||||||
|
val f = when(timeBreak[3].length) {
|
||||||
|
3 -> timeBreak[3].toInt()
|
||||||
|
2 -> "0${timeBreak[3]}".toInt()
|
||||||
|
else -> 0
|
||||||
|
}
|
||||||
|
return Time(h, m, s, f)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun fromAss(value: String): Time {
|
||||||
|
// ASS Timespec is : h:mm:ss:xx
|
||||||
|
val timed = fromSplatter(value.split(delimiters = cDotFormat))
|
||||||
|
timed[3] = timed[3] + "0"
|
||||||
|
return toTimeFromSplatter(timed)
|
||||||
|
}
|
||||||
|
fun fromSrt(value: String): Time {
|
||||||
|
return toTimeFromSplatter(fromSplatter(value.split(delimiters = cCFormat)))
|
||||||
|
}
|
||||||
|
fun fromSmi(value: Long): Time {
|
||||||
|
val timed = Calendar.getInstance().apply {
|
||||||
|
timeInMillis = value
|
||||||
|
}
|
||||||
|
return Time(timed.get(Calendar.HOUR), timed.get(Calendar.MINUTE), timed.get(Calendar.SECOND), timed.get(Calendar.MILLISECOND))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun fromMilliseconds(value: Long): Time {
|
||||||
|
val timed = Calendar.getInstance().apply {
|
||||||
|
timeInMillis = value
|
||||||
|
}
|
||||||
|
return Time(timed.get(Calendar.HOUR), timed.get(Calendar.MINUTE), timed.get(Calendar.SECOND), timed.get(Calendar.MILLISECOND))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun hourString(): String {
|
||||||
|
return if (hour < 10) "0$hour" else hour.toString()
|
||||||
|
}
|
||||||
|
private fun minuteString(): String {
|
||||||
|
return if (minute < 10) "0$minute" else minute.toString()
|
||||||
|
}
|
||||||
|
private fun secondString(): String {
|
||||||
|
return if (seconds < 10) "0$seconds" else seconds.toString()
|
||||||
|
}
|
||||||
|
private fun millisecondString(): String {
|
||||||
|
return when (milliSeconds.toString().length) {
|
||||||
|
1 -> "00$milliSeconds"
|
||||||
|
2 -> "0$milliSeconds"
|
||||||
|
3 -> milliSeconds.toString()
|
||||||
|
else -> "000"
|
||||||
|
}
|
||||||
|
|
||||||
|
//return if (milliSeconds < 10) (milliSeconds.toString() + "00") else if (milliSeconds < 100) (milliSeconds.toString() + "0") else milliSeconds.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun toAss(): String {
|
||||||
|
return "${hourString().toInt()}:${minuteString()}:${secondString()}.${millisecondString().dropLast(1)}"
|
||||||
|
}
|
||||||
|
|
||||||
|
fun toSrt(): String {
|
||||||
|
return "${hourString()}:${minuteString()}:${secondString()},${millisecondString()}"
|
||||||
|
}
|
||||||
|
fun toVtt(): String {
|
||||||
|
return "${hourString()}:${minuteString()}:${secondString()}.${millisecondString()}"
|
||||||
|
}
|
||||||
|
fun inMilli(): Long = toMilli()
|
||||||
|
fun toMilli(): Long {
|
||||||
|
return TimeUnit.HOURS.toMillis(hour.toLong()) +
|
||||||
|
TimeUnit.MINUTES.toMillis(minute.toLong()) +
|
||||||
|
TimeUnit.SECONDS.toMillis(seconds.toLong()) +
|
||||||
|
milliSeconds
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
package no.iktdev.library.subtitle.cleaner
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes noisy characters from the given text. Noisy characters are sequences of the same non-whitespace character
|
||||||
|
* that occur more than four times in a row.
|
||||||
|
*
|
||||||
|
* @param text the text to scrub
|
||||||
|
* @return the scrubbed text
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* // Returns "Hello world! This is a test."
|
||||||
|
* scrubNoisyCharacters("Hheeeeelllooooo wworllld! This isssss aaaaa teessst.")
|
||||||
|
*/
|
||||||
|
fun scrubNoisyCharacters(text: String): String {
|
||||||
|
return text.replace(Regex("(?<!\\S)(\\S)(?:\\s*\\1){4,}(?!\\S)"), "")
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes any sequence in the given text where a character with a backslash precedes a letter
|
||||||
|
* and this sequence is repeated more than twice consecutively.
|
||||||
|
*
|
||||||
|
* @param text the text to scrub
|
||||||
|
* @return the scrubbed text
|
||||||
|
*
|
||||||
|
* Example usage:
|
||||||
|
* scrubSlashers("\\\\\\\\t\\t\\t\\n\\n\\n") -> ""
|
||||||
|
* scrubSlashers("some\t\t\ttext with spaces\t\t\tand\t\t\ttabs") -> "some text with spaces and tabs"
|
||||||
|
*/
|
||||||
|
fun scrubSlashers(text: String): String {
|
||||||
|
val pattern = "(\\\\[A-Za-z])(?:\\1*)\\1+".toRegex()
|
||||||
|
val result = text.replace(pattern, " ")
|
||||||
|
return result.trim().replace(Regex("\\h|\\s+"), " ")
|
||||||
|
}
|
||||||
+14
@@ -0,0 +1,14 @@
|
|||||||
|
package no.iktdev.library.subtitle.cleaner
|
||||||
|
|
||||||
|
class SubStationAlphaScrubber {
|
||||||
|
companion object {
|
||||||
|
// ASS override blocks contain styling/drawing commands. Removing the entire
|
||||||
|
// block leaves the visible subtitle text intact, including inline overrides.
|
||||||
|
private val overrideBlock = Regex("\\{[^}]*}")
|
||||||
|
|
||||||
|
fun stripTagging(text: String): String {
|
||||||
|
return overrideBlock.replace(text, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
package no.iktdev.library.subtitle.export
|
||||||
|
|
||||||
|
import no.iktdev.library.subtitle.Configuration
|
||||||
|
import no.iktdev.library.subtitle.classes.Dialog
|
||||||
|
import no.iktdev.library.subtitle.export.formats.JsonDocument
|
||||||
|
import no.iktdev.library.subtitle.export.formats.Subrip
|
||||||
|
import no.iktdev.library.subtitle.export.formats.SynchronizedAccessibleMediaInterchange
|
||||||
|
import no.iktdev.library.subtitle.export.formats.VideoTextTracks
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param sourceFile File the dialogs are created from
|
||||||
|
*/
|
||||||
|
class Export(val sourceFile: File, val folder: File, val baseName: String) {
|
||||||
|
|
||||||
|
fun write(dialogs: List<Dialog>): MutableList<File> {
|
||||||
|
val writtenFiles: MutableList<File> = mutableListOf()
|
||||||
|
val ext = sourceFile.extension
|
||||||
|
if (!ext.contains("srt", true))
|
||||||
|
writtenFiles.add(writeSrt(dialogs))
|
||||||
|
if (!ext.contains("vtt", true))
|
||||||
|
writtenFiles.add(writeVtt(dialogs))
|
||||||
|
if (!ext.contains("smi", true) && !ext.contains("sami", true))
|
||||||
|
writtenFiles.add(writeSmi(dialogs))
|
||||||
|
if (Configuration.exportJson) {
|
||||||
|
JsonDocument(folder = folder, baseName = baseName).write(dialogs)
|
||||||
|
}
|
||||||
|
return writtenFiles
|
||||||
|
}
|
||||||
|
|
||||||
|
fun writeSrt(dialogs: List<Dialog>): File {
|
||||||
|
return Subrip(folder = folder, baseName = baseName).write(dialogs)
|
||||||
|
}
|
||||||
|
fun writeVtt(dialogs: List<Dialog>,): File {
|
||||||
|
return VideoTextTracks(folder = folder, baseName = baseName).write(dialogs)
|
||||||
|
}
|
||||||
|
fun writeSmi(dialogs: List<Dialog>,): File {
|
||||||
|
val smi = SynchronizedAccessibleMediaInterchange(folder = folder, baseName = baseName)
|
||||||
|
return smi.write(dialogs)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package no.iktdev.library.subtitle.export
|
||||||
|
|
||||||
|
import no.iktdev.library.subtitle.Configuration
|
||||||
|
import no.iktdev.library.subtitle.classes.Dialog
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
abstract class Exporter(val folder: File, val baseName: String) {
|
||||||
|
|
||||||
|
fun outFile(extension: String): File {
|
||||||
|
return File(folder, "$baseName.$extension")
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract fun format(dialogs: List<Dialog>): List<String>
|
||||||
|
abstract fun write(dialogs: List<Dialog>): File
|
||||||
|
|
||||||
|
fun deleteOnExistAndOverwriteAllowed(file: File) {
|
||||||
|
if (file.exists() && Configuration.allowOverwrite) {
|
||||||
|
file.delete()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package no.iktdev.library.subtitle.export.formats
|
||||||
|
|
||||||
|
import com.google.gson.Gson
|
||||||
|
import no.iktdev.library.subtitle.classes.Dialog
|
||||||
|
import no.iktdev.library.subtitle.export.Exporter
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
class JsonDocument(folder: File, baseName: String): Exporter(folder, baseName) {
|
||||||
|
override fun format(dialogs: List<Dialog>): List<String> {
|
||||||
|
// DO nothing
|
||||||
|
return listOf()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun write(dialogs: List<Dialog>): File {
|
||||||
|
val file = outFile("json")
|
||||||
|
deleteOnExistAndOverwriteAllowed(file)
|
||||||
|
|
||||||
|
val data = Gson().toJson(dialogs)
|
||||||
|
file.printWriter().use { out ->
|
||||||
|
out.print(data)
|
||||||
|
}
|
||||||
|
return file
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
package no.iktdev.library.subtitle.export.formats
|
||||||
|
|
||||||
|
import no.iktdev.library.subtitle.Configuration
|
||||||
|
import no.iktdev.library.subtitle.classes.Dialog
|
||||||
|
import no.iktdev.library.subtitle.export.Exporter
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
class Subrip(folder: File, baseName: String): Exporter(folder, baseName) {
|
||||||
|
|
||||||
|
override fun format(dialogs: List<Dialog>): List<String> {
|
||||||
|
val formatted: MutableList<String> = mutableListOf()
|
||||||
|
dialogs.withIndex().forEach {
|
||||||
|
formatted.add((it.index+1).toString()) //Adds counter to SRT
|
||||||
|
val time = "${it.value.start.toSrt()} --> ${it.value.end.toSrt()}"
|
||||||
|
formatted.add(time)
|
||||||
|
formatted.add(it.value.text)
|
||||||
|
formatted.add(Configuration.carriageReturn)
|
||||||
|
}
|
||||||
|
return formatted
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
override fun write(dialogs: List<Dialog>): File {
|
||||||
|
val file = outFile("srt")
|
||||||
|
deleteOnExistAndOverwriteAllowed(file)
|
||||||
|
val formatted = format(dialogs)
|
||||||
|
file.printWriter().use { out ->
|
||||||
|
formatted.forEach {
|
||||||
|
out.println(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return file
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
Executable
+62
@@ -0,0 +1,62 @@
|
|||||||
|
package no.iktdev.library.subtitle.export.formats
|
||||||
|
|
||||||
|
import no.iktdev.library.subtitle.Configuration
|
||||||
|
import no.iktdev.library.subtitle.classes.Dialog
|
||||||
|
import no.iktdev.library.subtitle.classes.Time
|
||||||
|
import no.iktdev.library.subtitle.export.Exporter
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
class SynchronizedAccessibleMediaInterchange(folder: File, baseName: String): Exporter(folder, baseName) {
|
||||||
|
|
||||||
|
|
||||||
|
override fun format(dialogs: List<Dialog>): List<String> {
|
||||||
|
val formatted: MutableList<String> = mutableListOf()
|
||||||
|
formatted.add("<sami>")
|
||||||
|
formatted.add("\t<head>")
|
||||||
|
formatted.add("\t\t<title>${baseName}</title>")
|
||||||
|
formatted.add("\t</head>")
|
||||||
|
formatted.add("\t<body>")
|
||||||
|
dialogs.forEach { dialog ->
|
||||||
|
getStartItem(dialog).forEach { line ->
|
||||||
|
formatted.add(line)
|
||||||
|
}
|
||||||
|
getEndItem(dialog.end).forEach { line ->
|
||||||
|
formatted.add(line)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
formatted.add("\t</body>")
|
||||||
|
formatted.add("</sami>")
|
||||||
|
return formatted
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getStartItem(dialog: Dialog): List<String> {
|
||||||
|
return listOf(
|
||||||
|
"\t\t<sync Start=\"${dialog.start.inMilli()}\">",
|
||||||
|
"\t\t\t<p>${dialog.text}</p>",
|
||||||
|
"\t\t</sync>",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getEndItem(time: Time): List<String> {
|
||||||
|
return listOf(
|
||||||
|
"\t\t<sync Start=\"${time.inMilli()}\">",
|
||||||
|
"\t\t\t<p>&nbsp;</p>",
|
||||||
|
"\t\t</sync>",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun write(dialogs: List<Dialog>): File {
|
||||||
|
val file = outFile("smi")
|
||||||
|
deleteOnExistAndOverwriteAllowed(file)
|
||||||
|
val formatted = format(dialogs)
|
||||||
|
file.printWriter().use { out ->
|
||||||
|
formatted.forEach {
|
||||||
|
out.println(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return file
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
+49
@@ -0,0 +1,49 @@
|
|||||||
|
package no.iktdev.library.subtitle.export.formats
|
||||||
|
|
||||||
|
import no.iktdev.library.subtitle.classes.Dialog
|
||||||
|
import no.iktdev.library.subtitle.classes.Time
|
||||||
|
import no.iktdev.library.subtitle.export.Exporter
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
class VideoTextTracks(folder: File, baseName: String): Exporter(folder, baseName) {
|
||||||
|
|
||||||
|
override fun format(dialogs: List<Dialog>): List<String> {
|
||||||
|
val formatted: MutableList<String> = mutableListOf()
|
||||||
|
|
||||||
|
formatted.add("WEBVTT")
|
||||||
|
formatted.add(" ")
|
||||||
|
formatted.add(" ")
|
||||||
|
|
||||||
|
dialogs.forEach {
|
||||||
|
val time = "${it.start.toVtt()} --> ${it.end.toVtt()}"
|
||||||
|
formatted.add(time)
|
||||||
|
formatted.add(it.text)
|
||||||
|
formatted.add("")
|
||||||
|
formatted.add("")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Since some devices makes the subtitle stuck on the screen after the trakc has ended
|
||||||
|
val overflowStart = dialogs.last().end
|
||||||
|
val overflowEnd = Time.fromMilliseconds(overflowStart.inMilli() + 2000)
|
||||||
|
val time = "${overflowStart.toVtt()} --> ${overflowEnd.toVtt()}"
|
||||||
|
formatted.add(time)
|
||||||
|
formatted.add("")
|
||||||
|
formatted.add("")
|
||||||
|
formatted.add("")
|
||||||
|
return formatted
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
override fun write(dialogs: List<Dialog>): File {
|
||||||
|
val file = outFile("vtt")
|
||||||
|
deleteOnExistAndOverwriteAllowed(file)
|
||||||
|
val formatted = format(dialogs)
|
||||||
|
file.printWriter().use { out ->
|
||||||
|
formatted.forEach {
|
||||||
|
out.println(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return file
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
package no.iktdev.library.subtitle.reader
|
||||||
|
|
||||||
|
import no.iktdev.library.subtitle.Configuration
|
||||||
|
import no.iktdev.library.subtitle.classes.Dialog
|
||||||
|
import no.iktdev.library.subtitle.classes.DialogType
|
||||||
|
import no.iktdev.library.subtitle.classes.SubstationAlphaDialogData
|
||||||
|
import no.iktdev.library.subtitle.classes.Time
|
||||||
|
import no.iktdev.library.subtitle.cleaner.SubStationAlphaScrubber
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
open class AssReader(file: File? = null): BaseReader(file) {
|
||||||
|
|
||||||
|
fun isItalic(value: SubstationAlphaDialogData): Boolean {
|
||||||
|
return hasOverride(value.text, 'i')
|
||||||
|
}
|
||||||
|
|
||||||
|
fun isBold(value: SubstationAlphaDialogData): Boolean {
|
||||||
|
return hasOverride(value.text, 'b')
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun hasOverride(text: String, command: Char): Boolean {
|
||||||
|
// ASS allows multiple commands in one block, e.g. {\\an8\\i1}.
|
||||||
|
val commandRegex = Regex("\\\\${command}([01])", RegexOption.IGNORE_CASE)
|
||||||
|
return SubStationAlphaScrubberBlocks.findAll(text)
|
||||||
|
.flatMap { commandRegex.findAll(it.value) }
|
||||||
|
.any { it.groupValues[1] == "1" }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun isSignOrSong(value: SubstationAlphaDialogData): Boolean {
|
||||||
|
val marker = Regex("(?i)(^|[^a-z])(op|ed|sign|song)([^a-z]|$)")
|
||||||
|
return marker.containsMatchIn(value.layer) || marker.containsMatchIn(value.style) ||
|
||||||
|
marker.containsMatchIn(value.name)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun isDialog(line: String): Boolean {
|
||||||
|
return line.trimStart().startsWith("Dialogue:", ignoreCase = true) ||
|
||||||
|
line.trimStart().startsWith("Dialog:", ignoreCase = true)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun applyCorrectNewLine(line: String): String {
|
||||||
|
return line.replace(Regex("\\\\[Nn]"), Configuration.carriageReturn)
|
||||||
|
.replace("\\h", "\u00a0")
|
||||||
|
}
|
||||||
|
|
||||||
|
fun deserialize(line: String): SubstationAlphaDialogData? {
|
||||||
|
if (!isDialog(line))
|
||||||
|
return null
|
||||||
|
val dialog = line.substringAfter(':', "")
|
||||||
|
val segment = dialog.split(',', limit = 10)
|
||||||
|
if (segment.size != 10)
|
||||||
|
return null
|
||||||
|
return SubstationAlphaDialogData(
|
||||||
|
layer = segment[0],
|
||||||
|
start = segment[1],
|
||||||
|
end = segment[2],
|
||||||
|
style = segment[3],
|
||||||
|
name = segment[4],
|
||||||
|
marginL = segment[5],
|
||||||
|
marginR = segment[6],
|
||||||
|
marginV = segment[7],
|
||||||
|
effect = segment[8],
|
||||||
|
text = segment[9]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fun transform(it: SubstationAlphaDialogData): Dialog {
|
||||||
|
val text = applyCorrectNewLine(SubStationAlphaScrubber.stripTagging(it.text))
|
||||||
|
|
||||||
|
|
||||||
|
return Dialog(
|
||||||
|
start = Time.fromAss(it.start),
|
||||||
|
end = Time.fromAss(it.end),
|
||||||
|
text = text,
|
||||||
|
type = if (isSignOrSong(it))
|
||||||
|
DialogType.SIGN_SONG
|
||||||
|
else if (isItalic(it))
|
||||||
|
DialogType.DIALOG_ITALIC
|
||||||
|
else if (isBold(it))
|
||||||
|
DialogType.DIALOG_BOLD
|
||||||
|
else
|
||||||
|
DialogType.DIALOG_NORMAL,
|
||||||
|
ignore = shouldIgnore(text)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun shouldIgnore(text: String): Boolean {
|
||||||
|
val regex = "(?i)[a-z]\\s(?:-?\\d+\\.?\\d*\\s){1,2}(?:l\\s-?\\d+\\.?\\d*\\s-?\\d+\\.?\\d*\\s?)*"
|
||||||
|
return Regex(regex).matches(text)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun read(): List<Dialog> {
|
||||||
|
val ssad = getLines().mapNotNull { deserialize(it) }
|
||||||
|
return ssad.map { transform(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private companion object {
|
||||||
|
// Keep classification limited to ASS override blocks so ordinary text such
|
||||||
|
// as "I" or "bold" cannot be mistaken for a formatting command.
|
||||||
|
val SubStationAlphaScrubberBlocks = Regex("\\{[^}]*}")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
package no.iktdev.library.subtitle.reader
|
||||||
|
|
||||||
|
import no.iktdev.library.subtitle.classes.Dialog
|
||||||
|
import no.iktdev.library.subtitle.cleaner.scrubNoisyCharacters
|
||||||
|
import no.iktdev.library.subtitle.cleaner.scrubSlashers
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
abstract class BaseReader(file: File? = null) {
|
||||||
|
private val lines: MutableList<String> = mutableListOf()
|
||||||
|
fun getLines(): List<String> {
|
||||||
|
return lines
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
if (file != null) {
|
||||||
|
val _lines = file.readLines(charset = Charsets.UTF_8)
|
||||||
|
.map {
|
||||||
|
scrubSlashers(it)
|
||||||
|
}.map {
|
||||||
|
scrubNoisyCharacters(it)
|
||||||
|
}
|
||||||
|
lines.addAll(_lines)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
abstract fun read(): List<Dialog>
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
package no.iktdev.library.subtitle.reader
|
||||||
|
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
class Reader(val file: File) {
|
||||||
|
|
||||||
|
fun getSubtitleReader(): BaseReader? {
|
||||||
|
if (!file.exists())
|
||||||
|
return null
|
||||||
|
return when (file.extension) {
|
||||||
|
"ass" -> AssReader(file)
|
||||||
|
"srt" -> SrtReader(file)
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
package no.iktdev.library.subtitle.reader
|
||||||
|
|
||||||
|
import no.iktdev.library.subtitle.Configuration
|
||||||
|
import no.iktdev.library.subtitle.classes.Dialog
|
||||||
|
import no.iktdev.library.subtitle.classes.DialogType
|
||||||
|
import no.iktdev.library.subtitle.classes.SubripData
|
||||||
|
import no.iktdev.library.subtitle.classes.Time
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
class SrtReader(file: File? = null): BaseReader(file) {
|
||||||
|
|
||||||
|
fun isNumberLine(line: String): Boolean {
|
||||||
|
val lineNumberRegex = Regex("^[0-9]*\$")
|
||||||
|
return lineNumberRegex.containsMatchIn(line)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun isTimeLine(line: String): Boolean {
|
||||||
|
val timeRegex = Regex("\\d{2}:\\d{2}:\\d{2},\\d{3}\\s-->\\s\\d{2}:\\d{2}:\\d{2},\\d{3}")
|
||||||
|
return timeRegex.containsMatchIn(line)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun isSpaceLine(line: String): Boolean {
|
||||||
|
return line.isBlank()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun group(lines: List<String>): MutableList<List<String>> {
|
||||||
|
val grouped: MutableList<List<String>> = mutableListOf()
|
||||||
|
val cache = mutableListOf<String>()
|
||||||
|
|
||||||
|
lines.forEach {
|
||||||
|
if (it.isNotEmpty() && it.isNotBlank())
|
||||||
|
cache.add(it)
|
||||||
|
|
||||||
|
if (it.isEmpty() || it.isBlank() || lines.last() == it) {
|
||||||
|
grouped.add(cache.toList())
|
||||||
|
cache.clear()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return grouped
|
||||||
|
}
|
||||||
|
|
||||||
|
fun deserialize(group: List<String>): SubripData? {
|
||||||
|
if (group.size < 3) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
val number = group[0].also {
|
||||||
|
if (!isNumberLine(it) || it.isBlank()) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val timeLine = group[1].also {
|
||||||
|
if (!isTimeLine(it) || it.isBlank()) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val textLines = group.subList(2, group.size).also {
|
||||||
|
if (it.isEmpty()) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return SubripData(
|
||||||
|
number,
|
||||||
|
timeLine,
|
||||||
|
textLines.toMutableList()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun transform(it: SubripData): Dialog {
|
||||||
|
val timeLines = it.time.split("-->")
|
||||||
|
val start = timeLines.first().trim()
|
||||||
|
val end = timeLines.last().trim()
|
||||||
|
var text = it.text.joinToString(Configuration.carriageReturn)
|
||||||
|
return Dialog(start = Time.fromSrt(start), end = Time.fromSrt(end), text = text, type = DialogType.DIALOG_NORMAL)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
override fun read(): List<Dialog> {
|
||||||
|
val srts = group(getLines()).mapNotNull { deserialize(it) }
|
||||||
|
return srts.map { transform(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
package no.iktdev.library.subtitle.reader
|
||||||
|
|
||||||
|
class VttReader {
|
||||||
|
}
|
||||||
Executable
+55
@@ -0,0 +1,55 @@
|
|||||||
|
open class Resources {
|
||||||
|
open val path: List<String> = listOf("")
|
||||||
|
|
||||||
|
fun getText(path: List<String>): String? {
|
||||||
|
val fullPath = path.joinToString("/")
|
||||||
|
return this.javaClass.classLoader.getResource(fullPath)?.readText()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getLines(path: List<String>): List<String> {
|
||||||
|
val fullPath = path.joinToString("/")
|
||||||
|
return this.javaClass.getResourceAsStream(fullPath)?.bufferedReader()?.readLines() ?: emptyList()
|
||||||
|
}
|
||||||
|
|
||||||
|
open class Output: Resources() {
|
||||||
|
override val path: List<String> = listOf("output")
|
||||||
|
open class Encoder(): Output() {
|
||||||
|
override val path: List<String> = listOf("output", "encoder")
|
||||||
|
|
||||||
|
fun sample1(): List<String> {
|
||||||
|
val text = getText(listOf(*path.toTypedArray(), "sample1.txt")) ?: return emptyList()
|
||||||
|
return text.split(System.lineSeparator(), ignoreCase = true)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun sample2(): List<String> {
|
||||||
|
val text = getText(listOf(*path.toTypedArray(), "sample2.txt")) ?: return emptyList()
|
||||||
|
return text.split(System.lineSeparator(), ignoreCase = true)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
fun sample3(): List<String> {
|
||||||
|
val text = getText(listOf(*path.toTypedArray(), "sample3.txt")) ?: return emptyList()
|
||||||
|
return text.split(System.lineSeparator(), ignoreCase = true)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
fun sample4(): List<String> {
|
||||||
|
val text = getText(listOf(*path.toTypedArray(), "sample4.txt")) ?: return emptyList()
|
||||||
|
return text.split(System.lineSeparator(), ignoreCase = true)
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
open class Streams: Output() {
|
||||||
|
override val path: List<String> = listOf("output", "streams")
|
||||||
|
|
||||||
|
fun getSample(number: Int): String {
|
||||||
|
return getText(listOf(*path.toTypedArray(), "sample$number.json"))!!
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
package no.iktdev.library.subtitle
|
||||||
|
|
||||||
|
/** Small synthetic inputs that preserve parser edge cases without shipping subtitle files. */
|
||||||
|
object SubtitleFixtures {
|
||||||
|
val ass: List<String> = """
|
||||||
|
[Script Info]
|
||||||
|
Title: Synthetic parser fixture
|
||||||
|
ScriptType: v4.00+
|
||||||
|
|
||||||
|
[Events]
|
||||||
|
Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
|
||||||
|
Dialogue: 0,0:00:01.00,0:00:03.00,Default,Traveler,0,0,0,,{\an8\i1}A fictional greeting, traveler.{\i0}\NWelcome.
|
||||||
|
Dialogue: 0,0:00:03.20,0:00:05.00,Default,Guide,0,0,0,,{\b1}This invented line is bold.{\b0}
|
||||||
|
Dialogue: 0,0:00:05.10,0:00:06.50,Sign_Scene,Sign,0,0,0,,A made-up sign
|
||||||
|
""".trimIndent().lines()
|
||||||
|
|
||||||
|
val srt: List<String> = """
|
||||||
|
1
|
||||||
|
00:00:01,000 --> 00:00:02,500
|
||||||
|
A fictional greeting.
|
||||||
|
|
||||||
|
2
|
||||||
|
00:00:03,000 --> 00:00:04,500
|
||||||
|
Another invented line,
|
||||||
|
split across two rows.
|
||||||
|
|
||||||
|
3
|
||||||
|
00:00:05,000 --> 00:00:06,000
|
||||||
|
The imaginary scene ends.
|
||||||
|
""".trimIndent().lines()
|
||||||
|
}
|
||||||
+138
@@ -0,0 +1,138 @@
|
|||||||
|
package no.iktdev.library.subtitle
|
||||||
|
|
||||||
|
import no.iktdev.library.subtitle.classes.Dialog
|
||||||
|
import no.iktdev.library.subtitle.classes.DialogType
|
||||||
|
import no.iktdev.library.subtitle.classes.Time
|
||||||
|
import no.iktdev.library.subtitle.reader.AssReader
|
||||||
|
import org.junit.Assert.*
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
class SyncroTest: Syncro() {
|
||||||
|
|
||||||
|
private val baseSet1 = listOf<Dialog>(
|
||||||
|
Dialog(
|
||||||
|
start = Time(0, 0,0,0),
|
||||||
|
end = Time(0,0,10,0),
|
||||||
|
text = "Start item",
|
||||||
|
type = DialogType.DIALOG_NORMAL
|
||||||
|
),
|
||||||
|
Dialog(
|
||||||
|
id = "target",
|
||||||
|
start = Time(0, 0,10,0),
|
||||||
|
end = Time(0,0,20,0),
|
||||||
|
text = "Item #1A",
|
||||||
|
type = DialogType.DIALOG_NORMAL
|
||||||
|
),
|
||||||
|
Dialog(
|
||||||
|
start = Time(0, 0,15,0),
|
||||||
|
end = Time(0,0,20,0),
|
||||||
|
text = "Item #1B",
|
||||||
|
type = DialogType.DIALOG_NORMAL
|
||||||
|
),
|
||||||
|
Dialog(
|
||||||
|
start = Time(0, 0,25,0),
|
||||||
|
end = Time(0,0,30,0),
|
||||||
|
text = "Item #2",
|
||||||
|
type = DialogType.DIALOG_NORMAL
|
||||||
|
),
|
||||||
|
Dialog(
|
||||||
|
start = Time(0, 0,30,0),
|
||||||
|
end = Time(0,0,40,0),
|
||||||
|
text = "Item 3",
|
||||||
|
type = DialogType.DIALOG_NORMAL
|
||||||
|
)
|
||||||
|
)
|
||||||
|
get() = field.toList()
|
||||||
|
|
||||||
|
private val baseSet2 = listOf<Dialog>(
|
||||||
|
Dialog(
|
||||||
|
start = Time(0, 0,0,0),
|
||||||
|
end = Time(0,0,10,0),
|
||||||
|
text = "Start item",
|
||||||
|
type = DialogType.DIALOG_NORMAL
|
||||||
|
),
|
||||||
|
Dialog(
|
||||||
|
id = "target",
|
||||||
|
start = Time(0, 0,10,0),
|
||||||
|
end = Time(0,0,25,0),
|
||||||
|
text = "Item #1A",
|
||||||
|
type = DialogType.DIALOG_NORMAL
|
||||||
|
),
|
||||||
|
Dialog(
|
||||||
|
start = Time(0, 0,15,0),
|
||||||
|
end = Time(0,0,20,0),
|
||||||
|
text = "Item #1B",
|
||||||
|
type = DialogType.DIALOG_NORMAL
|
||||||
|
),
|
||||||
|
Dialog(
|
||||||
|
start = Time(0, 0,25,0),
|
||||||
|
end = Time(0,0,30,0),
|
||||||
|
text = "Item #2",
|
||||||
|
type = DialogType.DIALOG_NORMAL
|
||||||
|
),
|
||||||
|
Dialog(
|
||||||
|
start = Time(0, 0,30,0),
|
||||||
|
end = Time(0,0,40,0),
|
||||||
|
text = "Item 3",
|
||||||
|
type = DialogType.DIALOG_NORMAL
|
||||||
|
)
|
||||||
|
)
|
||||||
|
get() = field.toList()
|
||||||
|
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testTrackSegmentation() {
|
||||||
|
val mockMergable = baseSet1
|
||||||
|
|
||||||
|
val segmentation = trackSegments(mockMergable)
|
||||||
|
assertTrue("List is empty", segmentation.isNotEmpty())
|
||||||
|
assertEquals(1, segmentation.size)
|
||||||
|
assertTrue(mockMergable.find { it.id == "target" }?.end?.seconds == 15)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testTrackSegmentation2() {
|
||||||
|
val mockMergable = baseSet2
|
||||||
|
|
||||||
|
val segmentation = trackSegments(mockMergable)
|
||||||
|
assertTrue("List is empty", segmentation.isNotEmpty())
|
||||||
|
assertEquals(2, segmentation.size)
|
||||||
|
assertTrue(mockMergable.find { it.id == "target" }?.end?.seconds == 15)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testTrackMerging() {
|
||||||
|
val mockMergable = baseSet2
|
||||||
|
|
||||||
|
val merged = tracks(mockMergable)
|
||||||
|
assertTrue("List is empty", merged.isNotEmpty())
|
||||||
|
assertEquals(6, merged.size)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testSync() {
|
||||||
|
val merged = sync(baseSet2)
|
||||||
|
assertTrue("List is empty", merged.isNotEmpty())
|
||||||
|
assertEquals(6, merged.size)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testAphocracy() {
|
||||||
|
val reader = AssReader();
|
||||||
|
val lines = SubtitleFixtures.ass
|
||||||
|
val deserialized = lines.mapNotNull { reader.deserialize(it) }
|
||||||
|
val mapped = deserialized.map { reader.transform(it) }
|
||||||
|
|
||||||
|
val merged = sync(mapped)
|
||||||
|
assertTrue(merged.size == mapped.size)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testSyncroNonDuplicates() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
package no.iktdev.library.subtitle
|
||||||
|
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
import org.junit.jupiter.api.assertDoesNotThrow
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
class TestFileExtensionChange {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testSrt() {
|
||||||
|
val outFile = File("/tmp/test/file/sample-subtitle.ass")
|
||||||
|
val fullpath = outFile.absolutePath
|
||||||
|
val srtFile = fullpath.replaceAfterLast(".", "srt")
|
||||||
|
assertEquals("/tmp/test/file/sample-subtitle.srt", srtFile)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun extensionComparison() {
|
||||||
|
val ext = File("/tmp/test/file/sample-subtitle.ass").extension
|
||||||
|
assertEquals(ext, "ass")
|
||||||
|
assertDoesNotThrow {
|
||||||
|
if (!ext.contains("ass", true)) {
|
||||||
|
throw RuntimeException("Not supposed to end up here")
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+90
@@ -0,0 +1,90 @@
|
|||||||
|
package no.iktdev.library.subtitle
|
||||||
|
|
||||||
|
import no.iktdev.library.subtitle.classes.DialogType
|
||||||
|
import no.iktdev.library.subtitle.reader.AssReader
|
||||||
|
import no.iktdev.library.subtitle.reader.SrtReader
|
||||||
|
import org.junit.Assert.*
|
||||||
|
import org.junit.Test
|
||||||
|
import org.assertj.core.api.Assertions.assertThat
|
||||||
|
|
||||||
|
class TestRun {
|
||||||
|
|
||||||
|
val reader = AssReader()
|
||||||
|
|
||||||
|
private fun hasDuplicates(items: List<String>): Boolean {
|
||||||
|
return items.size != items.distinct().size
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testSyntheticAssSync() {
|
||||||
|
val reader = AssReader();
|
||||||
|
val ssad = SubtitleFixtures.ass.mapNotNull { reader.deserialize(it) }
|
||||||
|
val mapped = ssad.map { reader.transform(it) }
|
||||||
|
val synced = Syncro().sync(mapped)
|
||||||
|
|
||||||
|
synced.forEach {
|
||||||
|
val lines = it.text.split(Configuration.carriageReturn)
|
||||||
|
assert(!hasDuplicates(lines)) {
|
||||||
|
"Duplicates found with \r\n" + it.text
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testShouldIgnore1() {
|
||||||
|
assertTrue(reader.shouldIgnore("m 0 0 l 50 0 l 50 88 l 0 88"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testTransform() {
|
||||||
|
val text = "Dialogue: 0,0:03:28.06,0:03:34.06,Sign_Basic,Sign,0,0,0,,{\\fad(500,550)\\p1\\bord0\\xshad1\\yshad2\\c&H000024&\\4c&H25254C&\\pos(65,268)}m 0 0 l 50 0 l 45 13 l -5 13"
|
||||||
|
val dez = reader.deserialize(text)
|
||||||
|
val transformed = reader.transform(dez!!)
|
||||||
|
assertNotNull(transformed)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testSyntheticAssRead() {
|
||||||
|
val reader = AssReader();
|
||||||
|
val lines = SubtitleFixtures.ass
|
||||||
|
val deserialized = lines.mapNotNull { reader.deserialize(it) }
|
||||||
|
val mapped = deserialized.map { reader.transform(it) }
|
||||||
|
|
||||||
|
assertEquals(deserialized.first().start, mapped.first().start.toAss())
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testAssFileParsing() {
|
||||||
|
val result = SubtitleFixtures.ass.mapNotNull { reader.deserialize(it) }.map { reader.transform(it) }
|
||||||
|
val filtered = result.filter { !it.ignore && it.type !in listOf(DialogType.SIGN_SONG, DialogType.CAPTION) }
|
||||||
|
// val syncOrNotSync = syncDialogs(filtered)
|
||||||
|
|
||||||
|
assertThat(result).isNotEmpty
|
||||||
|
assertThat(filtered.map { it.text }.joinToString("\n"))
|
||||||
|
.contains("A fictional greeting, traveler.")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testSyntheticSrt() {
|
||||||
|
val srtReader = SrtReader(null)
|
||||||
|
val reader = srtReader.group(SubtitleFixtures.srt)
|
||||||
|
.mapNotNull { srtReader.deserialize(it) }
|
||||||
|
.map { srtReader.transform(it) }
|
||||||
|
assertThat(reader).hasSize(3)
|
||||||
|
assertEquals("Another invented line,\r\nsplit across two rows.", reader[1].text)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun inlineSubtitleFixturesRunThroughBothParsers() {
|
||||||
|
val assDialogs = SubtitleFixtures.ass.mapNotNull { reader.deserialize(it) }.map { reader.transform(it) }
|
||||||
|
val srtReader = SrtReader(null)
|
||||||
|
val srtDialogs = srtReader.group(SubtitleFixtures.srt)
|
||||||
|
.mapNotNull { srtReader.deserialize(it) }
|
||||||
|
.map { srtReader.transform(it) }
|
||||||
|
|
||||||
|
assertThat(assDialogs).isNotEmpty
|
||||||
|
assertThat(srtDialogs).hasSize(3)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
+16
@@ -0,0 +1,16 @@
|
|||||||
|
package no.iktdev.library.subtitle
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
import no.iktdev.library.subtitle.classes.Time
|
||||||
|
import org.junit.jupiter.api.Assertions.assertEquals
|
||||||
|
|
||||||
|
class TestTime {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testParse() {
|
||||||
|
val time = Time.fromAss("0:00:56.82")
|
||||||
|
assertEquals(56, time.seconds)
|
||||||
|
assertEquals(820, time.milliSeconds)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
package no.iktdev.library.subtitle.classes
|
||||||
|
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.jupiter.api.Assertions.*
|
||||||
|
|
||||||
|
internal class TimeTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testFromAssToSrt1() {
|
||||||
|
val leadingZero = "0:00:14.04"
|
||||||
|
val testTime2 = "0:00:17.25"
|
||||||
|
|
||||||
|
val time = Time.fromAss(leadingZero)
|
||||||
|
assertEquals("00:00:14,040", time.toSrt())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testFromAssToSrt2() {
|
||||||
|
val leadingZero = "0:00:04.04"
|
||||||
|
val testTime2 = "0:00:17.25"
|
||||||
|
|
||||||
|
val time = Time.fromAss(leadingZero)
|
||||||
|
assertEquals("00:00:04,040", time.toSrt())
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testFromSrtToSrt1() {
|
||||||
|
val leadingZero = "00:00:14,040"
|
||||||
|
val time = Time.fromSrt(leadingZero)
|
||||||
|
assertEquals("00:00:14,040", time.toSrt())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testFromSrtToSrt2() {
|
||||||
|
val leadingZero = "00:00:14,004"
|
||||||
|
val time = Time.fromSrt(leadingZero)
|
||||||
|
assertEquals("00:00:14,004", time.toSrt())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testFromSrtToSrt3() {
|
||||||
|
val leadingZero = "00:00:04,004"
|
||||||
|
val time = Time.fromSrt(leadingZero)
|
||||||
|
assertEquals("00:00:04,004", time.toSrt())
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package no.iktdev.library.subtitle.cleaner
|
||||||
|
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.jupiter.api.Assertions.*
|
||||||
|
|
||||||
|
class LineScrubbersKtTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun removeTags() {
|
||||||
|
val tekst = "Name:\\h\\h\\h\\h\\h\\h\\h\\h\\h\\h\\h\\h\\h\\h\\h\\h\\h\\h\\h\\hUnspecified"
|
||||||
|
val scr = scrubSlashers(tekst)
|
||||||
|
assertEquals("Name: Unspecified", scr)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
+17
@@ -0,0 +1,17 @@
|
|||||||
|
package no.iktdev.library.subtitle.cleaner
|
||||||
|
|
||||||
|
import no.iktdev.library.subtitle.cleaner.SubStationAlphaScrubber
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
|
||||||
|
class SubStationAlphaScrubberTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun textIsScrubbed() {
|
||||||
|
val text = "{\\c&H2DFFFF&\\3c&H176EDC&\\bord2\\fs17\\an8\\pos(480,206)\\fad(1000,1)}WANNA FIGHT"
|
||||||
|
val result = SubStationAlphaScrubber.stripTagging(text)
|
||||||
|
val excpected = "WANNA FIGHT"
|
||||||
|
assertEquals("Unsupported values is removed cleanly",excpected, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
package no.iktdev.library.subtitle.reader
|
||||||
|
|
||||||
|
import no.iktdev.library.subtitle.Syncro
|
||||||
|
import no.iktdev.library.subtitle.SubtitleFixtures
|
||||||
|
import no.iktdev.library.subtitle.classes.DialogType
|
||||||
|
import no.iktdev.library.subtitle.classes.Time
|
||||||
|
import no.iktdev.library.subtitle.reader.AssReader
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.jupiter.api.Assertions.*
|
||||||
|
import org.junit.jupiter.api.assertDoesNotThrow
|
||||||
|
|
||||||
|
|
||||||
|
class AssReaderTest {
|
||||||
|
private val reader = AssReader(null)
|
||||||
|
val text = "Dialogue: 0,0:00:52.11,0:00:55.65,Default,Speaker,0,0,0,,{\\i1}A fictional pilot is speaking.{\\i0}"
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testIsDialog() {
|
||||||
|
assertTrue(reader.isDialog(text))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun assertDeserialization() {
|
||||||
|
val deserialized = reader.deserialize(text)
|
||||||
|
assertNotNull(deserialized)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun isAbleToDecodeTime() {
|
||||||
|
val deserialized = reader.deserialize(text) ?: throw Exception("Deserialized is null")
|
||||||
|
assertDoesNotThrow {
|
||||||
|
val parsed = Time.fromAss(deserialized.start)
|
||||||
|
assertEquals(52, parsed.seconds)
|
||||||
|
assertEquals(110, parsed.milliSeconds)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testSyntheticAss_ReadAndSync() {
|
||||||
|
val lines = SubtitleFixtures.ass
|
||||||
|
val ssad = lines.mapNotNull { reader.deserialize(it) }
|
||||||
|
val mapped = ssad.map { reader.transform(it) }
|
||||||
|
val synced = Syncro().sync(mapped.toList())
|
||||||
|
assertTrue(synced.isNotEmpty())
|
||||||
|
|
||||||
|
synced.forEach {
|
||||||
|
assertTrue(it.start.inMilli() <= it.end.inMilli(), "${it.text} has end time after start ${it.start.inMilli()}:${it.end.inMilli()}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun isDialog1() {
|
||||||
|
val line = "Dialogue: 0,0:04:11.05,0:04:15.62,Default,Speaker,0,0,0,,The map is ready, \\Nwe can leave now."
|
||||||
|
val deserialized = reader.deserialize(line)
|
||||||
|
val transformed = reader.transform(deserialized!!)
|
||||||
|
assertTrue(transformed.type != DialogType.SIGN_SONG)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun stripsDenseInlineTagsKeepsCommaTextAndDetectsPartialItalics() {
|
||||||
|
val line = "Dialogue: 0,0:00:01.00,0:00:02.00,Default,Speaker,0,0,0,,{\\fad(100,200)\\an8\\bord2\\fs20\\pos(12,34)\\i1}A fictional phrase, with commas.{\\i0} Normal again."
|
||||||
|
|
||||||
|
val parsed = reader.transform(reader.deserialize(line)!!)
|
||||||
|
|
||||||
|
assertEquals("A fictional phrase, with commas. Normal again.", parsed.text)
|
||||||
|
assertEquals(DialogType.DIALOG_ITALIC, parsed.type)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun convertsAssLineBreaksAndHardSpaces() {
|
||||||
|
val line = "Dialogue: 0,0:00:01.00,0:00:02.00,Default,Speaker,0,0,0,,First\\Nsecond\\nthird\\hword"
|
||||||
|
|
||||||
|
val parsed = reader.transform(reader.deserialize(line)!!)
|
||||||
|
|
||||||
|
assertEquals("First\r\nsecond\r\nthird\u00a0word", parsed.text)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun stripsKaraokeAndAnimatedOverridesWithoutLosingLyricText() {
|
||||||
|
val line = "Dialogue: 0,0:00:01.00,0:00:02.00,Karaoke,Singer,0,0,0,,{\\k20}La{\\ko15\\t(0,100,\\fscx110)}la"
|
||||||
|
|
||||||
|
val parsed = reader.transform(reader.deserialize(line)!!)
|
||||||
|
|
||||||
|
assertEquals("Lala", parsed.text)
|
||||||
|
assertEquals(DialogType.DIALOG_NORMAL, parsed.type)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun vectorDrawingEventsAreIgnoredAsNonDialogue() {
|
||||||
|
val line = "Dialogue: 0,0:00:01.00,0:00:02.00,Sign,Sign,0,0,0,,{\\p1}m 0 0 l 50 0 l 50 88 l 0 88{\\p0}"
|
||||||
|
|
||||||
|
val parsed = reader.transform(reader.deserialize(line)!!)
|
||||||
|
|
||||||
|
assertTrue(parsed.ignore)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
package no.iktdev.library.subtitle.reader
|
||||||
|
|
||||||
|
import no.iktdev.library.subtitle.SubtitleFixtures
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.jupiter.api.Assertions.*
|
||||||
|
import org.junit.jupiter.api.assertDoesNotThrow
|
||||||
|
|
||||||
|
class SrtReaderTest {
|
||||||
|
private val reader = SrtReader(null)
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun groupTest() {
|
||||||
|
val grouped: MutableList<List<String>> = mutableListOf()
|
||||||
|
val cache = mutableListOf<String>()
|
||||||
|
|
||||||
|
val lines = SubtitleFixtures.srt
|
||||||
|
lines.forEach {
|
||||||
|
if (it.isNotEmpty() && it.isNotBlank())
|
||||||
|
cache.add(it)
|
||||||
|
|
||||||
|
if (it.isEmpty() || it.isBlank() || lines.last() == it) {
|
||||||
|
grouped.add(cache.toList())
|
||||||
|
cache.clear()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
assertTrue(grouped.isNotEmpty())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testSyntheticSrt_Read() {
|
||||||
|
val lines = SubtitleFixtures.srt
|
||||||
|
val srts = reader.group(lines).mapNotNull { reader.deserialize(it) }
|
||||||
|
val mapped = srts.map { reader.transform(it) }
|
||||||
|
assertEquals(3, mapped.size)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
Reference in new issue
Block a user