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