commit 2f80222293b3fcc1eef309f982628d7afa119557 Author: Brage Date: Sun Jul 16 14:48:26 2023 +0200 Init diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..4ac4a0aa --- /dev/null +++ b/.gitignore @@ -0,0 +1,44 @@ +.gradle +build/ +!gradle/wrapper/gradle-wrapper.jar +!**/src/main/**/build/ +!**/src/test/**/build/ + +### IntelliJ IDEA ### +.idea +**/.idea/* +.idea/modules.xml +.idea/jarRepositories.xml +.idea/compiler.xml +.idea/libraries/ +*.iws +*.iml +*.ipr +out/ +!**/src/main/**/out/ +!**/src/test/**/out/ + +### Eclipse ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache +bin/ +!**/src/main/**/bin/ +!**/src/test/**/bin/ + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ + +### VS Code ### +.vscode/ + +### Mac OS ### +.DS_Store \ No newline at end of file diff --git a/CommonCode/.gitignore b/CommonCode/.gitignore new file mode 100644 index 00000000..b63da455 --- /dev/null +++ b/CommonCode/.gitignore @@ -0,0 +1,42 @@ +.gradle +build/ +!gradle/wrapper/gradle-wrapper.jar +!**/src/main/**/build/ +!**/src/test/**/build/ + +### IntelliJ IDEA ### +.idea/modules.xml +.idea/jarRepositories.xml +.idea/compiler.xml +.idea/libraries/ +*.iws +*.iml +*.ipr +out/ +!**/src/main/**/out/ +!**/src/test/**/out/ + +### Eclipse ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache +bin/ +!**/src/main/**/bin/ +!**/src/test/**/bin/ + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ + +### VS Code ### +.vscode/ + +### Mac OS ### +.DS_Store \ No newline at end of file diff --git a/CommonCode/build.gradle.kts b/CommonCode/build.gradle.kts new file mode 100644 index 00000000..b9574453 --- /dev/null +++ b/CommonCode/build.gradle.kts @@ -0,0 +1,26 @@ +plugins { + kotlin("jvm") version "1.8.21" +} + +group = "no.iktdev.streamit.content" +version = "1.0-SNAPSHOT" + +repositories { + mavenCentral() +} + +dependencies { + implementation("com.github.pgreze:kotlin-process:1.3.1") + implementation("io.github.microutils:kotlin-logging-jvm:2.0.11") + + + testImplementation("junit:junit:4.13.2") + testImplementation("org.junit.jupiter:junit-jupiter") + testImplementation("org.junit.jupiter:junit-jupiter-api:5.8.1") + testImplementation("org.junit.jupiter:junit-jupiter-params:5.8.1") + testImplementation("org.assertj:assertj-core:3.4.1") +} + +tasks.test { + useJUnitPlatform() +} \ No newline at end of file diff --git a/CommonCode/gradle/wrapper/gradle-wrapper.jar b/CommonCode/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 00000000..249e5832 Binary files /dev/null and b/CommonCode/gradle/wrapper/gradle-wrapper.jar differ diff --git a/CommonCode/gradle/wrapper/gradle-wrapper.properties b/CommonCode/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 00000000..dfb7e10f --- /dev/null +++ b/CommonCode/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Sat Jul 15 17:55:49 CEST 2023 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.0-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/CommonCode/gradlew b/CommonCode/gradlew new file mode 100644 index 00000000..1b6c7873 --- /dev/null +++ b/CommonCode/gradlew @@ -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" "$@" diff --git a/CommonCode/gradlew.bat b/CommonCode/gradlew.bat new file mode 100644 index 00000000..107acd32 --- /dev/null +++ b/CommonCode/gradlew.bat @@ -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 diff --git a/CommonCode/settings.gradle.kts b/CommonCode/settings.gradle.kts new file mode 100644 index 00000000..47b4e959 --- /dev/null +++ b/CommonCode/settings.gradle.kts @@ -0,0 +1,2 @@ +rootProject.name = "CommonCode" + diff --git a/CommonCode/src/main/java/no/iktdev/streamit/content/common/CommonConfig.kt b/CommonCode/src/main/java/no/iktdev/streamit/content/common/CommonConfig.kt new file mode 100644 index 00000000..697fb7b7 --- /dev/null +++ b/CommonCode/src/main/java/no/iktdev/streamit/content/common/CommonConfig.kt @@ -0,0 +1,9 @@ +package no.iktdev.streamit.content.common + +import java.io.File + +object CommonConfig { + var kafkaConsumerId: String = System.getenv("KAFKA_TOPIC") ?: "contentEvents" + var incomingContent: File? = if (!System.getenv("DIRECTORY_CONTENT_INCOMING").isNullOrEmpty()) File(System.getenv("DIRECTORY_CONTENT_INCOMING")) else null + +} \ No newline at end of file diff --git a/CommonCode/src/main/java/no/iktdev/streamit/content/common/FileAccess.kt b/CommonCode/src/main/java/no/iktdev/streamit/content/common/FileAccess.kt new file mode 100644 index 00000000..0ed74b82 --- /dev/null +++ b/CommonCode/src/main/java/no/iktdev/streamit/content/common/FileAccess.kt @@ -0,0 +1,25 @@ +package no.iktdev.streamit.content.common + +import mu.KotlinLogging +import java.io.File +import java.io.RandomAccessFile + +private val logger = KotlinLogging.logger {} +class FileAccess { + companion object { + fun isFileAvailable(file: File): Boolean { + if (!file.exists()) return false + var stream: RandomAccessFile? = null + try { + stream = RandomAccessFile(file, "rw") + stream.close() + logger.info { "File ${file.name} is read and writable" } + return true + } catch (e: Exception) { + stream?.close() + } + return false + } + } + +} \ No newline at end of file diff --git a/CommonCode/src/main/java/no/iktdev/streamit/content/common/Naming.kt b/CommonCode/src/main/java/no/iktdev/streamit/content/common/Naming.kt new file mode 100644 index 00000000..e8ca560e --- /dev/null +++ b/CommonCode/src/main/java/no/iktdev/streamit/content/common/Naming.kt @@ -0,0 +1,112 @@ +package no.iktdev.streamit.content.common + +class Naming(val fileName: String) { + var cleanedFileName: String + private set + + init { + cleanedFileName = fileName.apply { + removeBracketedText(this) + removeParenthesizedText(this) + removeResolutionAndTags(this) + removeInBetweenCharacters(this) + + removeExtraWhiteSpace(this) + } + } + + fun guessDesiredFileName(): String { + val parts = fileName.split(" - ") + return when { + parts.size == 2 && parts[1].matches(Regex("\\d{4}")) -> { + val title = parts[0] + val year = parts[1] + "$title ($year)" + } + + parts.size >= 3 && parts[1].matches(Regex("S\\d+")) && parts[2].matches(Regex("\\d+[vV]\\d+")) -> { + val title = parts[0] + val episodeWithRevision = parts[2] + val episodeParts = episodeWithRevision.split("v", "V") + val episodeNumber = episodeParts[0].toInt() + val revisionNumber = episodeParts[1].toInt() + val seasonEpisode = + "S${episodeNumber.toString().padStart(2, '0')}E${revisionNumber.toString().padStart(2, '0')}" + val episodeTitle = if (parts.size > 3) parts[3] else "" + "$title - $seasonEpisode - $episodeTitle" + } + + else -> fileName + } + } + + fun guessDesiredTitle(): String { + val desiredFileName = guessDesiredFileName() + return if (desiredFileName.contains(" - ")) { + return desiredFileName.split(" - ").firstOrNull() ?: desiredFileName + } else desiredFileName + } + + + /** + * Checks whether the filename contains the keyword movie, if so, default to movie + */ + fun doesContainMovieKeywords(): Boolean { + return getMatch("[(](?<=\\()movie(?=\\))[)]")?.isBlank() ?: false + } + + /** + * @return not null if matches "S01E01" + */ + fun isSeasonEpisodeDefined(): String? { + return getMatch("(?i)S[0-9]+E[0-9]+(?i)") + } + + /** + * @return not null if matches " 2020 " or ".2020." + */ + fun isDefinedWithYear(): String? { + return getMatch("[ .][0-9]{4}[ .]") + } + + + /** + * Modifies the input value and removes "[Text]" + * @param text "[TEST] Dummy - 01 [AZ 1080p] " + */ + fun removeBracketedText(text: String): String { + return Regex("\\[.*?]").replace(text, " ") + } + + /** + * + */ + fun removeParenthesizedText(text: String): String { + return Regex("\\(.*?\\)").replace(text, " ") + } + + /** + * + */ + fun removeResolutionAndTags(text: String): String { + return Regex("(.*?)(?=\\d+[pk]\\b)").replace(text, " ") + } + + fun removeInBetweenCharacters(text: String): String { + return Regex("[.]").replace(text, " ") + } + + /** + * @param text "example text with extra spaces" + * @return example text with extra spaces + */ + fun removeExtraWhiteSpace(text: String): String { + return Regex("\\s{2,}").replace(text, " ") + } + + + private fun getMatch(regex: String): String? { + return Regex(regex).find(fileName)?.value ?: return null + } + +} \ No newline at end of file diff --git a/CommonCode/src/main/java/no/iktdev/streamit/content/common/deamon/Daemon.kt b/CommonCode/src/main/java/no/iktdev/streamit/content/common/deamon/Daemon.kt new file mode 100644 index 00000000..f8ae4d8f --- /dev/null +++ b/CommonCode/src/main/java/no/iktdev/streamit/content/common/deamon/Daemon.kt @@ -0,0 +1,26 @@ +package no.iktdev.streamit.content.common.deamon + +import com.github.pgreze.process.ProcessResult +import com.github.pgreze.process.Redirect +import com.github.pgreze.process.process +import mu.KotlinLogging + +private val logger = KotlinLogging.logger {} + +class Daemon(open val executable: String, val parameters: List, val daemonInterface: IDaemon) { + var executor: ProcessResult? = null + suspend fun run(): Int { + daemonInterface.onStarted() + executor = process(executable, *parameters.toTypedArray(), + stdout = Redirect.CAPTURE, + stderr = Redirect.CAPTURE, + consumer = { + daemonInterface.onOutputChanged(it) + }) + val resultCode = executor?.resultCode ?: -1 + if (resultCode == 0) { + daemonInterface.onEnded() + } else daemonInterface.onError() + return resultCode + } +} \ No newline at end of file diff --git a/CommonCode/src/main/java/no/iktdev/streamit/content/common/deamon/IDaemon.kt b/CommonCode/src/main/java/no/iktdev/streamit/content/common/deamon/IDaemon.kt new file mode 100644 index 00000000..fd29328b --- /dev/null +++ b/CommonCode/src/main/java/no/iktdev/streamit/content/common/deamon/IDaemon.kt @@ -0,0 +1,13 @@ +package no.iktdev.streamit.content.common.deamon + +interface IDaemon { + + fun onStarted() {} + + fun onOutputChanged(line: String) {} + + fun onEnded() {} + + fun onError() + +} \ No newline at end of file diff --git a/CommonCode/src/main/java/no/iktdev/streamit/content/common/streams/MediaStreams.kt b/CommonCode/src/main/java/no/iktdev/streamit/content/common/streams/MediaStreams.kt new file mode 100644 index 00000000..4808d6ab --- /dev/null +++ b/CommonCode/src/main/java/no/iktdev/streamit/content/common/streams/MediaStreams.kt @@ -0,0 +1,184 @@ +package no.iktdev.streamit.content.common.streams + +data class MediaStreams( + val streams: List +) + +sealed class Stream( + @Transient open val index: Int, + @Transient open val codec_name: String, + @Transient open val codec_long_name: String, + @Transient open val codec_type: String, + @Transient open val codec_tag_string: String, + @Transient open val codec_tag: String, + @Transient open val r_frame_rate: String, + @Transient open val avg_frame_rate: String, + @Transient open val time_base: String, + @Transient open val start_pts: Long, + @Transient open val start_time: String, + @Transient open val duration_ts: Long? = null, + @Transient open val duration: String? = null, + @Transient open val disposition: Disposition, + @Transient open val tags: Tags +) + +data class VideoStream( + override val index: Int, + override val codec_name: String, + override val codec_long_name: String, + override val codec_type: String, + override val codec_tag_string: String, + override val codec_tag: String, + override val r_frame_rate: String, + override val avg_frame_rate: String, + override val time_base: String, + override val start_pts: Long, + override val start_time: String, + override val disposition: Disposition, + override val tags: Tags, + override val duration: String?, + override val duration_ts: Long?, + val profile: String, + val width: Int, + val height: Int, + val coded_width: Int, + val coded_height: Int, + val closed_captions: Int, + val has_b_frames: Int, + val sample_aspect_ratio: String, + val display_aspect_ratio: String, + val pix_fmt: String, + val level: Int, + val color_range: String, + val color_space: String, + val color_transfer: String, + val color_primaries: String, + val chroma_location: String, + val refs: Int +) : Stream( + index, + codec_name, + codec_long_name, + codec_type, + codec_tag_string, + codec_tag, + r_frame_rate, + avg_frame_rate, + time_base, + start_pts, + start_time, + duration_ts, + duration, + disposition, + tags +) + +data class AudioStream( + override val index: Int, + override val codec_name: String, + override val codec_long_name: String, + override val codec_type: String, + override val codec_tag_string: String, + override val codec_tag: String, + override val r_frame_rate: String, + override val avg_frame_rate: String, + override val time_base: String, + override val start_pts: Long, + override val start_time: String, + override val duration: String?, + override val duration_ts: Long?, + override val disposition: Disposition, + override val tags: Tags, + val profile: String, + val sample_fmt: String, + val sample_rate: String, + val channels: Int, + val channel_layout: String, + val bits_per_sample: Int +) : Stream( + index, + codec_name, + codec_long_name, + codec_type, + codec_tag_string, + codec_tag, + r_frame_rate, + avg_frame_rate, + time_base, + start_pts, + start_time, + duration_ts, + duration, + disposition, + tags +) + +data class SubtitleStream( + override val index: Int, + override val codec_name: String, + override val codec_long_name: String, + override val codec_type: String, + override val codec_tag_string: String, + override val codec_tag: String, + override val r_frame_rate: String, + override val avg_frame_rate: String, + override val time_base: String, + override val start_pts: Long, + override val start_time: String, + override val duration: String?, + override val duration_ts: Long?, + override val disposition: Disposition, + override val tags: Tags, + val subtitle_tags: SubtitleTags +) : Stream( + index, + codec_name, + codec_long_name, + codec_type, + codec_tag_string, + codec_tag, + r_frame_rate, + avg_frame_rate, + time_base, + start_pts, + start_time, + duration_ts, + duration, + disposition, + tags +) + +data class Disposition( + val default: Int, + val dub: Int, + val original: Int, + val comment: Int, + val lyrics: Int, + val karaoke: Int, + val forced: Int, + val hearing_impaired: Int, + val visual_impaired: Int, + val clean_effects: Int, + val attached_pic: Int, + val timed_thumbnails: Int +) + +data class Tags( + val title: String?, + val BPS: String?, + val DURATION: String?, + val NUMBER_OF_FRAMES: String?, + val NUMBER_OF_BYTES: String?, + val _STATISTICS_WRITING_APP: String?, + val _STATISTICS_WRITING_DATE_UTC: String?, + val _STATISTICS_TAGS: String?, + val language: String?, + val filename: String?, + val mimetype: String? +) + +data class SubtitleTags( + val language: String?, + val filename: String?, + val mimetype: String? +) diff --git a/CommonCode/src/test/java/no/iktdev/streamit/content/common/NamingTest.kt b/CommonCode/src/test/java/no/iktdev/streamit/content/common/NamingTest.kt new file mode 100644 index 00000000..877b3553 --- /dev/null +++ b/CommonCode/src/test/java/no/iktdev/streamit/content/common/NamingTest.kt @@ -0,0 +1,41 @@ +package no.iktdev.streamit.content.common + +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Named +import org.junit.jupiter.api.Test +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.MethodSource + +class NamingTest { +/* + @ParameterizedTest + @MethodSource("serieOnlyTest") + fun ensureOnlySerieAndDecodedCorrectly(testData: TestData) { + val naming = Naming(testData.input).getName() ?: throw NullPointerException("Named is null") + assertThat(naming.type).isEqualTo("serie") + assertThat(naming.season).isEqualTo(testData.expected.season) + assertThat(naming.episode).isEqualTo(testData.expected.episode) + } + + @Test + fun testTest() { + val tmp = TestData(Naming.Name(title = "Demo", season = 1, episode = 1, type = "serie"), "[Kametsu] Ghost in the Shell Arise - 05 - Pyrophoric Cult (BD 1080p Hi10 FLAC) [13FF85A7]") + val naming = Naming(tmp.input).getName() + assertThat(naming).isNotNull() + } + + + fun serieOnlyTest(): List> { + return listOf( + Named.of("Is defined", TestData(Naming.Name(title = "Demo", season = 1, episode = 1, type = "serie"), "Demo - S01E01")), + Named.of("Is decoded", TestData(Naming.Name("Demo!", "serie", season = 1, episode = 1), "[TMP] Demo! - 03")), + Named.of("Is only Episode", TestData(Naming.Name("Demo", "serie", 1, 1), "Demo E1")) + ) + }*/ + +/* + data class TestData( + val expected: Naming.Name, + val input: String + )*/ +} \ No newline at end of file diff --git a/Encode/.gitignore b/Encode/.gitignore new file mode 100644 index 00000000..b63da455 --- /dev/null +++ b/Encode/.gitignore @@ -0,0 +1,42 @@ +.gradle +build/ +!gradle/wrapper/gradle-wrapper.jar +!**/src/main/**/build/ +!**/src/test/**/build/ + +### IntelliJ IDEA ### +.idea/modules.xml +.idea/jarRepositories.xml +.idea/compiler.xml +.idea/libraries/ +*.iws +*.iml +*.ipr +out/ +!**/src/main/**/out/ +!**/src/test/**/out/ + +### Eclipse ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache +bin/ +!**/src/main/**/bin/ +!**/src/test/**/bin/ + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ + +### VS Code ### +.vscode/ + +### Mac OS ### +.DS_Store \ No newline at end of file diff --git a/Encode/build.gradle.kts b/Encode/build.gradle.kts new file mode 100644 index 00000000..fe3ef1fd --- /dev/null +++ b/Encode/build.gradle.kts @@ -0,0 +1,19 @@ +plugins { + kotlin("jvm") version "1.8.21" +} + +group = "no.iktdev.streamit.content" +version = "1.0-SNAPSHOT" + +repositories { + mavenCentral() +} + +dependencies { + testImplementation(platform("org.junit:junit-bom:5.9.1")) + testImplementation("org.junit.jupiter:junit-jupiter") +} + +tasks.test { + useJUnitPlatform() +} \ No newline at end of file diff --git a/Encode/gradle/wrapper/gradle-wrapper.jar b/Encode/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 00000000..249e5832 Binary files /dev/null and b/Encode/gradle/wrapper/gradle-wrapper.jar differ diff --git a/Encode/gradle/wrapper/gradle-wrapper.properties b/Encode/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 00000000..ab96900a --- /dev/null +++ b/Encode/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Tue Jul 11 02:14:45 CEST 2023 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.0-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/Encode/gradlew b/Encode/gradlew new file mode 100644 index 00000000..1b6c7873 --- /dev/null +++ b/Encode/gradlew @@ -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" "$@" diff --git a/Encode/gradlew.bat b/Encode/gradlew.bat new file mode 100644 index 00000000..107acd32 --- /dev/null +++ b/Encode/gradlew.bat @@ -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 diff --git a/Encode/settings.gradle.kts b/Encode/settings.gradle.kts new file mode 100644 index 00000000..2b84cfd8 --- /dev/null +++ b/Encode/settings.gradle.kts @@ -0,0 +1,2 @@ +rootProject.name = "Encode" + diff --git a/Reader/.gitignore b/Reader/.gitignore new file mode 100644 index 00000000..b63da455 --- /dev/null +++ b/Reader/.gitignore @@ -0,0 +1,42 @@ +.gradle +build/ +!gradle/wrapper/gradle-wrapper.jar +!**/src/main/**/build/ +!**/src/test/**/build/ + +### IntelliJ IDEA ### +.idea/modules.xml +.idea/jarRepositories.xml +.idea/compiler.xml +.idea/libraries/ +*.iws +*.iml +*.ipr +out/ +!**/src/main/**/out/ +!**/src/test/**/out/ + +### Eclipse ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache +bin/ +!**/src/main/**/bin/ +!**/src/test/**/bin/ + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ + +### VS Code ### +.vscode/ + +### Mac OS ### +.DS_Store \ No newline at end of file diff --git a/Reader/build.gradle.kts b/Reader/build.gradle.kts new file mode 100644 index 00000000..a46cba15 --- /dev/null +++ b/Reader/build.gradle.kts @@ -0,0 +1,49 @@ +plugins { + kotlin("jvm") version "1.8.21" + id("org.springframework.boot") version "2.5.5" + id("io.spring.dependency-management") version "1.0.11.RELEASE" + kotlin("plugin.spring") version "1.5.31" +} + +group = "no.iktdev.streamit.content" +version = "1.0-SNAPSHOT" + +repositories { + mavenCentral() + maven("https://jitpack.io") + maven { + url = uri("https://reposilite.iktdev.no/releases") + } + maven { + url = uri("https://reposilite.iktdev.no/snapshots") + } +} + +dependencies { + implementation("no.iktdev.streamit.library:streamit-library-kafka:0.0.2-alpha10") + implementation("no.iktdev:exfl:0.0.4-SNAPSHOT") + + implementation("com.github.pgreze:kotlin-process:1.3.1") + implementation("com.github.vishna:watchservice-ktx:master-SNAPSHOT") + implementation("io.github.microutils:kotlin-logging-jvm:2.0.11") + + implementation("com.google.code.gson:gson:2.8.9") + implementation("org.json:json:20210307") + + implementation("org.springframework.boot:spring-boot-starter-web") + implementation("org.springframework.boot:spring-boot-starter:2.7.0") + implementation("org.springframework.kafka:spring-kafka:2.8.5") + + implementation(project(":CommonCode")) + testImplementation("junit:junit:4.13.2") + testImplementation("org.junit.jupiter:junit-jupiter") + testImplementation("org.junit.jupiter:junit-jupiter-api:5.8.1") + testImplementation("org.junit.jupiter:junit-jupiter-params:5.8.1") + testImplementation("org.assertj:assertj-core:3.4.1") + + +} + +tasks.test { + useJUnitPlatform() +} \ No newline at end of file diff --git a/Reader/gradle/wrapper/gradle-wrapper.jar b/Reader/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 00000000..249e5832 Binary files /dev/null and b/Reader/gradle/wrapper/gradle-wrapper.jar differ diff --git a/Reader/gradle/wrapper/gradle-wrapper.properties b/Reader/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 00000000..4db8978e --- /dev/null +++ b/Reader/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Tue Jul 11 02:16:45 CEST 2023 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.0-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/Reader/gradlew b/Reader/gradlew new file mode 100644 index 00000000..1b6c7873 --- /dev/null +++ b/Reader/gradlew @@ -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" "$@" diff --git a/Reader/gradlew.bat b/Reader/gradlew.bat new file mode 100644 index 00000000..107acd32 --- /dev/null +++ b/Reader/gradlew.bat @@ -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 diff --git a/Reader/settings.gradle.kts b/Reader/settings.gradle.kts new file mode 100644 index 00000000..db9bf9a3 --- /dev/null +++ b/Reader/settings.gradle.kts @@ -0,0 +1,5 @@ +rootProject.name = "Reader" + +include(":CommonCode") +project(":CommonCode").projectDir = File("../CommonCode") + diff --git a/Reader/src/main/kotlin/no/iktdev/streamit/content/reader/ReaderApplication.kt b/Reader/src/main/kotlin/no/iktdev/streamit/content/reader/ReaderApplication.kt new file mode 100644 index 00000000..930fe04d --- /dev/null +++ b/Reader/src/main/kotlin/no/iktdev/streamit/content/reader/ReaderApplication.kt @@ -0,0 +1,16 @@ +package no.iktdev.streamit.content.reader + +import no.iktdev.streamit.content.reader.analyzer.PreferenceReader +import org.springframework.boot.autoconfigure.SpringBootApplication + +@SpringBootApplication +class ReaderApplication + +val preference = PreferenceReader().getPreference() + +fun main(array: Array) { + + + +} + diff --git a/Reader/src/main/kotlin/no/iktdev/streamit/content/reader/ReaderEnv.kt b/Reader/src/main/kotlin/no/iktdev/streamit/content/reader/ReaderEnv.kt new file mode 100644 index 00000000..3899e47d --- /dev/null +++ b/Reader/src/main/kotlin/no/iktdev/streamit/content/reader/ReaderEnv.kt @@ -0,0 +1,10 @@ +package no.iktdev.streamit.content.reader + +import java.io.File + +class ReaderEnv { + companion object { + val ffprobe: String = System.getenv("SUPPORTING_EXECUTABLE_FFPROBE") ?: "ffprobe" + val encodePreference: String? = System.getenv("ENCODE_PREFERENCE") ?: null + } +} \ No newline at end of file diff --git a/Reader/src/main/kotlin/no/iktdev/streamit/content/reader/analyzer/EncodeArgumentSelector.kt b/Reader/src/main/kotlin/no/iktdev/streamit/content/reader/analyzer/EncodeArgumentSelector.kt new file mode 100644 index 00000000..94f2ca12 --- /dev/null +++ b/Reader/src/main/kotlin/no/iktdev/streamit/content/reader/analyzer/EncodeArgumentSelector.kt @@ -0,0 +1,72 @@ +package no.iktdev.streamit.content.reader.analyzer + +import no.iktdev.streamit.content.common.streams.AudioStream +import no.iktdev.streamit.content.common.streams.MediaStreams +import no.iktdev.streamit.content.common.streams.SubtitleStream +import no.iktdev.streamit.content.common.streams.VideoStream +import no.iktdev.streamit.content.reader.analyzer.encoding.AudioEncodeArguments +import no.iktdev.streamit.content.reader.analyzer.encoding.EncodeInformation +import no.iktdev.streamit.content.reader.analyzer.encoding.SubtitleEncodeArguments +import no.iktdev.streamit.content.reader.analyzer.encoding.VideoEncodeArguments +import no.iktdev.streamit.content.reader.preference + +class EncodeArgumentSelector(val inputFile: String, val streams: MediaStreams, val outFileName: String) { + var defaultSelectedVideo: VideoStream? = getDefaultSelectedVideo() + var defaultSelectedAudio: AudioStream? = getDefaultSelectedAudio() + + private fun getAudioStreams() = streams.streams.filterIsInstance() + private fun getVideoStreams() = streams.streams.filterIsInstance() + + + private fun getDefaultSelectedVideo(): VideoStream? { + return getVideoStreams().filter { (it.duration_ts ?: 0) > 0 }.maxByOrNull { it.duration_ts!! } ?: getVideoStreams().minByOrNull { it.index } + } + + private fun getDefaultSelectedAudio(): AudioStream? { + return getAudioStreams().filter { (it.duration_ts ?: 0) > 0 }.maxByOrNull { it.duration_ts!! } ?: getAudioStreams().minByOrNull { it.index } + } + + /** + * @return VideoStream based on preference or defaultSelectedVideo + */ + /*private fun getSelectedVideoBasedOnPreference(): VideoStream { + val + }*/ + + /** + * @return AudioStrem based on preference or defaultSelectedAudio + */ + private fun getSelectedAudioBasedOnPreference(): AudioStream? { + val languageFiltered = getAudioStreams().filter { it.tags.language == preference.audio.language } + val channeledAndCodec = languageFiltered.find { it.channels >= (preference.audio.channels ?: 2) && it.codec_name == preference.audio.codec.lowercase() } + return channeledAndCodec ?: return languageFiltered.minByOrNull { it.index } ?: defaultSelectedAudio + } + + + fun getVideoAndAudioArguments(): EncodeInformation? { + val selectedVideo = defaultSelectedVideo + val selectedAudio = getSelectedAudioBasedOnPreference() ?: defaultSelectedAudio + return if (selectedVideo == null || selectedAudio == null) return null + else { + EncodeInformation( + inputFile = inputFile, + outFileName = "$outFileName.mp4", + language = selectedAudio.tags.language ?: "eng", + arguments = VideoEncodeArguments(selectedVideo).getVideoArguments() + + AudioEncodeArguments(selectedAudio).getAudioArguments() + ) + } + } + + fun getSubtitleArguments(): List { + return streams.streams.filterIsInstance().map { + val subArgs = SubtitleEncodeArguments(it) + EncodeInformation( + inputFile = inputFile, + outFileName = "$outFileName.${subArgs.getFormatToCodec()}", + language = it.tags.language ?: "eng", + arguments = subArgs.getSubtitleArguments() + ) + } + } +} \ No newline at end of file diff --git a/Reader/src/main/kotlin/no/iktdev/streamit/content/reader/analyzer/EncodeStreamsMessageParser.kt b/Reader/src/main/kotlin/no/iktdev/streamit/content/reader/analyzer/EncodeStreamsMessageParser.kt new file mode 100644 index 00000000..3a45303b --- /dev/null +++ b/Reader/src/main/kotlin/no/iktdev/streamit/content/reader/analyzer/EncodeStreamsMessageParser.kt @@ -0,0 +1,53 @@ +package no.iktdev.streamit.content.reader.analyzer + +import com.google.gson.Gson +import com.google.gson.JsonObject +import com.google.gson.reflect.TypeToken +import no.iktdev.streamit.content.common.streams.* +import no.iktdev.streamit.content.reader.fileWatcher.FileWatcher +import no.iktdev.streamit.library.kafka.KnownEvents +import no.iktdev.streamit.library.kafka.Message +import no.iktdev.streamit.library.kafka.StatusType +import org.apache.kafka.clients.consumer.ConsumerRecord +import java.io.File + +class EncodeStreamsMessageParser { + fun getFileNameFromEvent(records: MutableList>): FileWatcher.FileResult? { + val file = records.find { it.key() == KnownEvents.EVENT_READER_RECEIVED_FILE.event } ?: return null + if (file.value().status.statusType != StatusType.SUCCESS) return null + return if (file.value().data is String) { + return Gson().fromJson(file.value().data as String, FileWatcher.FileResult::class.java) + } else null + } + + fun getMediaStreamsFromEvent(records: MutableList>): MediaStreams? { + val streams = records.find { it.key() == KnownEvents.EVENT_READER_RECEIVED_STREAMS.event } ?: return null + if (streams.value().status.statusType != StatusType.SUCCESS || streams.value().data !is String) return null + val json = streams.value().data as String + val gson = Gson() + /*return gson.fromJson(streams.value().data as String, MediaStreams::class.java)*/ + + val jsonObject = gson.fromJson(json, JsonObject::class.java) + + val streamsJsonArray = jsonObject.getAsJsonArray("streams") + + val rstreams = streamsJsonArray.mapNotNull { streamJson -> + val streamObject = streamJson.asJsonObject + + val codecType = streamObject.get("codec_type").asString + if (streamObject.get("codec_name").asString == "mjpeg") { + null + } else { + when (codecType) { + "video" -> gson.fromJson(streamObject, VideoStream::class.java) + "audio" -> gson.fromJson(streamObject, AudioStream::class.java) + "subtitle" -> gson.fromJson(streamObject, SubtitleStream::class.java) + else -> null //throw IllegalArgumentException("Unknown stream type: $codecType") + } + } + } + + return MediaStreams(rstreams) + } + +} \ No newline at end of file diff --git a/Reader/src/main/kotlin/no/iktdev/streamit/content/reader/analyzer/EncodeStreamsProducer.kt b/Reader/src/main/kotlin/no/iktdev/streamit/content/reader/analyzer/EncodeStreamsProducer.kt new file mode 100644 index 00000000..c74d92f8 --- /dev/null +++ b/Reader/src/main/kotlin/no/iktdev/streamit/content/reader/analyzer/EncodeStreamsProducer.kt @@ -0,0 +1,89 @@ +package no.iktdev.streamit.content.reader.analyzer + +import com.google.gson.Gson +import no.iktdev.streamit.content.common.CommonConfig +import no.iktdev.streamit.content.common.streams.MediaStreams +import no.iktdev.streamit.content.reader.analyzer.encoding.EncodeInformation +import no.iktdev.streamit.content.reader.fileWatcher.FileWatcher +import no.iktdev.streamit.library.kafka.KnownEvents +import no.iktdev.streamit.library.kafka.Message +import no.iktdev.streamit.library.kafka.Status +import no.iktdev.streamit.library.kafka.StatusType +import no.iktdev.streamit.library.kafka.consumers.DefaultConsumer +import no.iktdev.streamit.library.kafka.listener.pooled.IPooledEvents +import no.iktdev.streamit.library.kafka.listener.pooled.PooledEventMessageListener +import no.iktdev.streamit.library.kafka.producer.DefaultProducer +import org.apache.kafka.clients.consumer.ConsumerRecord +import org.springframework.stereotype.Service +import java.io.File + +@Service +class EncodeStreamsProducer: IPooledEvents.OnEventsReceived { + + val messageProducer = DefaultProducer(CommonConfig.kafkaConsumerId) + + val defaultConsumer = DefaultConsumer().apply { + autoCommit = false + } + + init { + val ackListener = PooledEventMessageListener( + topic = CommonConfig.kafkaConsumerId, consumer = defaultConsumer, + mainFilter = KnownEvents.EVENT_READER_RECEIVED_FILE.event, + subFilter = listOf(KnownEvents.EVENT_READER_RECEIVED_STREAMS.event), + event = this + ) + ackListener.listen() + } + + override fun areAllMessagesReceived(recordedEvents: MutableMap): Boolean { + val expected = listOf(KnownEvents.EVENT_READER_RECEIVED_FILE.event, KnownEvents.EVENT_READER_RECEIVED_STREAMS.event) + return expected.containsAll(recordedEvents.keys) + } + + private fun produceErrorMessage(referenceId: String, reason: String) { + val message = Message(referenceId = referenceId, + Status(statusType = StatusType.ERROR, errorMessage = reason) + ) + messageProducer.sendMessage(KnownEvents.EVENT_READER_ENCODE_GENERATED.event, message) + } + + private fun produceEncodeMessage(referenceId: String, data: EncodeInformation?) { + val message = Message(referenceId = referenceId, + Status(statusType = if (data != null) StatusType.SUCCESS else StatusType.IGNORED), + data = data + ) + messageProducer.sendMessage(KnownEvents.EVENT_READER_ENCODE_GENERATED.event, message) + } + + override fun onAllEventsConsumed(referenceId: String, records: MutableList>) { + val parser = EncodeStreamsMessageParser() + val fileResult = parser.getFileNameFromEvent(records) + if (fileResult == null) { + produceErrorMessage(referenceId, "FileResult is either null or not deserializable!") + return + } + val outFileName = fileResult.desiredNewName.ifBlank { File(fileResult.file).nameWithoutExtension } + val streams = parser.getMediaStreamsFromEvent(records) + if (streams == null) { + produceErrorMessage(referenceId, "No streams received!") + return + } + + val encodeInformation = EncodeArgumentSelector(inputFile = fileResult.file, streams = streams, outFileName = outFileName) + produceEncodeMessage(referenceId, encodeInformation.getVideoAndAudioArguments()) + encodeInformation.getSubtitleArguments().forEach { s -> + produceEncodeMessage(referenceId, s) + } + + + + + } + + + + + + +} \ No newline at end of file diff --git a/Reader/src/main/kotlin/no/iktdev/streamit/content/reader/analyzer/EncodingPreference.kt b/Reader/src/main/kotlin/no/iktdev/streamit/content/reader/analyzer/EncodingPreference.kt new file mode 100644 index 00000000..f6156e78 --- /dev/null +++ b/Reader/src/main/kotlin/no/iktdev/streamit/content/reader/analyzer/EncodingPreference.kt @@ -0,0 +1,86 @@ +package no.iktdev.streamit.content.reader.analyzer + +import com.google.gson.Gson +import no.iktdev.streamit.content.reader.ReaderEnv +import org.slf4j.LoggerFactory +import java.io.File + +data class EncodingPreference( + val video: VideoPreference, + val audio: AudioPreference +) + +data class VideoPreference( + val codec: String = "h264", + val pixelFormat: String = "yuv420p", + val pixelFormatPassthrough: List = listOf("yuv420p", "yuv420p10le"), + val threshold: Int = 16 +) + +data class AudioPreference( + val codec: String = "aac", + val sample_rate: Int? = null, + val channels: Int? = null, + val language: String = "eng", //ISO3 format + val preserveChannels: Boolean = true, + val defaultToEAC3OnSurroundDetected: Boolean = true, + val forceStereo: Boolean = false +) + + +class PreferenceReader { + fun getPreference(): EncodingPreference { + val defaultPreference = EncodingPreference(video = VideoPreference(), audio = AudioPreference()) + val preferenceText = readPreference() ?: return defaultPreference + val configured = deserialize(preferenceText) + + printConfiguration("Audio", "Codec", configured?.audio?.codec, defaultPreference.audio.codec) + printConfiguration("Audio", "Language", configured?.audio?.language, defaultPreference.audio.language) + printConfiguration("Audio", "Channels", configured?.audio?.channels.toString(), defaultPreference.audio.channels.toString()) + printConfiguration("Audio", "Sample rate", configured?.audio?.sample_rate.toString(), defaultPreference.audio.sample_rate.toString()) + printConfiguration("Audio", "Override to EAC3 for surround", configured?.audio?.defaultToEAC3OnSurroundDetected.toString(), defaultPreference.audio.defaultToEAC3OnSurroundDetected.toString()) + + + printConfiguration("Video", "Codec", configured?.video?.codec, defaultPreference.video.codec) + printConfiguration("Video", "Pixel format", configured?.video?.pixelFormat, defaultPreference.video.pixelFormat) + printConfiguration("Video", "Threshold", configured?.video?.threshold.toString(), defaultPreference.video.threshold.toString()) + + + return configured ?: defaultPreference + } + + fun printConfiguration(sourceType: String, key: String, value: String?, default: String?) { + val usedValue = if (!value.isNullOrEmpty()) value else if (!default.isNullOrEmpty()) "$default (default)" else "no changes will be made" + LoggerFactory.getLogger(javaClass.simpleName).info("$sourceType: $key => $usedValue") + + } + + + fun readPreference(): String? { + val prefFile = File(ReaderEnv.encodePreference) + if (!prefFile.exists()) { + LoggerFactory.getLogger(javaClass.simpleName).info("Preference file: ${prefFile.absolutePath} does not exists...") + LoggerFactory.getLogger(javaClass.simpleName).info("Using default configuration") + return null + } + else { + LoggerFactory.getLogger(javaClass.simpleName).info("Preference file: ${prefFile.absolutePath} found") + } + + try { + val instr = prefFile.inputStream() + return instr.bufferedReader().use { it.readText() } + } + catch (e: Exception) { + LoggerFactory.getLogger(javaClass.simpleName).error("Failed to read preference file: ${prefFile.absolutePath}.. Will use default configuration") + } + return null + } + + fun deserialize(value: String?): EncodingPreference? { + value ?: return null + return Gson().fromJson(value, EncodingPreference::class.java) ?: null + } + + +} \ No newline at end of file diff --git a/Reader/src/main/kotlin/no/iktdev/streamit/content/reader/analyzer/encoding/AudioEncodeArguments.kt b/Reader/src/main/kotlin/no/iktdev/streamit/content/reader/analyzer/encoding/AudioEncodeArguments.kt new file mode 100644 index 00000000..fff4b7d7 --- /dev/null +++ b/Reader/src/main/kotlin/no/iktdev/streamit/content/reader/analyzer/encoding/AudioEncodeArguments.kt @@ -0,0 +1,24 @@ +package no.iktdev.streamit.content.reader.analyzer.encoding + +import no.iktdev.streamit.content.common.streams.AudioStream +import no.iktdev.streamit.content.reader.preference + +class AudioEncodeArguments(val audio: AudioStream) { + + fun isAudioCodecEqual() = audio.codec_name.lowercase() == preference.audio.codec.lowercase() + + fun shouldUseEAC3(): Boolean { + return (preference.audio.defaultToEAC3OnSurroundDetected && audio.channels > 2 && audio.codec_name.lowercase() != "eac3") + } + + fun getAudioArguments(): MutableList { + val result = mutableListOf() + if (shouldUseEAC3()) { + result.addAll(listOf("-c:a", "eac3")) + } else if (!isAudioCodecEqual()) { + result.addAll(listOf("-c:a", preference.audio.codec)) + } else result.addAll(listOf("-acodec", "copy")) + result.addAll(listOf("-map", "0:a:${audio.index}")) + return result + } +} \ No newline at end of file diff --git a/Reader/src/main/kotlin/no/iktdev/streamit/content/reader/analyzer/encoding/EncodeInformation.kt b/Reader/src/main/kotlin/no/iktdev/streamit/content/reader/analyzer/encoding/EncodeInformation.kt new file mode 100644 index 00000000..9aab2ee0 --- /dev/null +++ b/Reader/src/main/kotlin/no/iktdev/streamit/content/reader/analyzer/encoding/EncodeInformation.kt @@ -0,0 +1,8 @@ +package no.iktdev.streamit.content.reader.analyzer.encoding + +data class EncodeInformation( + val inputFile: String, + val outFileName: String, + val language: String, + val arguments: List +) diff --git a/Reader/src/main/kotlin/no/iktdev/streamit/content/reader/analyzer/encoding/SubtitleEncodeArguments.kt b/Reader/src/main/kotlin/no/iktdev/streamit/content/reader/analyzer/encoding/SubtitleEncodeArguments.kt new file mode 100644 index 00000000..5bea26df --- /dev/null +++ b/Reader/src/main/kotlin/no/iktdev/streamit/content/reader/analyzer/encoding/SubtitleEncodeArguments.kt @@ -0,0 +1,25 @@ +package no.iktdev.streamit.content.reader.analyzer.encoding + +import no.iktdev.streamit.content.common.streams.SubtitleStream + +class SubtitleEncodeArguments(val subtitle: SubtitleStream) { + + fun getSubtitleArguments(): List { + val result = mutableListOf() + result.addAll(listOf("-c:s", "copy")) + result.addAll(listOf("-map", "0:s:${subtitle.index}")) + return result + } + + fun getFormatToCodec(): String? { + return when(subtitle.codec_name) { + "ass" -> "ass" + "subrip" -> "srt" + "webvtt", "vtt" -> "vtt" + "smi" -> "smi" + "hdmv_pgs_subtitle" -> null + else -> null + } + } + +} \ No newline at end of file diff --git a/Reader/src/main/kotlin/no/iktdev/streamit/content/reader/analyzer/encoding/VideoEncodeArguments.kt b/Reader/src/main/kotlin/no/iktdev/streamit/content/reader/analyzer/encoding/VideoEncodeArguments.kt new file mode 100644 index 00000000..cffc35a1 --- /dev/null +++ b/Reader/src/main/kotlin/no/iktdev/streamit/content/reader/analyzer/encoding/VideoEncodeArguments.kt @@ -0,0 +1,40 @@ +package no.iktdev.streamit.content.reader.analyzer.encoding + +import no.iktdev.streamit.content.common.streams.AudioStream +import no.iktdev.streamit.content.common.streams.VideoStream +import no.iktdev.streamit.content.reader.preference + +class VideoEncodeArguments(val video: VideoStream) { + + fun isVideoCodecEqual() = video.codec_name == getCorrectCodec() + + + fun getVideoArguments(): List { + val result = mutableListOf() + if (isVideoCodecEqual()) result.addAll(listOf( + "-vcodec", "copy" + )) else { + result.addAll(listOf("-c:v", getCorrectCodec())) + result.addAll(listOf("-crf", preference.video.threshold.toString())) + } + if (preference.video.pixelFormatPassthrough.none { it == video.pix_fmt }) { + result.addAll(listOf("-pix_fmt", preference.video.pixelFormat)) + } + result.addAll(listOf("-map", "0:v:${video.index}")) + return result + } + + + protected fun getCorrectCodec(): String { + return when(preference.video.codec.lowercase()) { + "hevc" -> "libx265" + "h265" -> "libx265" + "h.265" -> "libx265" + + "h.264" -> "libx264" + "h264" -> "libx264" + + else -> preference.video.codec.lowercase() + } + } +} \ No newline at end of file diff --git a/Reader/src/main/kotlin/no/iktdev/streamit/content/reader/fileWatcher/FileWatcher.kt b/Reader/src/main/kotlin/no/iktdev/streamit/content/reader/fileWatcher/FileWatcher.kt new file mode 100644 index 00000000..873430b4 --- /dev/null +++ b/Reader/src/main/kotlin/no/iktdev/streamit/content/reader/fileWatcher/FileWatcher.kt @@ -0,0 +1,91 @@ +package no.iktdev.streamit.content.reader.fileWatcher + +import dev.vishna.watchservice.KWatchEvent +import dev.vishna.watchservice.asWatchChannel +import kotlinx.coroutines.channels.consumeEach +import kotlinx.coroutines.launch +import mu.KotlinLogging +import no.iktdev.exfl.coroutines.Coroutines +import no.iktdev.streamit.content.common.CommonConfig +import no.iktdev.streamit.content.common.Naming + +import no.iktdev.streamit.content.reader.ReaderEnv +import no.iktdev.streamit.library.kafka.KnownEvents +import no.iktdev.streamit.library.kafka.Message +import no.iktdev.streamit.library.kafka.Status +import no.iktdev.streamit.library.kafka.StatusType +import no.iktdev.streamit.library.kafka.producer.DefaultProducer +import org.springframework.stereotype.Service + +private val logger = KotlinLogging.logger {} +@Service +class FileWatcher: FileWatcherEvents { + val messageProducer = DefaultProducer(CommonConfig.kafkaConsumerId) + + val queue = FileWatcherQueue() + + + val watcherChannel = CommonConfig.incomingContent?.asWatchChannel() + init { + Coroutines.io().launch { + if (watcherChannel == null) { + logger.error { "Can't start watcherChannel on null!" } + } + watcherChannel?.consumeEach { + when (it.kind) { + KWatchEvent.Kind.Deleted -> { + queue.removeFromQueue(it.file, this@FileWatcher::onFileRemoved) + } + KWatchEvent.Kind.Created, KWatchEvent.Kind.Initialized -> { + queue.addToQueue(it.file, this@FileWatcher::onFilePending, this@FileWatcher::onFileAvailable) + } + else -> { + logger.info { "Ignoring event kind: ${it.kind.name} for file ${it.file.name}" } + } + } + } + } + } + + + override fun onFileAvailable(file: PendingFile) { + val naming = Naming(file.file.nameWithoutExtension) + val message = Message( + referenceId = file.id, + status = Status(StatusType.SUCCESS), + data = FileResult(file = file.file.absolutePath, title = naming.guessDesiredTitle(), desiredNewName = naming.guessDesiredFileName()) + ) + messageProducer.sendMessage(KnownEvents.EVENT_READER_RECEIVED_FILE.event, message) + } + + override fun onFilePending(file: PendingFile) { + val message = Message( + status = Status(StatusType.PENDING), + data = FileResult(file = file.file.absolutePath) + ) + messageProducer.sendMessage(KnownEvents.EVENT_READER_RECEIVED_FILE.event , message) + } + + override fun onFileFailed(file: PendingFile) { + val message = Message( + status = Status(StatusType.ERROR), + data = file.file.absolutePath + ) + messageProducer.sendMessage(KnownEvents.EVENT_READER_RECEIVED_FILE.event , message) + } + + override fun onFileRemoved(file: PendingFile) { + val message = Message( + status = Status(StatusType.IGNORED), + data = file.file.absolutePath + ) + messageProducer.sendMessage(KnownEvents.EVENT_READER_RECEIVED_FILE.event , message) + } + + data class FileResult( + val file: String, + val title: String = "", + val desiredNewName: String = "" + ) + +} \ No newline at end of file diff --git a/Reader/src/main/kotlin/no/iktdev/streamit/content/reader/fileWatcher/FileWatcherEvents.kt b/Reader/src/main/kotlin/no/iktdev/streamit/content/reader/fileWatcher/FileWatcherEvents.kt new file mode 100644 index 00000000..e65cae2c --- /dev/null +++ b/Reader/src/main/kotlin/no/iktdev/streamit/content/reader/fileWatcher/FileWatcherEvents.kt @@ -0,0 +1,20 @@ +package no.iktdev.streamit.content.reader.fileWatcher + +import java.io.File + +interface FileWatcherEvents { + fun onFileAvailable(file: PendingFile) + + /** + * If the file is being copied or incomplete, or in case a process currently owns the file, pending should be issued + */ + fun onFilePending(file: PendingFile) + + /** + * If the file is either removed or is not a valid file + */ + fun onFileFailed(file: PendingFile) + + + fun onFileRemoved(file: PendingFile) +} \ No newline at end of file diff --git a/Reader/src/main/kotlin/no/iktdev/streamit/content/reader/fileWatcher/FileWatcherQueue.kt b/Reader/src/main/kotlin/no/iktdev/streamit/content/reader/fileWatcher/FileWatcherQueue.kt new file mode 100644 index 00000000..83ba6775 --- /dev/null +++ b/Reader/src/main/kotlin/no/iktdev/streamit/content/reader/fileWatcher/FileWatcherQueue.kt @@ -0,0 +1,67 @@ +package no.iktdev.streamit.content.reader.fileWatcher + +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import no.iktdev.exfl.coroutines.Coroutines +import no.iktdev.streamit.content.common.FileAccess +import java.io.File +import java.util.UUID + +data class PendingFile(val id: String = UUID.randomUUID().toString(), val file: File, var time: Long = 0) +class FileWatcherQueue { + private val fileChannel = Channel() + + fun addToQueue(file: File, onFilePending: (PendingFile) -> Unit, onFileAccessible: (PendingFile) -> Unit) { + // Check if the file is accessible + if (FileAccess.isFileAvailable(file)) { + // If accessible, run the function immediately and return + onFileAccessible(PendingFile(file = file)) + return + } + + // Add the file to the channel for processing + fileChannel.trySend(PendingFile(file = file)) + + // Coroutine to process the file and remove it from the queue when accessible + Coroutines.default().launch { + while (true) { + delay(500) + val currentFile = fileChannel.receive() + if (FileAccess.isFileAvailable(currentFile.file)) { + onFileAccessible(currentFile) + // File is accessible, remove it from the queue + removeFromQueue(currentFile.file) { /* Do nothing here as the operation is not intended to be performed here */ } + } else { + // File is not accessible, put it back in the channel for later processing + fileChannel.send(currentFile.apply { time += 500 }) + onFilePending(currentFile) + } + } + } // https://chat.openai.com/share/f3c8f6ea-603a-40d6-a811-f8fea5067501 + } + + fun removeFromQueue(file: File, onFileRemoved: (PendingFile) -> Unit) { + val removedFile = fileChannel.findAndRemove { it.file == file } + removedFile?.let { + onFileRemoved(it) + } + } + + // Extension function to find and remove an element from the channel + fun Channel.findAndRemove(predicate: (T) -> Boolean): T? { + val items = mutableListOf() + while (true) { + val item = poll() ?: break + if (predicate(item)) { + return item + } + items.add(item) + } + for (item in items) { + offer(item) + } + return null + } + +} \ No newline at end of file diff --git a/Reader/src/main/kotlin/no/iktdev/streamit/content/reader/streams/StreamsReader.kt b/Reader/src/main/kotlin/no/iktdev/streamit/content/reader/streams/StreamsReader.kt new file mode 100644 index 00000000..e184f01f --- /dev/null +++ b/Reader/src/main/kotlin/no/iktdev/streamit/content/reader/streams/StreamsReader.kt @@ -0,0 +1,68 @@ +package no.iktdev.streamit.content.reader.streams + +import kotlinx.coroutines.runBlocking +import mu.KotlinLogging +import no.iktdev.streamit.content.common.CommonConfig +import no.iktdev.streamit.content.common.deamon.Daemon +import no.iktdev.streamit.content.common.deamon.IDaemon +import no.iktdev.streamit.content.reader.ReaderEnv +import no.iktdev.streamit.library.kafka.KnownEvents +import no.iktdev.streamit.library.kafka.KnownEvents.EVENT_READER_RECEIVED_FILE +import no.iktdev.streamit.library.kafka.Message +import no.iktdev.streamit.library.kafka.Status +import no.iktdev.streamit.library.kafka.StatusType +import no.iktdev.streamit.library.kafka.consumers.DefaultConsumer +import no.iktdev.streamit.library.kafka.listener.EventMessageListener +import no.iktdev.streamit.library.kafka.producer.DefaultProducer +import org.apache.kafka.clients.consumer.ConsumerRecord +import org.springframework.stereotype.Service + +private val logger = KotlinLogging.logger {} +@Service +class StreamsReader { + + val messageProducer = DefaultProducer(CommonConfig.kafkaConsumerId) + val defaultConsumer = DefaultConsumer().apply { + // autoCommit = false + } + init { + object: EventMessageListener(CommonConfig.kafkaConsumerId, defaultConsumer, listOf(EVENT_READER_RECEIVED_FILE.event)) { + override fun onMessage(data: ConsumerRecord) { + if (data.value().status.statusType != StatusType.SUCCESS) { + logger.info { "Ignoring event: ${data.key()} as status is not Success!" } + return + } else if (data.value().data !is String) { + logger.info { "Ignoring event: ${data.key()} as values is not of expected type!" } + return + } + logger.info { "Preparing Probe for ${data.value().data}" } + val output = mutableListOf() + val d = Daemon(executable = ReaderEnv.ffprobe, parameters = listOf("-v", "quiet", "-print_format", "json", "-show_streams", data.value().data as String), daemonInterface = object: + IDaemon { + override fun onOutputChanged(line: String) { + output.add(line) + } + + override fun onStarted() { + logger.info { "Probe started for ${data.value().data}" } + } + + override fun onError() { + logger.error { "An error occurred for ${data.value().data}" } + } + + override fun onEnded() { + logger.info { "Probe ended for ${data.value().data}" } + } + + }) + val resultCode = runBlocking { + d.run() + } + + val message = Message(status = Status( statusType = if (resultCode == 0) StatusType.SUCCESS else StatusType.ERROR), data = output.joinToString("\n")) + messageProducer.sendMessage(KnownEvents.EVENT_READER_RECEIVED_STREAMS.event, message) + } + }.listen() + } +} \ No newline at end of file diff --git a/Reader/src/test/kotlin/no/iktdev/streamit/content/reader/Resources.kt b/Reader/src/test/kotlin/no/iktdev/streamit/content/reader/Resources.kt new file mode 100644 index 00000000..9449301e --- /dev/null +++ b/Reader/src/test/kotlin/no/iktdev/streamit/content/reader/Resources.kt @@ -0,0 +1,33 @@ +package no.iktdev.streamit.content.reader + +import org.apache.kafka.clients.consumer.ConsumerRecord + +open class Resources { + + fun getText(path: String): String? { + return this.javaClass.classLoader.getResource(path)?.readText() + } + + open class Streams(): Resources() { + fun all(): List { + return listOf( + getSample(0), + getSample(1), + getSample(2), + getSample(3), + getSample(4), + getSample(5), + getSample(6), + ) + } + + fun getSample(number: Int): String { + return getText("streams/sample$number.json")!! + } + } + + fun getConsumerRecord(event: String, data: T): ConsumerRecord { + return ConsumerRecord("testTopic", 0, 0L, event, data) + } + +} \ No newline at end of file diff --git a/Reader/src/test/kotlin/no/iktdev/streamit/content/reader/analyzer/EncodeStreamsMessageParserTest.kt b/Reader/src/test/kotlin/no/iktdev/streamit/content/reader/analyzer/EncodeStreamsMessageParserTest.kt new file mode 100644 index 00000000..2699823a --- /dev/null +++ b/Reader/src/test/kotlin/no/iktdev/streamit/content/reader/analyzer/EncodeStreamsMessageParserTest.kt @@ -0,0 +1,34 @@ +package no.iktdev.streamit.content.reader.analyzer + +import no.iktdev.streamit.content.reader.Resources +import no.iktdev.streamit.library.kafka.KnownEvents +import no.iktdev.streamit.library.kafka.Message +import no.iktdev.streamit.library.kafka.Status +import no.iktdev.streamit.library.kafka.StatusType +import org.apache.kafka.clients.consumer.ConsumerRecord +import org.junit.jupiter.api.Test + +import org.junit.jupiter.api.Assertions.* + +class EncodeStreamsMessageParserTest { + val parser = EncodeStreamsMessageParser() + val baseEvent = Message(status = Status( statusType = StatusType.SUCCESS)) + + @Test + fun getFileNameFromEvent() { + val payload = Resources.Streams().getSample(3) + assertDoesNotThrow { + val msg = baseEvent.copy(data = payload) + val result = parser.getMediaStreamsFromEvent(mutableListOf( + Resources().getConsumerRecord( + KnownEvents.EVENT_READER_RECEIVED_STREAMS.event, + msg + ) + )) + } + } + + @Test + fun getMediaStreamsFromEvent() { + } +} \ No newline at end of file diff --git a/Reader/src/test/resources/streams/sample1.json b/Reader/src/test/resources/streams/sample1.json new file mode 100644 index 00000000..3b347322 --- /dev/null +++ b/Reader/src/test/resources/streams/sample1.json @@ -0,0 +1,97 @@ +{ + "streams": [ + { + "index": 0, + "codec_name": "hevc", + "codec_long_name": "H.265 / HEVC (High Efficiency Video Coding)", + "profile": "Main 10", + "codec_type": "video", + "codec_time_base": "1/25", + "codec_tag_string": "hev1", + "codec_tag": "0x31766568", + "width": 1920, + "height": 960, + "coded_width": 1920, + "coded_height": 960, + "has_b_frames": 2, + "sample_aspect_ratio": "1:1", + "display_aspect_ratio": "2:1", + "pix_fmt": "yuv420p10le", + "level": 120, + "color_range": "tv", + "refs": 1, + "r_frame_rate": "25/1", + "avg_frame_rate": "25/1", + "time_base": "1/25000", + "start_pts": 0, + "start_time": "0.000000", + "duration_ts": 70902000, + "duration": "2836.080000", + "bit_rate": "1999184", + "nb_frames": "70902", + "disposition": { + "default": 1, + "dub": 0, + "original": 0, + "comment": 0, + "lyrics": 0, + "karaoke": 0, + "forced": 0, + "hearing_impaired": 0, + "visual_impaired": 0, + "clean_effects": 0, + "attached_pic": 0, + "timed_thumbnails": 0 + }, + "tags": { + "creation_time": "2022-01-04T07:01:48.000000Z", + "language": "und", + "handler_name": "VideoHandler" + } + }, + { + "index": 1, + "codec_name": "aac", + "codec_long_name": "AAC (Advanced Audio Coding)", + "profile": "LC", + "codec_type": "audio", + "codec_time_base": "1/48000", + "codec_tag_string": "mp4a", + "codec_tag": "0x6134706d", + "sample_fmt": "fltp", + "sample_rate": "48000", + "channels": 6, + "channel_layout": "5.1", + "bits_per_sample": 0, + "r_frame_rate": "0/0", + "avg_frame_rate": "0/0", + "time_base": "1/48000", + "start_pts": 0, + "start_time": "0.000000", + "duration_ts": 136131024, + "duration": "2836.063000", + "bit_rate": "224000", + "max_bit_rate": "224000", + "nb_frames": "132943", + "disposition": { + "default": 1, + "dub": 0, + "original": 0, + "comment": 0, + "lyrics": 0, + "karaoke": 0, + "forced": 0, + "hearing_impaired": 0, + "visual_impaired": 0, + "clean_effects": 0, + "attached_pic": 0, + "timed_thumbnails": 0 + }, + "tags": { + "creation_time": "2022-01-04T07:01:48.000000Z", + "language": "nor", + "handler_name": "SoundHandler" + } + } + ] +} \ No newline at end of file diff --git a/Reader/src/test/resources/streams/sample2.json b/Reader/src/test/resources/streams/sample2.json new file mode 100644 index 00000000..98dfecf3 --- /dev/null +++ b/Reader/src/test/resources/streams/sample2.json @@ -0,0 +1,118 @@ +{ + "streams": [ + { + "index": 0, + "codec_name": "hevc", + "codec_long_name": "H.265 / HEVC (High Efficiency Video Coding)", + "profile": "Main 10", + "codec_type": "video", + "codec_time_base": "1/24", + "codec_tag_string": "hev1", + "codec_tag": "0x31766568", + "width": 1920, + "height": 960, + "coded_width": 1920, + "coded_height": 960, + "has_b_frames": 2, + "sample_aspect_ratio": "1:1", + "display_aspect_ratio": "2:1", + "pix_fmt": "yuv420p10le", + "level": 120, + "color_range": "tv", + "refs": 1, + "r_frame_rate": "24/1", + "avg_frame_rate": "24/1", + "time_base": "1/24000", + "start_pts": 0, + "start_time": "0.000000", + "duration_ts": 58857000, + "duration": "2452.375000", + "bit_rate": "1999262", + "nb_frames": "58857", + "disposition": { + "default": 1, + "dub": 0, + "original": 0, + "comment": 0, + "lyrics": 0, + "karaoke": 0, + "forced": 0, + "hearing_impaired": 0, + "visual_impaired": 0, + "clean_effects": 0, + "attached_pic": 0, + "timed_thumbnails": 0 + }, + "tags": { + "creation_time": "2021-12-03T08:59:16.000000Z", + "language": "und", + "handler_name": "VideoHandler" + } + }, + { + "index": 1, + "codec_name": "aac", + "codec_long_name": "AAC (Advanced Audio Coding)", + "profile": "LC", + "codec_type": "audio", + "codec_time_base": "1/48000", + "codec_tag_string": "mp4a", + "codec_tag": "0x6134706d", + "sample_fmt": "fltp", + "sample_rate": "48000", + "channels": 6, + "channel_layout": "5.1", + "bits_per_sample": 0, + "r_frame_rate": "0/0", + "avg_frame_rate": "0/0", + "time_base": "1/48000", + "start_pts": 0, + "start_time": "0.000000", + "duration_ts": 117714384, + "duration": "2452.383000", + "bit_rate": "224003", + "max_bit_rate": "224003", + "nb_frames": "114958", + "disposition": { + "default": 1, + "dub": 0, + "original": 0, + "comment": 0, + "lyrics": 0, + "karaoke": 0, + "forced": 0, + "hearing_impaired": 0, + "visual_impaired": 0, + "clean_effects": 0, + "attached_pic": 0, + "timed_thumbnails": 0 + }, + "tags": { + "creation_time": "2021-12-03T08:59:16.000000Z", + "language": "eng", + "handler_name": "SoundHandler" + } + } + ], + "format": { + "filename": "Alex.Rider.S02E01.1080p.WEBRip.x265-RARBG.mp4", + "nb_streams": 2, + "nb_programs": 0, + "format_name": "mov,mp4,m4a,3gp,3g2,mj2", + "format_long_name": "QuickTime / MOV", + "start_time": "0.000000", + "duration": "2452.426000", + "size": "683226674", + "bit_rate": "2228737", + "probe_score": 100, + "tags": { + "major_brand": "isom", + "minor_version": "512", + "compatible_brands": "isomiso2mp41", + "creation_time": "2021-12-03T08:59:16.000000Z", + "title": "Alex.Rider.S02E01.1080p.WEBRip.x265-RARBG", + "encoder": "Lavf58.20.100", + "comment": "Alex.Rider.S02E01.1080p.WEBRip.x265-RARBG" + } + } +} \ No newline at end of file diff --git a/Reader/src/test/resources/streams/sample3.json b/Reader/src/test/resources/streams/sample3.json new file mode 100644 index 00000000..fdded673 --- /dev/null +++ b/Reader/src/test/resources/streams/sample3.json @@ -0,0 +1,550 @@ +{ + "streams": [ + { + "index": 0, + "codec_name": "hevc", + "codec_long_name": "H.265 / HEVC (High Efficiency Video Coding)", + "profile": "Main 10", + "codec_type": "video", + "codec_time_base": "1001/24000", + "codec_tag_string": "[0][0][0][0]", + "codec_tag": "0x0000", + "width": 1920, + "height": 804, + "coded_width": 1920, + "coded_height": 808, + "has_b_frames": 2, + "sample_aspect_ratio": "1:1", + "display_aspect_ratio": "160:67", + "pix_fmt": "yuv420p10le", + "level": 123, + "color_range": "tv", + "color_space": "bt709", + "color_transfer": "bt709", + "color_primaries": "bt709", + "refs": 1, + "r_frame_rate": "24000/1001", + "avg_frame_rate": "24000/1001", + "time_base": "1/1000", + "start_pts": 0, + "start_time": "0.000000", + "disposition": { + "default": 1, + "dub": 0, + "original": 0, + "comment": 0, + "lyrics": 0, + "karaoke": 0, + "forced": 0, + "hearing_impaired": 0, + "visual_impaired": 0, + "clean_effects": 0, + "attached_pic": 0, + "timed_thumbnails": 0 + }, + "tags": { + "title": "Presented By EMBER", + "BPS": "3796879", + "DURATION": "02:01:28.782000000", + "NUMBER_OF_FRAMES": "174756", + "NUMBER_OF_BYTES": "3459328516", + "_STATISTICS_WRITING_APP": "mkvmerge v65.0.0 ('Too Much') 64-bit", + "_STATISTICS_WRITING_DATE_UTC": "2022-05-24 07:43:44", + "_STATISTICS_TAGS": "BPS DURATION NUMBER_OF_FRAMES NUMBER_OF_BYTES" + } + }, + { + "index": 1, + "codec_name": "ac3", + "codec_long_name": "ATSC A/52A (AC-3)", + "codec_type": "audio", + "codec_time_base": "1/48000", + "codec_tag_string": "[0][0][0][0]", + "codec_tag": "0x0000", + "sample_fmt": "fltp", + "sample_rate": "48000", + "channels": 6, + "channel_layout": "5.1(side)", + "bits_per_sample": 0, + "dmix_mode": "-1", + "ltrt_cmixlev": "-1.000000", + "ltrt_surmixlev": "-1.000000", + "loro_cmixlev": "-1.000000", + "loro_surmixlev": "-1.000000", + "r_frame_rate": "0/0", + "avg_frame_rate": "0/0", + "time_base": "1/1000", + "start_pts": 0, + "start_time": "0.000000", + "bit_rate": "448000", + "disposition": { + "default": 1, + "dub": 0, + "original": 0, + "comment": 0, + "lyrics": 0, + "karaoke": 0, + "forced": 0, + "hearing_impaired": 0, + "visual_impaired": 0, + "clean_effects": 0, + "attached_pic": 0, + "timed_thumbnails": 0 + }, + "tags": { + "language": "eng", + "BPS": "448000", + "DURATION": "02:01:28.832000000", + "NUMBER_OF_FRAMES": "227776", + "NUMBER_OF_BYTES": "408174592", + "_STATISTICS_WRITING_APP": "mkvmerge v65.0.0 ('Too Much') 64-bit", + "_STATISTICS_WRITING_DATE_UTC": "2022-05-24 07:43:44", + "_STATISTICS_TAGS": "BPS DURATION NUMBER_OF_FRAMES NUMBER_OF_BYTES" + } + }, + { + "index": 2, + "codec_name": "ac3", + "codec_long_name": "ATSC A/52A (AC-3)", + "codec_type": "audio", + "codec_time_base": "1/48000", + "codec_tag_string": "[0][0][0][0]", + "codec_tag": "0x0000", + "sample_fmt": "fltp", + "sample_rate": "48000", + "channels": 6, + "channel_layout": "5.1(side)", + "bits_per_sample": 0, + "dmix_mode": "-1", + "ltrt_cmixlev": "-1.000000", + "ltrt_surmixlev": "-1.000000", + "loro_cmixlev": "-1.000000", + "loro_surmixlev": "-1.000000", + "r_frame_rate": "0/0", + "avg_frame_rate": "0/0", + "time_base": "1/1000", + "start_pts": 0, + "start_time": "0.000000", + "bit_rate": "448000", + "disposition": { + "default": 0, + "dub": 0, + "original": 0, + "comment": 0, + "lyrics": 0, + "karaoke": 0, + "forced": 0, + "hearing_impaired": 0, + "visual_impaired": 0, + "clean_effects": 0, + "attached_pic": 0, + "timed_thumbnails": 0 + }, + "tags": { + "language": "jpn", + "BPS": "448000", + "DURATION": "02:01:28.832000000", + "NUMBER_OF_FRAMES": "227776", + "NUMBER_OF_BYTES": "408174592", + "_STATISTICS_WRITING_APP": "mkvmerge v65.0.0 ('Too Much') 64-bit", + "_STATISTICS_WRITING_DATE_UTC": "2022-05-24 07:43:44", + "_STATISTICS_TAGS": "BPS DURATION NUMBER_OF_FRAMES NUMBER_OF_BYTES" + } + }, + { + "index": 3, + "codec_name": "ass", + "codec_long_name": "ASS (Advanced SSA) subtitle", + "codec_type": "subtitle", + "codec_time_base": "0/1", + "codec_tag_string": "[0][0][0][0]", + "codec_tag": "0x0000", + "r_frame_rate": "0/0", + "avg_frame_rate": "0/0", + "time_base": "1/1000", + "start_pts": 0, + "start_time": "0.000000", + "duration_ts": 7288832, + "duration": "7288.832000", + "disposition": { + "default": 1, + "dub": 0, + "original": 0, + "comment": 0, + "lyrics": 0, + "karaoke": 0, + "forced": 0, + "hearing_impaired": 0, + "visual_impaired": 0, + "clean_effects": 0, + "attached_pic": 0, + "timed_thumbnails": 0 + }, + "tags": { + "language": "eng", + "title": "Signs & Songs@EMBER", + "BPS": "5", + "DURATION": "01:54:41.630000000", + "NUMBER_OF_FRAMES": "90", + "NUMBER_OF_BYTES": "4696", + "_STATISTICS_WRITING_APP": "mkvmerge v65.0.0 ('Too Much') 64-bit", + "_STATISTICS_WRITING_DATE_UTC": "2022-05-24 07:43:44", + "_STATISTICS_TAGS": "BPS DURATION NUMBER_OF_FRAMES NUMBER_OF_BYTES" + } + }, + { + "index": 4, + "codec_name": "ass", + "codec_long_name": "ASS (Advanced SSA) subtitle", + "codec_type": "subtitle", + "codec_time_base": "0/1", + "codec_tag_string": "[0][0][0][0]", + "codec_tag": "0x0000", + "r_frame_rate": "0/0", + "avg_frame_rate": "0/0", + "time_base": "1/1000", + "start_pts": 0, + "start_time": "0.000000", + "duration_ts": 7288832, + "duration": "7288.832000", + "disposition": { + "default": 0, + "dub": 0, + "original": 0, + "comment": 0, + "lyrics": 0, + "karaoke": 0, + "forced": 0, + "hearing_impaired": 0, + "visual_impaired": 0, + "clean_effects": 0, + "attached_pic": 0, + "timed_thumbnails": 0 + }, + "tags": { + "language": "eng", + "title": "Dialogue@EMBER", + "BPS": "78", + "DURATION": "01:56:48.150000000", + "NUMBER_OF_FRAMES": "1434", + "NUMBER_OF_BYTES": "69001", + "_STATISTICS_WRITING_APP": "mkvmerge v65.0.0 ('Too Much') 64-bit", + "_STATISTICS_WRITING_DATE_UTC": "2022-05-24 07:43:44", + "_STATISTICS_TAGS": "BPS DURATION NUMBER_OF_FRAMES NUMBER_OF_BYTES" + } + }, + { + "index": 5, + "codec_name": "hdmv_pgs_subtitle", + "codec_long_name": "HDMV Presentation Graphic Stream subtitles", + "codec_type": "subtitle", + "codec_time_base": "0/1", + "codec_tag_string": "[0][0][0][0]", + "codec_tag": "0x0000", + "r_frame_rate": "0/0", + "avg_frame_rate": "0/0", + "time_base": "1/1000", + "start_pts": 0, + "start_time": "0.000000", + "duration_ts": 7288832, + "duration": "7288.832000", + "disposition": { + "default": 0, + "dub": 0, + "original": 0, + "comment": 0, + "lyrics": 0, + "karaoke": 0, + "forced": 0, + "hearing_impaired": 0, + "visual_impaired": 0, + "clean_effects": 0, + "attached_pic": 0, + "timed_thumbnails": 0 + }, + "tags": { + "language": "eng", + "title": "Signs & Songs@USBD", + "BPS": "402", + "DURATION": "01:50:49.111000000", + "NUMBER_OF_FRAMES": "56", + "NUMBER_OF_BYTES": "334551", + "_STATISTICS_WRITING_APP": "mkvmerge v65.0.0 ('Too Much') 64-bit", + "_STATISTICS_WRITING_DATE_UTC": "2022-05-24 07:43:44", + "_STATISTICS_TAGS": "BPS DURATION NUMBER_OF_FRAMES NUMBER_OF_BYTES" + } + }, + { + "index": 6, + "codec_name": "hdmv_pgs_subtitle", + "codec_long_name": "HDMV Presentation Graphic Stream subtitles", + "codec_type": "subtitle", + "codec_time_base": "0/1", + "codec_tag_string": "[0][0][0][0]", + "codec_tag": "0x0000", + "r_frame_rate": "0/0", + "avg_frame_rate": "0/0", + "time_base": "1/1000", + "start_pts": 0, + "start_time": "0.000000", + "duration_ts": 7288832, + "duration": "7288.832000", + "disposition": { + "default": 0, + "dub": 0, + "original": 0, + "comment": 0, + "lyrics": 0, + "karaoke": 0, + "forced": 0, + "hearing_impaired": 0, + "visual_impaired": 0, + "clean_effects": 0, + "attached_pic": 0, + "timed_thumbnails": 0 + }, + "tags": { + "language": "eng", + "title": "Dialogue@USBD", + "BPS": "21019", + "DURATION": "02:00:56.802000000", + "NUMBER_OF_FRAMES": "2829", + "NUMBER_OF_BYTES": "19067149", + "_STATISTICS_WRITING_APP": "mkvmerge v65.0.0 ('Too Much') 64-bit", + "_STATISTICS_WRITING_DATE_UTC": "2022-05-24 07:43:44", + "_STATISTICS_TAGS": "BPS DURATION NUMBER_OF_FRAMES NUMBER_OF_BYTES" + } + }, + { + "index": 7, + "codec_name": "hdmv_pgs_subtitle", + "codec_long_name": "HDMV Presentation Graphic Stream subtitles", + "codec_type": "subtitle", + "codec_time_base": "0/1", + "codec_tag_string": "[0][0][0][0]", + "codec_tag": "0x0000", + "r_frame_rate": "0/0", + "avg_frame_rate": "0/0", + "time_base": "1/1000", + "start_pts": 0, + "start_time": "0.000000", + "duration_ts": 7288832, + "duration": "7288.832000", + "disposition": { + "default": 0, + "dub": 0, + "original": 0, + "comment": 0, + "lyrics": 0, + "karaoke": 0, + "forced": 0, + "hearing_impaired": 0, + "visual_impaired": 0, + "clean_effects": 0, + "attached_pic": 0, + "timed_thumbnails": 0 + }, + "tags": { + "language": "eng", + "title": "CC@USBD", + "BPS": "34179", + "DURATION": "01:58:57.850000000", + "NUMBER_OF_FRAMES": "4338", + "NUMBER_OF_BYTES": "30495881", + "_STATISTICS_WRITING_APP": "mkvmerge v65.0.0 ('Too Much') 64-bit", + "_STATISTICS_WRITING_DATE_UTC": "2022-05-24 07:43:44", + "_STATISTICS_TAGS": "BPS DURATION NUMBER_OF_FRAMES NUMBER_OF_BYTES" + } + }, + { + "index": 8, + "codec_type": "attachment", + "codec_tag_string": "[0][0][0][0]", + "codec_tag": "0x0000", + "r_frame_rate": "0/0", + "avg_frame_rate": "0/0", + "time_base": "1/90000", + "start_pts": 0, + "start_time": "0.000000", + "duration_ts": 655994880, + "duration": "7288.832000", + "disposition": { + "default": 0, + "dub": 0, + "original": 0, + "comment": 0, + "lyrics": 0, + "karaoke": 0, + "forced": 0, + "hearing_impaired": 0, + "visual_impaired": 0, + "clean_effects": 0, + "attached_pic": 0, + "timed_thumbnails": 0 + }, + "tags": { + "filename": "GandhiSans-BoldItalic.otf", + "mimetype": "font/otf" + } + }, + { + "index": 9, + "codec_type": "attachment", + "codec_tag_string": "[0][0][0][0]", + "codec_tag": "0x0000", + "r_frame_rate": "0/0", + "avg_frame_rate": "0/0", + "time_base": "1/90000", + "start_pts": 0, + "start_time": "0.000000", + "duration_ts": 655994880, + "duration": "7288.832000", + "disposition": { + "default": 0, + "dub": 0, + "original": 0, + "comment": 0, + "lyrics": 0, + "karaoke": 0, + "forced": 0, + "hearing_impaired": 0, + "visual_impaired": 0, + "clean_effects": 0, + "attached_pic": 0, + "timed_thumbnails": 0 + }, + "tags": { + "filename": "HAPPYHELL.TTF", + "mimetype": "font/ttf" + } + }, + { + "index": 10, + "codec_type": "attachment", + "codec_tag_string": "[0][0][0][0]", + "codec_tag": "0x0000", + "r_frame_rate": "0/0", + "avg_frame_rate": "0/0", + "time_base": "1/90000", + "start_pts": 0, + "start_time": "0.000000", + "duration_ts": 655994880, + "duration": "7288.832000", + "disposition": { + "default": 0, + "dub": 0, + "original": 0, + "comment": 0, + "lyrics": 0, + "karaoke": 0, + "forced": 0, + "hearing_impaired": 0, + "visual_impaired": 0, + "clean_effects": 0, + "attached_pic": 0, + "timed_thumbnails": 0 + }, + "tags": { + "filename": "AVERIALIBRE-BOLD.TTF", + "mimetype": "font/ttf" + } + }, + { + "index": 11, + "codec_type": "attachment", + "codec_tag_string": "[0][0][0][0]", + "codec_tag": "0x0000", + "r_frame_rate": "0/0", + "avg_frame_rate": "0/0", + "time_base": "1/90000", + "start_pts": 0, + "start_time": "0.000000", + "duration_ts": 655994880, + "duration": "7288.832000", + "disposition": { + "default": 0, + "dub": 0, + "original": 0, + "comment": 0, + "lyrics": 0, + "karaoke": 0, + "forced": 0, + "hearing_impaired": 0, + "visual_impaired": 0, + "clean_effects": 0, + "attached_pic": 0, + "timed_thumbnails": 0 + }, + "tags": { + "filename": "GandhiSans-Bold.otf", + "mimetype": "font/otf" + } + }, + { + "index": 12, + "codec_name": "mjpeg", + "codec_long_name": "Motion JPEG", + "profile": "Baseline", + "codec_type": "video", + "codec_time_base": "0/1", + "codec_tag_string": "[0][0][0][0]", + "codec_tag": "0x0000", + "width": 640, + "height": 360, + "coded_width": 640, + "coded_height": 360, + "has_b_frames": 0, + "sample_aspect_ratio": "1:1", + "display_aspect_ratio": "16:9", + "pix_fmt": "yuvj420p", + "level": -99, + "color_range": "pc", + "color_space": "bt470bg", + "chroma_location": "center", + "refs": 1, + "r_frame_rate": "90000/1", + "avg_frame_rate": "0/0", + "time_base": "1/90000", + "start_pts": 0, + "start_time": "0.000000", + "duration_ts": 655994880, + "duration": "7288.832000", + "bits_per_raw_sample": "8", + "disposition": { + "default": 0, + "dub": 0, + "original": 0, + "comment": 0, + "lyrics": 0, + "karaoke": 0, + "forced": 0, + "hearing_impaired": 0, + "visual_impaired": 0, + "clean_effects": 0, + "attached_pic": 1, + "timed_thumbnails": 0 + }, + "tags": { + "filename": "cover.jpg", + "mimetype": "image/jpeg" + } + } + ], + "format": { + "filename": "[EMBER] Belle - Ryuu to Sobakasu no Hime (2021) (Movie) [BDRip] [804p Dual Audio HEVC 10 bits DD].mkv", + "nb_streams": 13, + "nb_programs": 0, + "format_name": "matroska,webm", + "format_long_name": "Matroska / WebM", + "start_time": "0.000000", + "duration": "7288.832000", + "size": "4333518626", + "bit_rate": "4756338", + "probe_score": 100, + "tags": { + "title": "Belle.1080p.Dual.Audio.BDRip.10.bits.DD.x265-EMBER", + "encoder": "libebml v1.4.2 + libmatroska v1.6.4", + "creation_time": "2022-05-24T07:43:44.000000Z" + } + } +} \ No newline at end of file diff --git a/Reader/src/test/resources/streams/sample4.json b/Reader/src/test/resources/streams/sample4.json new file mode 100644 index 00000000..e93715d7 --- /dev/null +++ b/Reader/src/test/resources/streams/sample4.json @@ -0,0 +1,1093 @@ +{ + "streams": [ + { + "index": 0, + "codec_name": "h264", + "codec_long_name": "H.264 / AVC / MPEG-4 AVC / MPEG-4 part 10", + "profile": "High", + "codec_type": "video", + "codec_tag_string": "[0][0][0][0]", + "codec_tag": "0x0000", + "width": 1920, + "height": 1080, + "coded_width": 1920, + "coded_height": 1080, + "closed_captions": 0, + "has_b_frames": 0, + "sample_aspect_ratio": "1:1", + "display_aspect_ratio": "16:9", + "pix_fmt": "yuv420p", + "level": 40, + "chroma_location": "left", + "field_order": "progressive", + "refs": 1, + "is_avc": "true", + "nal_length_size": "4", + "r_frame_rate": "24000/1001", + "avg_frame_rate": "24000/1001", + "time_base": "1/1000", + "start_pts": 0, + "start_time": "0.000000", + "bits_per_raw_sample": "8", + "disposition": { + "default": 1, + "dub": 0, + "original": 0, + "comment": 0, + "lyrics": 0, + "karaoke": 0, + "forced": 0, + "hearing_impaired": 0, + "visual_impaired": 0, + "clean_effects": 0, + "attached_pic": 0, + "timed_thumbnails": 0 + } + }, + { + "index": 1, + "codec_name": "aac", + "codec_long_name": "AAC (Advanced Audio Coding)", + "profile": "LC", + "codec_type": "audio", + "codec_tag_string": "[0][0][0][0]", + "codec_tag": "0x0000", + "sample_fmt": "fltp", + "sample_rate": "44100", + "channels": 2, + "channel_layout": "stereo", + "bits_per_sample": 0, + "r_frame_rate": "0/0", + "avg_frame_rate": "0/0", + "time_base": "1/1000", + "start_pts": 0, + "start_time": "0.000000", + "disposition": { + "default": 1, + "dub": 0, + "original": 0, + "comment": 0, + "lyrics": 0, + "karaoke": 0, + "forced": 0, + "hearing_impaired": 0, + "visual_impaired": 0, + "clean_effects": 0, + "attached_pic": 0, + "timed_thumbnails": 0 + }, + "tags": { + "language": "jpn", + "title": "Japanese" + } + }, + { + "index": 2, + "codec_name": "ass", + "codec_long_name": "ASS (Advanced SSA) subtitle", + "codec_type": "subtitle", + "codec_tag_string": "[0][0][0][0]", + "codec_tag": "0x0000", + "r_frame_rate": "0/0", + "avg_frame_rate": "0/0", + "time_base": "1/1000", + "start_pts": 0, + "start_time": "0.000000", + "duration_ts": 1420016, + "duration": "1420.016000", + "disposition": { + "default": 1, + "dub": 0, + "original": 0, + "comment": 0, + "lyrics": 0, + "karaoke": 0, + "forced": 0, + "hearing_impaired": 0, + "visual_impaired": 0, + "clean_effects": 0, + "attached_pic": 0, + "timed_thumbnails": 0 + }, + "tags": { + "language": "eng", + "title": "English (United States)" + } + }, + { + "index": 3, + "codec_name": "ass", + "codec_long_name": "ASS (Advanced SSA) subtitle", + "codec_type": "subtitle", + "codec_tag_string": "[0][0][0][0]", + "codec_tag": "0x0000", + "r_frame_rate": "0/0", + "avg_frame_rate": "0/0", + "time_base": "1/1000", + "start_pts": 0, + "start_time": "0.000000", + "duration_ts": 1420016, + "duration": "1420.016000", + "disposition": { + "default": 0, + "dub": 0, + "original": 0, + "comment": 0, + "lyrics": 0, + "karaoke": 0, + "forced": 0, + "hearing_impaired": 0, + "visual_impaired": 0, + "clean_effects": 0, + "attached_pic": 0, + "timed_thumbnails": 0 + }, + "tags": { + "language": "ger", + "title": "German" + } + }, + { + "index": 4, + "codec_name": "ass", + "codec_long_name": "ASS (Advanced SSA) subtitle", + "codec_type": "subtitle", + "codec_tag_string": "[0][0][0][0]", + "codec_tag": "0x0000", + "r_frame_rate": "0/0", + "avg_frame_rate": "0/0", + "time_base": "1/1000", + "start_pts": 0, + "start_time": "0.000000", + "duration_ts": 1420016, + "duration": "1420.016000", + "disposition": { + "default": 0, + "dub": 0, + "original": 0, + "comment": 0, + "lyrics": 0, + "karaoke": 0, + "forced": 0, + "hearing_impaired": 0, + "visual_impaired": 0, + "clean_effects": 0, + "attached_pic": 0, + "timed_thumbnails": 0 + }, + "tags": { + "language": "spa", + "title": "Spanish" + } + }, + { + "index": 5, + "codec_name": "ass", + "codec_long_name": "ASS (Advanced SSA) subtitle", + "codec_type": "subtitle", + "codec_tag_string": "[0][0][0][0]", + "codec_tag": "0x0000", + "r_frame_rate": "0/0", + "avg_frame_rate": "0/0", + "time_base": "1/1000", + "start_pts": 0, + "start_time": "0.000000", + "duration_ts": 1420016, + "duration": "1420.016000", + "disposition": { + "default": 0, + "dub": 0, + "original": 0, + "comment": 0, + "lyrics": 0, + "karaoke": 0, + "forced": 0, + "hearing_impaired": 0, + "visual_impaired": 0, + "clean_effects": 0, + "attached_pic": 0, + "timed_thumbnails": 0 + }, + "tags": { + "language": "spa", + "title": "Spanish (Latin America)" + } + }, + { + "index": 6, + "codec_name": "ass", + "codec_long_name": "ASS (Advanced SSA) subtitle", + "codec_type": "subtitle", + "codec_tag_string": "[0][0][0][0]", + "codec_tag": "0x0000", + "r_frame_rate": "0/0", + "avg_frame_rate": "0/0", + "time_base": "1/1000", + "start_pts": 0, + "start_time": "0.000000", + "duration_ts": 1420016, + "duration": "1420.016000", + "disposition": { + "default": 0, + "dub": 0, + "original": 0, + "comment": 0, + "lyrics": 0, + "karaoke": 0, + "forced": 0, + "hearing_impaired": 0, + "visual_impaired": 0, + "clean_effects": 0, + "attached_pic": 0, + "timed_thumbnails": 0 + }, + "tags": { + "language": "fre", + "title": "French" + } + }, + { + "index": 7, + "codec_name": "ass", + "codec_long_name": "ASS (Advanced SSA) subtitle", + "codec_type": "subtitle", + "codec_tag_string": "[0][0][0][0]", + "codec_tag": "0x0000", + "r_frame_rate": "0/0", + "avg_frame_rate": "0/0", + "time_base": "1/1000", + "start_pts": 0, + "start_time": "0.000000", + "duration_ts": 1420016, + "duration": "1420.016000", + "disposition": { + "default": 0, + "dub": 0, + "original": 0, + "comment": 0, + "lyrics": 0, + "karaoke": 0, + "forced": 0, + "hearing_impaired": 0, + "visual_impaired": 0, + "clean_effects": 0, + "attached_pic": 0, + "timed_thumbnails": 0 + }, + "tags": { + "language": "ita", + "title": "Italian" + } + }, + { + "index": 8, + "codec_name": "ass", + "codec_long_name": "ASS (Advanced SSA) subtitle", + "codec_type": "subtitle", + "codec_tag_string": "[0][0][0][0]", + "codec_tag": "0x0000", + "r_frame_rate": "0/0", + "avg_frame_rate": "0/0", + "time_base": "1/1000", + "start_pts": 0, + "start_time": "0.000000", + "duration_ts": 1420016, + "duration": "1420.016000", + "disposition": { + "default": 0, + "dub": 0, + "original": 0, + "comment": 0, + "lyrics": 0, + "karaoke": 0, + "forced": 0, + "hearing_impaired": 0, + "visual_impaired": 0, + "clean_effects": 0, + "attached_pic": 0, + "timed_thumbnails": 0 + }, + "tags": { + "language": "por", + "title": "Portuguese (Brazil)" + } + }, + { + "index": 9, + "codec_name": "ass", + "codec_long_name": "ASS (Advanced SSA) subtitle", + "codec_type": "subtitle", + "codec_tag_string": "[0][0][0][0]", + "codec_tag": "0x0000", + "r_frame_rate": "0/0", + "avg_frame_rate": "0/0", + "time_base": "1/1000", + "start_pts": 0, + "start_time": "0.000000", + "duration_ts": 1420016, + "duration": "1420.016000", + "disposition": { + "default": 0, + "dub": 0, + "original": 0, + "comment": 0, + "lyrics": 0, + "karaoke": 0, + "forced": 0, + "hearing_impaired": 0, + "visual_impaired": 0, + "clean_effects": 0, + "attached_pic": 0, + "timed_thumbnails": 0 + }, + "tags": { + "language": "rus", + "title": "Russian" + } + }, + { + "index": 10, + "codec_type": "attachment", + "codec_tag_string": "[0][0][0][0]", + "codec_tag": "0x0000", + "r_frame_rate": "0/0", + "avg_frame_rate": "0/0", + "time_base": "1/90000", + "start_pts": 0, + "start_time": "0.000000", + "duration_ts": 127801440, + "duration": "1420.016000", + "disposition": { + "default": 0, + "dub": 0, + "original": 0, + "comment": 0, + "lyrics": 0, + "karaoke": 0, + "forced": 0, + "hearing_impaired": 0, + "visual_impaired": 0, + "clean_effects": 0, + "attached_pic": 0, + "timed_thumbnails": 0 + }, + "tags": { + "filename": "arial.ttf", + "mimetype": "font/ttf" + } + }, + { + "index": 11, + "codec_type": "attachment", + "codec_tag_string": "[0][0][0][0]", + "codec_tag": "0x0000", + "r_frame_rate": "0/0", + "avg_frame_rate": "0/0", + "time_base": "1/90000", + "start_pts": 0, + "start_time": "0.000000", + "duration_ts": 127801440, + "duration": "1420.016000", + "disposition": { + "default": 0, + "dub": 0, + "original": 0, + "comment": 0, + "lyrics": 0, + "karaoke": 0, + "forced": 0, + "hearing_impaired": 0, + "visual_impaired": 0, + "clean_effects": 0, + "attached_pic": 0, + "timed_thumbnails": 0 + }, + "tags": { + "filename": "arialbd.ttf", + "mimetype": "font/ttf" + } + }, + { + "index": 12, + "codec_type": "attachment", + "codec_tag_string": "[0][0][0][0]", + "codec_tag": "0x0000", + "r_frame_rate": "0/0", + "avg_frame_rate": "0/0", + "time_base": "1/90000", + "start_pts": 0, + "start_time": "0.000000", + "duration_ts": 127801440, + "duration": "1420.016000", + "disposition": { + "default": 0, + "dub": 0, + "original": 0, + "comment": 0, + "lyrics": 0, + "karaoke": 0, + "forced": 0, + "hearing_impaired": 0, + "visual_impaired": 0, + "clean_effects": 0, + "attached_pic": 0, + "timed_thumbnails": 0 + }, + "tags": { + "filename": "arialbi.ttf", + "mimetype": "font/ttf" + } + }, + { + "index": 13, + "codec_type": "attachment", + "codec_tag_string": "[0][0][0][0]", + "codec_tag": "0x0000", + "r_frame_rate": "0/0", + "avg_frame_rate": "0/0", + "time_base": "1/90000", + "start_pts": 0, + "start_time": "0.000000", + "duration_ts": 127801440, + "duration": "1420.016000", + "disposition": { + "default": 0, + "dub": 0, + "original": 0, + "comment": 0, + "lyrics": 0, + "karaoke": 0, + "forced": 0, + "hearing_impaired": 0, + "visual_impaired": 0, + "clean_effects": 0, + "attached_pic": 0, + "timed_thumbnails": 0 + }, + "tags": { + "filename": "ariali.ttf", + "mimetype": "font/ttf" + } + }, + { + "index": 14, + "codec_type": "attachment", + "codec_tag_string": "[0][0][0][0]", + "codec_tag": "0x0000", + "r_frame_rate": "0/0", + "avg_frame_rate": "0/0", + "time_base": "1/90000", + "start_pts": 0, + "start_time": "0.000000", + "duration_ts": 127801440, + "duration": "1420.016000", + "disposition": { + "default": 0, + "dub": 0, + "original": 0, + "comment": 0, + "lyrics": 0, + "karaoke": 0, + "forced": 0, + "hearing_impaired": 0, + "visual_impaired": 0, + "clean_effects": 0, + "attached_pic": 0, + "timed_thumbnails": 0 + }, + "tags": { + "filename": "arialuni.ttf", + "mimetype": "font/ttf" + } + }, + { + "index": 15, + "codec_type": "attachment", + "codec_tag_string": "[0][0][0][0]", + "codec_tag": "0x0000", + "r_frame_rate": "0/0", + "avg_frame_rate": "0/0", + "time_base": "1/90000", + "start_pts": 0, + "start_time": "0.000000", + "duration_ts": 127801440, + "duration": "1420.016000", + "disposition": { + "default": 0, + "dub": 0, + "original": 0, + "comment": 0, + "lyrics": 0, + "karaoke": 0, + "forced": 0, + "hearing_impaired": 0, + "visual_impaired": 0, + "clean_effects": 0, + "attached_pic": 0, + "timed_thumbnails": 0 + }, + "tags": { + "filename": "ariblk.ttf", + "mimetype": "font/ttf" + } + }, + { + "index": 16, + "codec_type": "attachment", + "codec_tag_string": "[0][0][0][0]", + "codec_tag": "0x0000", + "r_frame_rate": "0/0", + "avg_frame_rate": "0/0", + "time_base": "1/90000", + "start_pts": 0, + "start_time": "0.000000", + "duration_ts": 127801440, + "duration": "1420.016000", + "disposition": { + "default": 0, + "dub": 0, + "original": 0, + "comment": 0, + "lyrics": 0, + "karaoke": 0, + "forced": 0, + "hearing_impaired": 0, + "visual_impaired": 0, + "clean_effects": 0, + "attached_pic": 0, + "timed_thumbnails": 0 + }, + "tags": { + "filename": "cour.ttf", + "mimetype": "font/ttf" + } + }, + { + "index": 17, + "codec_type": "attachment", + "codec_tag_string": "[0][0][0][0]", + "codec_tag": "0x0000", + "r_frame_rate": "0/0", + "avg_frame_rate": "0/0", + "time_base": "1/90000", + "start_pts": 0, + "start_time": "0.000000", + "duration_ts": 127801440, + "duration": "1420.016000", + "disposition": { + "default": 0, + "dub": 0, + "original": 0, + "comment": 0, + "lyrics": 0, + "karaoke": 0, + "forced": 0, + "hearing_impaired": 0, + "visual_impaired": 0, + "clean_effects": 0, + "attached_pic": 0, + "timed_thumbnails": 0 + }, + "tags": { + "filename": "courbd.ttf", + "mimetype": "font/ttf" + } + }, + { + "index": 18, + "codec_type": "attachment", + "codec_tag_string": "[0][0][0][0]", + "codec_tag": "0x0000", + "r_frame_rate": "0/0", + "avg_frame_rate": "0/0", + "time_base": "1/90000", + "start_pts": 0, + "start_time": "0.000000", + "duration_ts": 127801440, + "duration": "1420.016000", + "disposition": { + "default": 0, + "dub": 0, + "original": 0, + "comment": 0, + "lyrics": 0, + "karaoke": 0, + "forced": 0, + "hearing_impaired": 0, + "visual_impaired": 0, + "clean_effects": 0, + "attached_pic": 0, + "timed_thumbnails": 0 + }, + "tags": { + "filename": "courbi.ttf", + "mimetype": "font/ttf" + } + }, + { + "index": 19, + "codec_type": "attachment", + "codec_tag_string": "[0][0][0][0]", + "codec_tag": "0x0000", + "r_frame_rate": "0/0", + "avg_frame_rate": "0/0", + "time_base": "1/90000", + "start_pts": 0, + "start_time": "0.000000", + "duration_ts": 127801440, + "duration": "1420.016000", + "disposition": { + "default": 0, + "dub": 0, + "original": 0, + "comment": 0, + "lyrics": 0, + "karaoke": 0, + "forced": 0, + "hearing_impaired": 0, + "visual_impaired": 0, + "clean_effects": 0, + "attached_pic": 0, + "timed_thumbnails": 0 + }, + "tags": { + "filename": "couri.ttf", + "mimetype": "font/ttf" + } + }, + { + "index": 20, + "codec_type": "attachment", + "codec_tag_string": "[0][0][0][0]", + "codec_tag": "0x0000", + "r_frame_rate": "0/0", + "avg_frame_rate": "0/0", + "time_base": "1/90000", + "start_pts": 0, + "start_time": "0.000000", + "duration_ts": 127801440, + "duration": "1420.016000", + "disposition": { + "default": 0, + "dub": 0, + "original": 0, + "comment": 0, + "lyrics": 0, + "karaoke": 0, + "forced": 0, + "hearing_impaired": 0, + "visual_impaired": 0, + "clean_effects": 0, + "attached_pic": 0, + "timed_thumbnails": 0 + }, + "tags": { + "filename": "NotoSans-Medium.ttf", + "mimetype": "font/ttf" + } + }, + { + "index": 21, + "codec_type": "attachment", + "codec_tag_string": "[0][0][0][0]", + "codec_tag": "0x0000", + "r_frame_rate": "0/0", + "avg_frame_rate": "0/0", + "time_base": "1/90000", + "start_pts": 0, + "start_time": "0.000000", + "duration_ts": 127801440, + "duration": "1420.016000", + "disposition": { + "default": 0, + "dub": 0, + "original": 0, + "comment": 0, + "lyrics": 0, + "karaoke": 0, + "forced": 0, + "hearing_impaired": 0, + "visual_impaired": 0, + "clean_effects": 0, + "attached_pic": 0, + "timed_thumbnails": 0 + }, + "tags": { + "filename": "tahoma.ttf", + "mimetype": "font/ttf" + } + }, + { + "index": 22, + "codec_type": "attachment", + "codec_tag_string": "[0][0][0][0]", + "codec_tag": "0x0000", + "r_frame_rate": "0/0", + "avg_frame_rate": "0/0", + "time_base": "1/90000", + "start_pts": 0, + "start_time": "0.000000", + "duration_ts": 127801440, + "duration": "1420.016000", + "disposition": { + "default": 0, + "dub": 0, + "original": 0, + "comment": 0, + "lyrics": 0, + "karaoke": 0, + "forced": 0, + "hearing_impaired": 0, + "visual_impaired": 0, + "clean_effects": 0, + "attached_pic": 0, + "timed_thumbnails": 0 + }, + "tags": { + "filename": "times.ttf", + "mimetype": "font/ttf" + } + }, + { + "index": 23, + "codec_type": "attachment", + "codec_tag_string": "[0][0][0][0]", + "codec_tag": "0x0000", + "r_frame_rate": "0/0", + "avg_frame_rate": "0/0", + "time_base": "1/90000", + "start_pts": 0, + "start_time": "0.000000", + "duration_ts": 127801440, + "duration": "1420.016000", + "disposition": { + "default": 0, + "dub": 0, + "original": 0, + "comment": 0, + "lyrics": 0, + "karaoke": 0, + "forced": 0, + "hearing_impaired": 0, + "visual_impaired": 0, + "clean_effects": 0, + "attached_pic": 0, + "timed_thumbnails": 0 + }, + "tags": { + "filename": "timesbd.ttf", + "mimetype": "font/ttf" + } + }, + { + "index": 24, + "codec_type": "attachment", + "codec_tag_string": "[0][0][0][0]", + "codec_tag": "0x0000", + "r_frame_rate": "0/0", + "avg_frame_rate": "0/0", + "time_base": "1/90000", + "start_pts": 0, + "start_time": "0.000000", + "duration_ts": 127801440, + "duration": "1420.016000", + "disposition": { + "default": 0, + "dub": 0, + "original": 0, + "comment": 0, + "lyrics": 0, + "karaoke": 0, + "forced": 0, + "hearing_impaired": 0, + "visual_impaired": 0, + "clean_effects": 0, + "attached_pic": 0, + "timed_thumbnails": 0 + }, + "tags": { + "filename": "timesbi.ttf", + "mimetype": "font/ttf" + } + }, + { + "index": 25, + "codec_type": "attachment", + "codec_tag_string": "[0][0][0][0]", + "codec_tag": "0x0000", + "r_frame_rate": "0/0", + "avg_frame_rate": "0/0", + "time_base": "1/90000", + "start_pts": 0, + "start_time": "0.000000", + "duration_ts": 127801440, + "duration": "1420.016000", + "disposition": { + "default": 0, + "dub": 0, + "original": 0, + "comment": 0, + "lyrics": 0, + "karaoke": 0, + "forced": 0, + "hearing_impaired": 0, + "visual_impaired": 0, + "clean_effects": 0, + "attached_pic": 0, + "timed_thumbnails": 0 + }, + "tags": { + "filename": "timesi.ttf", + "mimetype": "font/ttf" + } + }, + { + "index": 26, + "codec_type": "attachment", + "codec_tag_string": "[0][0][0][0]", + "codec_tag": "0x0000", + "r_frame_rate": "0/0", + "avg_frame_rate": "0/0", + "time_base": "1/90000", + "start_pts": 0, + "start_time": "0.000000", + "duration_ts": 127801440, + "duration": "1420.016000", + "disposition": { + "default": 0, + "dub": 0, + "original": 0, + "comment": 0, + "lyrics": 0, + "karaoke": 0, + "forced": 0, + "hearing_impaired": 0, + "visual_impaired": 0, + "clean_effects": 0, + "attached_pic": 0, + "timed_thumbnails": 0 + }, + "tags": { + "filename": "trebuc.ttf", + "mimetype": "font/ttf" + } + }, + { + "index": 27, + "codec_type": "attachment", + "codec_tag_string": "[0][0][0][0]", + "codec_tag": "0x0000", + "r_frame_rate": "0/0", + "avg_frame_rate": "0/0", + "time_base": "1/90000", + "start_pts": 0, + "start_time": "0.000000", + "duration_ts": 127801440, + "duration": "1420.016000", + "disposition": { + "default": 0, + "dub": 0, + "original": 0, + "comment": 0, + "lyrics": 0, + "karaoke": 0, + "forced": 0, + "hearing_impaired": 0, + "visual_impaired": 0, + "clean_effects": 0, + "attached_pic": 0, + "timed_thumbnails": 0 + }, + "tags": { + "filename": "trebucbd.ttf", + "mimetype": "font/ttf" + } + }, + { + "index": 28, + "codec_type": "attachment", + "codec_tag_string": "[0][0][0][0]", + "codec_tag": "0x0000", + "r_frame_rate": "0/0", + "avg_frame_rate": "0/0", + "time_base": "1/90000", + "start_pts": 0, + "start_time": "0.000000", + "duration_ts": 127801440, + "duration": "1420.016000", + "disposition": { + "default": 0, + "dub": 0, + "original": 0, + "comment": 0, + "lyrics": 0, + "karaoke": 0, + "forced": 0, + "hearing_impaired": 0, + "visual_impaired": 0, + "clean_effects": 0, + "attached_pic": 0, + "timed_thumbnails": 0 + }, + "tags": { + "filename": "trebucbi.ttf", + "mimetype": "font/ttf" + } + }, + { + "index": 29, + "codec_type": "attachment", + "codec_tag_string": "[0][0][0][0]", + "codec_tag": "0x0000", + "r_frame_rate": "0/0", + "avg_frame_rate": "0/0", + "time_base": "1/90000", + "start_pts": 0, + "start_time": "0.000000", + "duration_ts": 127801440, + "duration": "1420.016000", + "disposition": { + "default": 0, + "dub": 0, + "original": 0, + "comment": 0, + "lyrics": 0, + "karaoke": 0, + "forced": 0, + "hearing_impaired": 0, + "visual_impaired": 0, + "clean_effects": 0, + "attached_pic": 0, + "timed_thumbnails": 0 + }, + "tags": { + "filename": "trebucit.ttf", + "mimetype": "font/ttf" + } + }, + { + "index": 30, + "codec_type": "attachment", + "codec_tag_string": "[0][0][0][0]", + "codec_tag": "0x0000", + "r_frame_rate": "0/0", + "avg_frame_rate": "0/0", + "time_base": "1/90000", + "start_pts": 0, + "start_time": "0.000000", + "duration_ts": 127801440, + "duration": "1420.016000", + "disposition": { + "default": 0, + "dub": 0, + "original": 0, + "comment": 0, + "lyrics": 0, + "karaoke": 0, + "forced": 0, + "hearing_impaired": 0, + "visual_impaired": 0, + "clean_effects": 0, + "attached_pic": 0, + "timed_thumbnails": 0 + }, + "tags": { + "filename": "verdana.ttf", + "mimetype": "font/ttf" + } + }, + { + "index": 31, + "codec_type": "attachment", + "codec_tag_string": "[0][0][0][0]", + "codec_tag": "0x0000", + "r_frame_rate": "0/0", + "avg_frame_rate": "0/0", + "time_base": "1/90000", + "start_pts": 0, + "start_time": "0.000000", + "duration_ts": 127801440, + "duration": "1420.016000", + "disposition": { + "default": 0, + "dub": 0, + "original": 0, + "comment": 0, + "lyrics": 0, + "karaoke": 0, + "forced": 0, + "hearing_impaired": 0, + "visual_impaired": 0, + "clean_effects": 0, + "attached_pic": 0, + "timed_thumbnails": 0 + }, + "tags": { + "filename": "verdanab.ttf", + "mimetype": "font/ttf" + } + }, + { + "index": 32, + "codec_type": "attachment", + "codec_tag_string": "[0][0][0][0]", + "codec_tag": "0x0000", + "r_frame_rate": "0/0", + "avg_frame_rate": "0/0", + "time_base": "1/90000", + "start_pts": 0, + "start_time": "0.000000", + "duration_ts": 127801440, + "duration": "1420.016000", + "disposition": { + "default": 0, + "dub": 0, + "original": 0, + "comment": 0, + "lyrics": 0, + "karaoke": 0, + "forced": 0, + "hearing_impaired": 0, + "visual_impaired": 0, + "clean_effects": 0, + "attached_pic": 0, + "timed_thumbnails": 0 + }, + "tags": { + "filename": "verdanai.ttf", + "mimetype": "font/ttf" + } + }, + { + "index": 33, + "codec_type": "attachment", + "codec_tag_string": "[0][0][0][0]", + "codec_tag": "0x0000", + "r_frame_rate": "0/0", + "avg_frame_rate": "0/0", + "time_base": "1/90000", + "start_pts": 0, + "start_time": "0.000000", + "duration_ts": 127801440, + "duration": "1420.016000", + "disposition": { + "default": 0, + "dub": 0, + "original": 0, + "comment": 0, + "lyrics": 0, + "karaoke": 0, + "forced": 0, + "hearing_impaired": 0, + "visual_impaired": 0, + "clean_effects": 0, + "attached_pic": 0, + "timed_thumbnails": 0 + }, + "tags": { + "filename": "verdanaz.ttf", + "mimetype": "font/ttf" + } + } + ] +} \ No newline at end of file diff --git a/Reader/src/test/resources/streams/sample5.json b/Reader/src/test/resources/streams/sample5.json new file mode 100644 index 00000000..67aa6a96 --- /dev/null +++ b/Reader/src/test/resources/streams/sample5.json @@ -0,0 +1,98 @@ +{ + "streams": [ + { + "index": 0, + "codec_name": "hevc", + "codec_long_name": "H.265 / HEVC (High Efficiency Video Coding)", + "profile": "Main 10", + "codec_type": "video", + "codec_tag_string": "hev1", + "codec_tag": "0x31766568", + "width": 1920, + "height": 960, + "coded_width": 1920, + "coded_height": 960, + "closed_captions": 0, + "has_b_frames": 2, + "sample_aspect_ratio": "1:1", + "display_aspect_ratio": "2:1", + "pix_fmt": "yuv420p10le", + "level": 120, + "color_range": "tv", + "chroma_location": "left", + "refs": 1, + "r_frame_rate": "24/1", + "avg_frame_rate": "24/1", + "time_base": "1/24000", + "start_pts": 0, + "start_time": "0.000000", + "duration_ts": 84883000, + "duration": "3536.791667", + "bit_rate": "1998078", + "nb_frames": "84883", + "disposition": { + "default": 1, + "dub": 0, + "original": 0, + "comment": 0, + "lyrics": 0, + "karaoke": 0, + "forced": 0, + "hearing_impaired": 0, + "visual_impaired": 0, + "clean_effects": 0, + "attached_pic": 0, + "timed_thumbnails": 0 + }, + "tags": { + "creation_time": "2022-05-19T19:59:17.000000Z", + "language": "und", + "handler_name": "VideoHandler", + "vendor_id": "[0][0][0][0]" + } + }, + { + "index": 1, + "codec_name": "aac", + "codec_long_name": "AAC (Advanced Audio Coding)", + "profile": "LC", + "codec_type": "audio", + "codec_tag_string": "mp4a", + "codec_tag": "0x6134706d", + "sample_fmt": "fltp", + "sample_rate": "48000", + "channels": 6, + "channel_layout": "5.1", + "bits_per_sample": 0, + "r_frame_rate": "0/0", + "avg_frame_rate": "0/0", + "time_base": "1/48000", + "start_pts": 0, + "start_time": "0.000000", + "duration_ts": 169766400, + "duration": "3536.800000", + "bit_rate": "224001", + "nb_frames": "165790", + "disposition": { + "default": 1, + "dub": 0, + "original": 0, + "comment": 0, + "lyrics": 0, + "karaoke": 0, + "forced": 0, + "hearing_impaired": 0, + "visual_impaired": 0, + "clean_effects": 0, + "attached_pic": 0, + "timed_thumbnails": 0 + }, + "tags": { + "creation_time": "2022-05-19T19:59:17.000000Z", + "language": "eng", + "handler_name": "SoundHandler", + "vendor_id": "[0][0][0][0]" + } + } + ] +} \ No newline at end of file diff --git a/Reader/src/test/resources/streams/sample6.json b/Reader/src/test/resources/streams/sample6.json new file mode 100644 index 00000000..dae6a8e7 --- /dev/null +++ b/Reader/src/test/resources/streams/sample6.json @@ -0,0 +1,193 @@ +{ + "streams": [ + { + "index": 0, + "codec_name": "hevc", + "codec_long_name": "H.265 / HEVC (High Efficiency Video Coding)", + "profile": "Main 10", + "codec_type": "video", + "codec_time_base": "1001/24000", + "codec_tag_string": "[0][0][0][0]", + "codec_tag": "0x0000", + "width": 1920, + "height": 1080, + "coded_width": 1920, + "coded_height": 1080, + "has_b_frames": 2, + "sample_aspect_ratio": "1:1", + "display_aspect_ratio": "16:9", + "pix_fmt": "yuv420p10le", + "level": 120, + "color_range": "tv", + "color_space": "bt709", + "color_transfer": "bt709", + "color_primaries": "bt709", + "refs": 1, + "r_frame_rate": "24000/1001", + "avg_frame_rate": "24000/1001", + "time_base": "1/1000", + "start_pts": 0, + "start_time": "0.000000", + "disposition": { + "default": 1, + "dub": 0, + "original": 0, + "comment": 0, + "lyrics": 0, + "karaoke": 0, + "forced": 0, + "hearing_impaired": 0, + "visual_impaired": 0, + "clean_effects": 0, + "attached_pic": 0, + "timed_thumbnails": 0 + }, + "tags": { + "title": "Presented By EMBER", + "BPS": "2438576", + "DURATION": "00:23:42.004000000", + "NUMBER_OF_FRAMES": "34094", + "NUMBER_OF_BYTES": "433458227", + "_STATISTICS_WRITING_APP": "mkvmerge v65.0.0 ('Too Much') 64-bit", + "_STATISTICS_WRITING_DATE_UTC": "2022-07-06 20:30:37", + "_STATISTICS_TAGS": "BPS DURATION NUMBER_OF_FRAMES NUMBER_OF_BYTES" + } + }, + { + "index": 1, + "codec_name": "eac3", + "codec_long_name": "ATSC A/52B (AC-3, E-AC-3)", + "codec_type": "audio", + "codec_time_base": "1/48000", + "codec_tag_string": "[0][0][0][0]", + "codec_tag": "0x0000", + "sample_fmt": "fltp", + "sample_rate": "48000", + "channels": 2, + "bits_per_sample": 0, + "dmix_mode": "-1", + "ltrt_cmixlev": "-1.000000", + "ltrt_surmixlev": "-1.000000", + "loro_cmixlev": "-1.000000", + "loro_surmixlev": "-1.000000", + "r_frame_rate": "0/0", + "avg_frame_rate": "0/0", + "time_base": "1/1000", + "start_pts": 0, + "start_time": "0.000000", + "disposition": { + "default": 0, + "dub": 0, + "original": 0, + "comment": 0, + "lyrics": 0, + "karaoke": 0, + "forced": 0, + "hearing_impaired": 0, + "visual_impaired": 0, + "clean_effects": 0, + "attached_pic": 0, + "timed_thumbnails": 0 + }, + "tags": { + "language": "jpn", + "BPS": "128000", + "DURATION": "00:23:42.112000000", + "NUMBER_OF_FRAMES": "44441", + "NUMBER_OF_BYTES": "22753792", + "_STATISTICS_WRITING_APP": "mkvmerge v65.0.0 ('Too Much') 64-bit", + "_STATISTICS_WRITING_DATE_UTC": "2022-07-06 20:30:37", + "_STATISTICS_TAGS": "BPS DURATION NUMBER_OF_FRAMES NUMBER_OF_BYTES" + } + }, + { + "index": 2, + "codec_name": "subrip", + "codec_long_name": "SubRip subtitle", + "codec_type": "subtitle", + "codec_time_base": "0/1", + "codec_tag_string": "[0][0][0][0]", + "codec_tag": "0x0000", + "r_frame_rate": "0/0", + "avg_frame_rate": "0/0", + "time_base": "1/1000", + "start_pts": 0, + "start_time": "0.000000", + "duration_ts": 1422112, + "duration": "1422.112000", + "disposition": { + "default": 1, + "dub": 0, + "original": 0, + "comment": 0, + "lyrics": 0, + "karaoke": 0, + "forced": 0, + "hearing_impaired": 0, + "visual_impaired": 0, + "clean_effects": 0, + "attached_pic": 0, + "timed_thumbnails": 0 + }, + "tags": { + "language": "eng", + "BPS": "65", + "DURATION": "00:23:25.487000000", + "NUMBER_OF_FRAMES": "342", + "NUMBER_OF_BYTES": "11595", + "_STATISTICS_WRITING_APP": "mkvmerge v65.0.0 ('Too Much') 64-bit", + "_STATISTICS_WRITING_DATE_UTC": "2022-07-06 20:30:37", + "_STATISTICS_TAGS": "BPS DURATION NUMBER_OF_FRAMES NUMBER_OF_BYTES" + } + }, + { + "index": 3, + "codec_name": "mjpeg", + "codec_long_name": "Motion JPEG", + "profile": "Progressive", + "codec_type": "video", + "codec_time_base": "0/1", + "codec_tag_string": "[0][0][0][0]", + "codec_tag": "0x0000", + "width": 400, + "height": 564, + "coded_width": 400, + "coded_height": 564, + "has_b_frames": 0, + "sample_aspect_ratio": "1:1", + "display_aspect_ratio": "100:141", + "pix_fmt": "yuvj420p", + "level": -99, + "color_range": "pc", + "color_space": "bt470bg", + "chroma_location": "center", + "refs": 1, + "r_frame_rate": "90000/1", + "avg_frame_rate": "0/0", + "time_base": "1/90000", + "start_pts": 0, + "start_time": "0.000000", + "duration_ts": 127990080, + "duration": "1422.112000", + "bits_per_raw_sample": "8", + "disposition": { + "default": 0, + "dub": 0, + "original": 0, + "comment": 0, + "lyrics": 0, + "karaoke": 0, + "forced": 0, + "hearing_impaired": 0, + "visual_impaired": 0, + "clean_effects": 0, + "attached_pic": 1, + "timed_thumbnails": 0 + }, + "tags": { + "filename": "cover.jpg", + "mimetype": "image/jpeg" + } + } + ] +} \ No newline at end of file diff --git a/Reader/src/test/resources/streams/sample7.json b/Reader/src/test/resources/streams/sample7.json new file mode 100644 index 00000000..14273089 --- /dev/null +++ b/Reader/src/test/resources/streams/sample7.json @@ -0,0 +1,205 @@ +{ + "streams": [ + { + "index": 0, + "codec_name": "hevc", + "codec_long_name": "H.265 / HEVC (High Efficiency Video Coding)", + "profile": "Main 10", + "codec_type": "video", + "codec_time_base": "1/25", + "codec_tag_string": "[0][0][0][0]", + "codec_tag": "0x0000", + "width": 1920, + "height": 952, + "coded_width": 1920, + "coded_height": 952, + "has_b_frames": 2, + "sample_aspect_ratio": "1:1", + "display_aspect_ratio": "240:119", + "pix_fmt": "yuv420p10le", + "level": 120, + "color_range": "tv", + "refs": 1, + "r_frame_rate": "25/1", + "avg_frame_rate": "25/1", + "time_base": "1/1000", + "start_pts": 0, + "start_time": "0.000000", + "disposition": { + "default": 1, + "dub": 0, + "original": 0, + "comment": 0, + "lyrics": 0, + "karaoke": 0, + "forced": 0, + "hearing_impaired": 0, + "visual_impaired": 0, + "clean_effects": 0, + "attached_pic": 0, + "timed_thumbnails": 0 + }, + "tags": { + "BPS": "3698552", + "BPS-eng": "3698552", + "DURATION": "00:43:59.240000000", + "DURATION-eng": "00:43:59.240000000", + "NUMBER_OF_FRAMES": "65981", + "NUMBER_OF_FRAMES-eng": "65981", + "NUMBER_OF_BYTES": "1220170846", + "NUMBER_OF_BYTES-eng": "1220170846", + "_STATISTICS_WRITING_APP": "mkvmerge v17.0.0 ('Be Ur Friend') 64-bit", + "_STATISTICS_WRITING_APP-eng": "mkvmerge v17.0.0 ('Be Ur Friend') 64-bit", + "_STATISTICS_WRITING_DATE_UTC": "2019-05-21 18:17:28", + "_STATISTICS_WRITING_DATE_UTC-eng": "2019-05-21 18:17:28", + "_STATISTICS_TAGS": "BPS DURATION NUMBER_OF_FRAMES NUMBER_OF_BYTES", + "_STATISTICS_TAGS-eng": "BPS DURATION NUMBER_OF_FRAMES NUMBER_OF_BYTES" + } + }, + { + "index": 1, + "codec_name": "aac", + "codec_long_name": "AAC (Advanced Audio Coding)", + "profile": "LC", + "codec_type": "audio", + "codec_time_base": "1/48000", + "codec_tag_string": "[0][0][0][0]", + "codec_tag": "0x0000", + "sample_fmt": "fltp", + "sample_rate": "48000", + "channels": 2, + "channel_layout": "stereo", + "bits_per_sample": 0, + "r_frame_rate": "0/0", + "avg_frame_rate": "0/0", + "time_base": "1/1000", + "start_pts": 20, + "start_time": "0.020000", + "disposition": { + "default": 1, + "dub": 0, + "original": 0, + "comment": 0, + "lyrics": 0, + "karaoke": 0, + "forced": 0, + "hearing_impaired": 0, + "visual_impaired": 0, + "clean_effects": 0, + "attached_pic": 0, + "timed_thumbnails": 0 + }, + "tags": { + "language": "nor", + "BPS": "152584", + "BPS-eng": "152584", + "DURATION": "00:43:58.250000000", + "DURATION-eng": "00:43:58.250000000", + "NUMBER_OF_FRAMES": "123668", + "NUMBER_OF_FRAMES-eng": "123668", + "NUMBER_OF_BYTES": "50319602", + "NUMBER_OF_BYTES-eng": "50319602", + "_STATISTICS_WRITING_APP": "mkvmerge v17.0.0 ('Be Ur Friend') 64-bit", + "_STATISTICS_WRITING_APP-eng": "mkvmerge v17.0.0 ('Be Ur Friend') 64-bit", + "_STATISTICS_WRITING_DATE_UTC": "2019-05-21 18:17:28", + "_STATISTICS_WRITING_DATE_UTC-eng": "2019-05-21 18:17:28", + "_STATISTICS_TAGS": "BPS DURATION NUMBER_OF_FRAMES NUMBER_OF_BYTES", + "_STATISTICS_TAGS-eng": "BPS DURATION NUMBER_OF_FRAMES NUMBER_OF_BYTES" + } + }, + { + "index": 2, + "codec_name": "subrip", + "codec_long_name": "SubRip subtitle", + "codec_type": "subtitle", + "codec_time_base": "0/1", + "codec_tag_string": "[0][0][0][0]", + "codec_tag": "0x0000", + "r_frame_rate": "0/0", + "avg_frame_rate": "0/0", + "time_base": "1/1000", + "start_pts": 0, + "start_time": "0.000000", + "duration_ts": 2639240, + "duration": "2639.240000", + "disposition": { + "default": 0, + "dub": 0, + "original": 0, + "comment": 0, + "lyrics": 0, + "karaoke": 0, + "forced": 0, + "hearing_impaired": 0, + "visual_impaired": 0, + "clean_effects": 0, + "attached_pic": 0, + "timed_thumbnails": 0 + }, + "tags": { + "language": "eng", + "BPS": "21", + "BPS-eng": "21", + "DURATION": "00:43:00.840000000", + "DURATION-eng": "00:43:00.840000000", + "NUMBER_OF_FRAMES": "197", + "NUMBER_OF_FRAMES-eng": "197", + "NUMBER_OF_BYTES": "6798", + "NUMBER_OF_BYTES-eng": "6798", + "_STATISTICS_WRITING_APP": "mkvmerge v17.0.0 ('Be Ur Friend') 64-bit", + "_STATISTICS_WRITING_APP-eng": "mkvmerge v17.0.0 ('Be Ur Friend') 64-bit", + "_STATISTICS_WRITING_DATE_UTC": "2019-05-21 18:17:28", + "_STATISTICS_WRITING_DATE_UTC-eng": "2019-05-21 18:17:28", + "_STATISTICS_TAGS": "BPS DURATION NUMBER_OF_FRAMES NUMBER_OF_BYTES", + "_STATISTICS_TAGS-eng": "BPS DURATION NUMBER_OF_FRAMES NUMBER_OF_BYTES" + } + }, + { + "index": 3, + "codec_name": "subrip", + "codec_long_name": "SubRip subtitle", + "codec_type": "subtitle", + "codec_time_base": "0/1", + "codec_tag_string": "[0][0][0][0]", + "codec_tag": "0x0000", + "r_frame_rate": "0/0", + "avg_frame_rate": "0/0", + "time_base": "1/1000", + "start_pts": 0, + "start_time": "0.000000", + "duration_ts": 2639240, + "duration": "2639.240000", + "disposition": { + "default": 0, + "dub": 0, + "original": 0, + "comment": 0, + "lyrics": 0, + "karaoke": 0, + "forced": 0, + "hearing_impaired": 0, + "visual_impaired": 0, + "clean_effects": 0, + "attached_pic": 0, + "timed_thumbnails": 0 + }, + "tags": { + "language": "dan", + "BPS": "37", + "BPS-eng": "37", + "DURATION": "00:43:20.306000000", + "DURATION-eng": "00:43:20.306000000", + "NUMBER_OF_FRAMES": "276", + "NUMBER_OF_FRAMES-eng": "276", + "NUMBER_OF_BYTES": "12172", + "NUMBER_OF_BYTES-eng": "12172", + "_STATISTICS_WRITING_APP": "mkvmerge v17.0.0 ('Be Ur Friend') 64-bit", + "_STATISTICS_WRITING_APP-eng": "mkvmerge v17.0.0 ('Be Ur Friend') 64-bit", + "_STATISTICS_WRITING_DATE_UTC": "2019-05-21 18:17:28", + "_STATISTICS_WRITING_DATE_UTC-eng": "2019-05-21 18:17:28", + "_STATISTICS_TAGS": "BPS DURATION NUMBER_OF_FRAMES NUMBER_OF_BYTES", + "_STATISTICS_TAGS-eng": "BPS DURATION NUMBER_OF_FRAMES NUMBER_OF_BYTES" + } + } + ] +} \ No newline at end of file diff --git a/UI/.gitignore b/UI/.gitignore new file mode 100644 index 00000000..b63da455 --- /dev/null +++ b/UI/.gitignore @@ -0,0 +1,42 @@ +.gradle +build/ +!gradle/wrapper/gradle-wrapper.jar +!**/src/main/**/build/ +!**/src/test/**/build/ + +### IntelliJ IDEA ### +.idea/modules.xml +.idea/jarRepositories.xml +.idea/compiler.xml +.idea/libraries/ +*.iws +*.iml +*.ipr +out/ +!**/src/main/**/out/ +!**/src/test/**/out/ + +### Eclipse ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache +bin/ +!**/src/main/**/bin/ +!**/src/test/**/bin/ + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ + +### VS Code ### +.vscode/ + +### Mac OS ### +.DS_Store \ No newline at end of file diff --git a/UI/build.gradle.kts b/UI/build.gradle.kts new file mode 100644 index 00000000..5b2cc572 --- /dev/null +++ b/UI/build.gradle.kts @@ -0,0 +1,19 @@ +plugins { + id("java") +} + +group = "no.iktdev.streamit.content" +version = "1.0-SNAPSHOT" + +repositories { + mavenCentral() +} + +dependencies { + testImplementation(platform("org.junit:junit-bom:5.9.1")) + testImplementation("org.junit.jupiter:junit-jupiter") +} + +tasks.test { + useJUnitPlatform() +} \ No newline at end of file diff --git a/UI/gradle/wrapper/gradle-wrapper.jar b/UI/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 00000000..249e5832 Binary files /dev/null and b/UI/gradle/wrapper/gradle-wrapper.jar differ diff --git a/UI/gradle/wrapper/gradle-wrapper.properties b/UI/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 00000000..b6881dae --- /dev/null +++ b/UI/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Sat Jul 15 22:33:37 CEST 2023 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.0-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/UI/gradlew b/UI/gradlew new file mode 100644 index 00000000..1b6c7873 --- /dev/null +++ b/UI/gradlew @@ -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" "$@" diff --git a/UI/gradlew.bat b/UI/gradlew.bat new file mode 100644 index 00000000..107acd32 --- /dev/null +++ b/UI/gradlew.bat @@ -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 diff --git a/UI/settings.gradle.kts b/UI/settings.gradle.kts new file mode 100644 index 00000000..b7d79bfd --- /dev/null +++ b/UI/settings.gradle.kts @@ -0,0 +1,2 @@ +rootProject.name = "UI" + diff --git a/UI/src/main/java/no/iktdev/streamit/content/Main.java b/UI/src/main/java/no/iktdev/streamit/content/Main.java new file mode 100644 index 00000000..e38173bf --- /dev/null +++ b/UI/src/main/java/no/iktdev/streamit/content/Main.java @@ -0,0 +1,7 @@ +package no.iktdev.streamit.content; + +public class Main { + public static void main(String[] args) { + System.out.println("Hello world!"); + } +} \ No newline at end of file diff --git a/pyMetadata/app.py b/pyMetadata/app.py new file mode 100644 index 00000000..787059a6 --- /dev/null +++ b/pyMetadata/app.py @@ -0,0 +1,178 @@ +import signal +import sys, os, uuid +import threading +import json +from kafka import KafkaConsumer, KafkaProducer +from fuzzywuzzy import fuzz +from sources.result import Result, Metadata +from sources.anii import metadata as AniiMetadata +from sources.imdb import metadata as ImdbMetadata + +# Konfigurer Kafka-forbindelsen +bootstrap_servers = os.environ.get("KAFKA_BOOTSTRAP_SERVER") if os.environ.get("KAFKA_BOOTSTRAP_SERVER") != None else "127.0.0.1:9092" +consumer_group = os.environ.get("KAFKA_CONSUMER_ID") if os.environ.get("KAFKA_CONSUMER_ID") != None else f"Metadata-{uuid.uuid4()}" +kafka_topic = os.environ.get("KAFKA_BOOTSTRAP_SERVER") if os.environ.get("KAFKA_BOOTSTRAP_SERVER") != None else "127.0.0.1:9092" + +class ProducerDataValueSchema: + def __init__(self, referenceId, statusType, errorMessage, data): + self.referenceId = referenceId + self.statusType = statusType + self.errorMessage = errorMessage + self.data = data + + def to_dict(self): + return { + 'referenceId': self.referenceId, + 'status': { + 'statusType': self.statusType, + 'errorMessage': self.errorMessage + }, + 'data': self.data.to_dict() if self.data else None + } + + def to_json(self): + data_dict = self.to_dict() + return json.dumps(data_dict) + + @classmethod + def from_dict(cls, data_dict): + referenceId = data_dict.get('referenceId') + statusType = data_dict['status'].get('statusType') + errorMessage = data_dict['status'].get('errorMessage') + data = data_dict.get('data') + + return cls(referenceId, statusType, errorMessage, data) + + + +# Kafka consumer-klasse +class KafkaConsumerThread(threading.Thread): + def __init__(self, bootstrap_servers, topic): + super().__init__() + self.bootstrap_servers = bootstrap_servers + self.topic = topic + self.shutdown = threading.Event() + + def run(self): + consumer = KafkaConsumer(self.topic, bootstrap_servers=self.bootstrap_servers) + + while not self.shutdown.is_set(): + for message in consumer: + if self.shutdown.is_set(): + break + + # Sjekk om meldingen har målnøkkelen + if message.key == "request:metadata:obtain" or message.key == "event:reader:received-file": + # Opprett en ny tråd for å håndtere meldingen + handler_thread = MessageHandlerThread(message) + handler_thread.start() + + consumer.close() + + def stop(self): + self.shutdown.set() + +# Kafka message handler-klasse +class MessageHandlerThread(threading.Thread): + def __init__(self, message): + super().__init__() + self.message = message + + def run(self): + # Deserialiser meldingsverdien fra JSON til et Python-dictionary + message_value = json.loads(self.message.value) + + # Sjekk om meldingen har en Status + if 'status' in message_value: + status_type = message_value['status']['statusType'] + + # Sjekk om statusen er SUCCESS + if status_type == 'SUCCESS': + data_value = message_value['data']["title"] + + # Utfør handlingen basert på verdien + result = self.perform_action(title=data_value) + + producerMessage = self.compose_message(referenceId=message_value["referenceId"], result=result) + + # Serialiser resultatet til JSON + result_json = json.dumps(producerMessage.to_json()) + + # Send resultatet tilbake ved hjelp av Kafka-producer + producer = KafkaProducer(bootstrap_servers=bootstrap_servers) + producer.send(kafka_topic, key="event:metadata:obtained", value=result_json) + producer.close() + + def perform_action(self, title) -> Result: + anii = AniiMetadata(title) + imdb = ImdbMetadata(title) + + anii_result = anii.lookup() + imdb_result = imdb.lookup() + + # Sammenlign resultater basert på likheter og sammenhenger med tittelen + if anii_result.statusType == "SUCCESS" and imdb_result.statusType == "SUCCESS": + # Begge registrene ga suksessresultater, bruk fuzzy matching for å gjøre en vurdering + title_similarity_anii = fuzz.ratio(title.lower(), anii_result.data.title.lower()) + title_similarity_imdb = fuzz.ratio(title.lower(), imdb_result.data.title.lower()) + + # Sammenlign likheter mellom tittel og registertitler + if title_similarity_anii > title_similarity_imdb: + most_likely_result = anii_result + else: + most_likely_result = imdb_result + + elif anii_result.statusType == "SUCCESS": + # AniList ga suksessresultat, bruk det som det mest sannsynlige + most_likely_result = anii_result + + elif imdb_result.statusType == "SUCCESS": + # IMDb ga suksessresultat, bruk det som det mest sannsynlige + most_likely_result = imdb_result + + else: + # Begge registrene feilet, håndter etter eget behov + most_likely_result = Result(statusType="ERROR", errorMessage="No Result") + + # Returner det mest sannsynlige resultatet + return most_likely_result + + + def compose_message(self, referenceId: str, result: Result) -> ProducerDataValueSchema: + """""" + return ProducerDataValueSchema( + referenceId=referenceId, + statusType=result.statusType, + errorMessage=result.errorMessage, + data=result.data + ) + + + +# Global variabel for å indikere om applikasjonen skal avsluttes +should_stop = False + +# Signalhåndteringsfunksjon +def signal_handler(sig, frame): + global should_stop + should_stop = True + +# Hovedprogrammet +def main(): + # Angi signalhåndterer for å fange opp SIGINT (Ctrl+C) + signal.signal(signal.SIGINT, signal_handler) + + # Opprett og start consumer-tråden + consumer_thread = KafkaConsumerThread(bootstrap_servers, kafka_topic) + consumer_thread.start() + + # Vent til should_stop er satt til True for å avslutte applikasjonen + while not should_stop: + pass + + # Stopp consumer-tråden + consumer_thread.stop() + consumer_thread.join() + +if __name__ == '__main__': + main() \ No newline at end of file diff --git a/pyMetadata/requirments.txt b/pyMetadata/requirments.txt new file mode 100644 index 00000000..f0f85313 --- /dev/null +++ b/pyMetadata/requirments.txt @@ -0,0 +1,4 @@ +cinemagoer>=2023.5.1 +AnilistPython>=0.1.3 +kafka-python>=2.0.2 +fuzzywuzzy>=0.18.0 \ No newline at end of file diff --git a/pyMetadata/sources/anii.py b/pyMetadata/sources/anii.py new file mode 100644 index 00000000..5832bffe --- /dev/null +++ b/pyMetadata/sources/anii.py @@ -0,0 +1,33 @@ +from AnilistPython import Anilist +from result import Metadata, Result + +class metadata(): + name: str = None + anilist = Anilist() + + def __init__(self, name) -> None: + self.name = name + + def lookup(self) -> Result: + """""" + try: + result = self.anilist.get_anime(self.name) + meta = Metadata() + meta.title = result.get("name_english", None) + meta.altTitle = result.get("name_romaji", None) + meta.cover = result.get("cover_image", None) + meta.summary = result.get("desc", None) + + airing_format = result.get('airing_format', '').lower() + if airing_format == 'movie': + meta.type = 'movie' + else: + meta.type = 'serie' + meta.genres = result.get('genres', []) + return Result("SUCCESS", None, meta) + + except IndexError as ingore: + return Result(statusType="IGNORE", errorMessage=f"No result for {self.name}") + except Exception as e: + return Result(statusType="ERROR", errorMessage=str(e)) + \ No newline at end of file diff --git a/pyMetadata/sources/imdb.py b/pyMetadata/sources/imdb.py new file mode 100644 index 00000000..7ea77095 --- /dev/null +++ b/pyMetadata/sources/imdb.py @@ -0,0 +1,34 @@ +import imdb +from result import Metadata, Result + +class metadata(): + name: str = None + imdb = imdb.Cinemagoer() + + def __init__(self, name) -> None: + self.name = name + + + def lookup(self) -> Result: + """""" + try: + query = self.imdb.search_movie(self.name) + imdbId = query[0].movieID + result = self.imdb.get_movie(imdbId) + meta = Metadata() + meta.title = result.get("title", None) + meta.altTitle = result.get("localized title", None) + meta.cover = result.get("cover url", None) + meta.summary = result.get("plot outline", None) + + airing_format = result.get('kind', '').lower() + if airing_format == 'movie': + meta.type = 'movie' + else: + meta.type = 'serie' + + meta.genres = result.get('genres', []) + + return Result("SUCCESS", None, meta) + except Exception as e: + return Result(statusType="ERROR", errorMessage=str(e)) \ No newline at end of file diff --git a/pyMetadata/sources/result.py b/pyMetadata/sources/result.py new file mode 100644 index 00000000..b244c6bb --- /dev/null +++ b/pyMetadata/sources/result.py @@ -0,0 +1,30 @@ +from typing import List, Optional +from dataclasses import dataclass, asdict + +@dataclass +class Metadata: + title: str + altTitle: str + cover: str + type: str # Serie/Movie + summary: str + genres: List[str] + +@dataclass +class Result: + statusType: str + errorMessage: str + data: Metadata + + def to_dict(self): + return asdict(self) + + @classmethod + def from_dict(cls, data_dict): + metadata_dict = data_dict.get('data') + metadata = Metadata(**metadata_dict) if metadata_dict else None + return cls( + statusType=data_dict['statusType'], + errorMessage=data_dict['errorMessage'], + data=metadata + )