This commit is contained in:
bskjon 2024-12-23 22:01:06 +01:00
parent 49a88bcbaf
commit 938a5c7ee8
35 changed files with 1311 additions and 528 deletions

295
.github/workflows/v4.yml vendored Normal file
View File

@ -0,0 +1,295 @@
name: Build v4
on:
push:
branches:
- v4
pull_request:
branches:
- v4
workflow_dispatch:
jobs:
pre-check:
runs-on: ubuntu-latest
outputs:
pyMetadata: ${{ steps.filter.outputs.pyMetadata }}
coordinator: ${{ steps.filter.outputs.coordinator }}
processer: ${{ steps.filter.outputs.processer }}
converter: ${{ steps.filter.outputs.converter }}
shared: ${{ steps.filter.outputs.shared }}
steps:
- name: Checkout repository
uses: actions/checkout@v2
- uses: dorny/paths-filter@v2
id: filter
with:
filters: |
pyMetadata:
- 'apps/pyMetadata/**'
apps/coordinator:
- 'apps/coordinator/**'
apps/processer:
- 'apps/processer/**'
apps/converter:
- 'apps/converter/**'
shared:
- 'shared/**'
# Step to print the outputs from "pre-check" job
- name: Print Outputs from pre-check job
run: |
echo "Apps\n"
echo "app:pyMetadata: ${{ needs.pre-check.outputs.pyMetadata }}"
echo "app:coordinator: ${{ needs.pre-check.outputs.coordinator }}"
echo "app:processer: ${{ needs.pre-check.outputs.processer }}"
echo "app:converter: ${{ needs.pre-check.outputs.converter }}"
echo "Shared"
echo "shared: ${{ needs.pre-check.outputs.shared }}"
echo "\n"
echo "${{ needs.pre-check.outputs }}"
echo "${{ needs.pre-check }}"
build-shared:
runs-on: ubuntu-latest
needs: pre-check
steps:
- name: Checkout repository
uses: actions/checkout@v2
- name: Cache Shared code Gradle dependencies
id: cache-gradle
uses: actions/cache@v2
with:
path: ~/.gradle/caches
key: ${{ runner.os }}-gradle-${{ hashFiles('shared/build.gradle.kts') }}
- name: Build Shared code
if: steps.cache-gradle.outputs.cache-hit != 'true' || needs.pre-check.outputs.shared == 'true' || github.event_name == 'workflow_dispatch'
run: |
chmod +x ./gradlew
./gradlew :shared:build --stacktrace --info
build-processer:
needs: build-shared
if: ${{ needs.pre-check.outputs.processer == 'true' || github.event_name == 'workflow_dispatch' || needs.pre-check.outputs.shared == 'true' }}
runs-on: ubuntu-latest
#if: ${{ github.event_name == 'push' || github.event_name == 'workflow_dispatch' }}
steps:
- name: Checkout repository
uses: actions/checkout@v2
- name: Cache Shared Gradle dependencies
id: cache-gradle
uses: actions/cache@v2
with:
path: ~/.gradle/caches
key: ${{ runner.os }}-gradle-${{ hashFiles('shared/build.gradle.kts') }}
- name: Extract version from build.gradle.kts
id: extract_version
run: |
VERSION=$(cat ./apps/processer/build.gradle.kts | grep '^version\s*=\s*\".*\"' | sed 's/^version\s*=\s*\"\(.*\)\"/\1/')
echo "VERSION=$VERSION"
echo "VERSION=$VERSION" >> $GITHUB_ENV
- name: Build Processer module
id: build-processer
run: |
chmod +x ./gradlew
./gradlew :apps:processer:bootJar --info --stacktrace
echo "Build completed"
- name: Generate Docker image tag
id: docker-tag
run: echo "::set-output name=tag::$(date -u +'%Y.%m.%d')-$(uuidgen | cut -c 1-8)"
- name: Docker login
uses: docker/login-action@f054a8b539a109f9f41c372932f1ae047eff08c9
with:
username: ${{ secrets.DOCKER_HUB_NAME }}
password: ${{ secrets.DOCKER_HUB_TOKEN }}
- name: Build and push Docker image
uses: docker/build-push-action@v2
with:
context: .
file: ./dockerfiles/DebianJavaFfmpeg
build-args: |
MODULE_NAME=processer
PASS_APP_VERSION=${{ env.VERSION }}
push: true
tags: |
bskjon/mediaprocessing-processer:v4
bskjon/mediaprocessing-processer:v4-${{ github.sha }}
bskjon/mediaprocessing-processer:v4-${{ steps.docker-tag.outputs.tag }}
build-converter:
needs: build-shared
if: ${{ needs.pre-check.outputs.converter == 'true' || github.event_name == 'workflow_dispatch' || needs.pre-check.outputs.shared == 'true' }}
runs-on: ubuntu-latest
#if: ${{ github.event_name == 'push' || github.event_name == 'workflow_dispatch' }}
steps:
- name: Checkout repository
uses: actions/checkout@v3
- name: Cache Shared Gradle dependencies
id: cache-gradle
uses: actions/cache@v3
with:
path: ~/.gradle/caches
key: ${{ runner.os }}-gradle-${{ hashFiles('shared/build.gradle.kts') }}
- name: Extract version from build.gradle.kts
id: extract_version
run: |
VERSION=$(cat ./apps/converter/build.gradle.kts | grep '^version\s*=\s*\".*\"' | sed 's/^version\s*=\s*\"\(.*\)\"/\1/')
echo "VERSION=$VERSION"
echo "VERSION=$VERSION" >> $GITHUB_ENV
- name: Build Converter module
id: build-converter
run: |
chmod +x ./gradlew
./gradlew :apps:converter:bootJar --info --debug
echo "Build completed"
- name: Generate Docker image tag
id: docker-tag
run: echo "::set-output name=tag::$(date -u +'%Y.%m.%d')-$(uuidgen | cut -c 1-8)"
- name: Docker login
uses: docker/login-action@f054a8b539a109f9f41c372932f1ae047eff08c9
with:
username: ${{ secrets.DOCKER_HUB_NAME }}
password: ${{ secrets.DOCKER_HUB_TOKEN }}
- name: Build and push Docker image
uses: docker/build-push-action@v2
with:
context: .
file: ./dockerfiles/DebianJava
build-args: |
MODULE_NAME=converter
PASS_APP_VERSION=${{ env.VERSION }}
push: true
tags: |
bskjon/mediaprocessing-converter:v4
bskjon/mediaprocessing-converter:v4-${{ github.sha }}
bskjon/mediaprocessing-converter:v4-${{ steps.docker-tag.outputs.tag }}
build-coordinator:
needs: build-shared
if: ${{ needs.pre-check.outputs.coordinator == 'true' || github.event_name == 'workflow_dispatch' || needs.pre-check.outputs.shared == 'true' }}
runs-on: ubuntu-latest
#if: ${{ github.event_name == 'push' || github.event_name == 'workflow_dispatch' }}
steps:
- name: Checkout repository
uses: actions/checkout@v2
- name: Cache Shared Gradle dependencies
id: cache-gradle
uses: actions/cache@v2
with:
path: ~/.gradle/caches
key: ${{ runner.os }}-gradle-${{ hashFiles('shared/build.gradle.kts') }}
- name: Extract version from build.gradle.kts
id: extract_version
run: |
VERSION=$(cat ./apps/coordinator/build.gradle.kts | grep '^version\s*=\s*\".*\"' | sed 's/^version\s*=\s*\"\(.*\)\"/\1/')
echo "VERSION=$VERSION"
echo "VERSION=$VERSION" >> $GITHUB_ENV
- name: Build Coordinator module
id: build-coordinator
run: |
chmod +x ./gradlew
./gradlew :apps:coordinator:bootJar
echo "Build completed"
- name: Generate Docker image tag
id: docker-tag
run: echo "::set-output name=tag::$(date -u +'%Y.%m.%d')-$(uuidgen | cut -c 1-8)"
- name: Docker login
uses: docker/login-action@f054a8b539a109f9f41c372932f1ae047eff08c9
with:
username: ${{ secrets.DOCKER_HUB_NAME }}
password: ${{ secrets.DOCKER_HUB_TOKEN }}
- name: Debug Check extracted version
run: |
echo "Extracted version: ${{ env.VERSION }}"
- name: Build and push Docker image
uses: docker/build-push-action@v2
with:
context: .
file: ./dockerfiles/DebianJavaFfmpeg
build-args: |
MODULE_NAME=coordinator
PASS_APP_VERSION=${{ env.VERSION }}
push: true
tags: |
bskjon/mediaprocessing-coordinator:v4
bskjon/mediaprocessing-coordinator:v4-${{ github.sha }}
bskjon/mediaprocessing-coordinator:v4-${{ steps.docker-tag.outputs.tag }}
build-pymetadata:
needs: pre-check
if: ${{ needs.pre-check.outputs.pyMetadata == 'true' || github.event_name == 'workflow_dispatch' }}
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v2
- name: Build pyMetadata module
id: build-pymetadata
run: |
if [[ "${{ steps.check-pymetadata.outputs.changed }}" == "true" || "${{ github.event_name }}" == "push" || "${{ github.event_name }}" == "workflow_dispatch" ]]; then
cd apps/pyMetadata
# Add the necessary build steps for your Python module here
echo "Build completed"
else
echo "pyMetadata has not changed. Skipping pyMetadata module build."
echo "::set-output name=job_skipped::true"
fi
- name: Generate Docker image tag
id: docker-tag
run: echo "::set-output name=tag::$(date -u +'%Y.%m.%d')-$(uuidgen | cut -c 1-8)"
- name: Docker login
uses: docker/login-action@f054a8b539a109f9f41c372932f1ae047eff08c9
with:
username: ${{ secrets.DOCKER_HUB_NAME }}
password: ${{ secrets.DOCKER_HUB_TOKEN }}
- name: Build and push Docker image
uses: docker/build-push-action@v5.1.0
with:
context: .
file: ./dockerfiles/Python
build-args:
MODULE_NAME=pyMetadata
push: true
tags: |
bskjon/mediaprocessing-pymetadata:v4
bskjon/mediaprocessing-pymetadata:v4-${{ github.sha }}
bskjon/mediaprocessing-pymetadata:v4-${{ steps.docker-tag.outputs.tag }}

399
.idea/workspace.xml generated
View File

@ -4,37 +4,44 @@
<option name="autoReloadType" value="SELECTIVE" /> <option name="autoReloadType" value="SELECTIVE" />
</component> </component>
<component name="ChangeListManager"> <component name="ChangeListManager">
<list default="true" id="79fc3e25-8083-4c15-b17f-a1e0d61c77a6" name="Changes" comment=""> <list default="true" id="79fc3e25-8083-4c15-b17f-a1e0d61c77a6" name="Changes" comment="Closing connection after every query">
<change afterPath="$PROJECT_DIR$/apps/ui/src/main/kotlin/no/iktdev/mediaprocessing/ui/dto/EventChain.kt" afterDir="false" /> <change afterPath="$PROJECT_DIR$/apps/coordinator/src/main/kotlin/no/iktdev/mediaprocessing/coordinator/tasksV2/mapping/store/ContentCatalogStore.kt" afterDir="false" />
<change afterPath="$PROJECT_DIR$/apps/ui/src/main/kotlin/no/iktdev/mediaprocessing/ui/service/AvailableEventsService.kt" afterDir="false" /> <change afterPath="$PROJECT_DIR$/apps/coordinator/src/main/kotlin/no/iktdev/mediaprocessing/coordinator/tasksV2/mapping/store/ContentCompletionMover.kt" afterDir="false" />
<change afterPath="$PROJECT_DIR$/apps/ui/src/main/kotlin/no/iktdev/mediaprocessing/ui/service/CompletedEventsService.kt" afterDir="false" /> <change afterPath="$PROJECT_DIR$/apps/coordinator/src/main/kotlin/no/iktdev/mediaprocessing/coordinator/tasksV2/mapping/store/ContentGenresStore.kt" afterDir="false" />
<change afterPath="$PROJECT_DIR$/apps/ui/src/main/kotlin/no/iktdev/mediaprocessing/ui/service/EventExecutionOrderService.kt" afterDir="false" /> <change afterPath="$PROJECT_DIR$/apps/coordinator/src/main/kotlin/no/iktdev/mediaprocessing/coordinator/tasksV2/mapping/store/ContentMetadataStore.kt" afterDir="false" />
<change afterPath="$PROJECT_DIR$/apps/ui/src/main/kotlin/no/iktdev/mediaprocessing/ui/socket/ChainedEventsTopic.kt" afterDir="false" /> <change afterPath="$PROJECT_DIR$/apps/coordinator/src/main/kotlin/no/iktdev/mediaprocessing/coordinator/tasksV2/mapping/store/ContentSubtitleStore.kt" afterDir="false" />
<change afterPath="$PROJECT_DIR$/shared/common/src/main/kotlin/no/iktdev/mediaprocessing/shared/common/database/EventsDatabase.kt" afterDir="false" /> <change afterPath="$PROJECT_DIR$/apps/coordinator/src/main/kotlin/no/iktdev/mediaprocessing/coordinator/tasksV2/mapping/store/ContentTitleStore.kt" afterDir="false" />
<change beforePath="$PROJECT_DIR$/.idea/gradle.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/gradle.xml" afterDir="false" /> <change afterPath="$PROJECT_DIR$/apps/coordinator/src/main/kotlin/no/iktdev/mediaprocessing/coordinator/tasksV2/validator/CompletionValidator.kt" afterDir="false" />
<change beforePath="$PROJECT_DIR$/.idea/misc.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/misc.xml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" /> <change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/apps/coordinator/src/main/kotlin/no/iktdev/mediaprocessing/coordinator/CoordinatorApplication.kt" beforeDir="false" afterPath="$PROJECT_DIR$/apps/coordinator/src/main/kotlin/no/iktdev/mediaprocessing/coordinator/CoordinatorApplication.kt" afterDir="false" /> <change beforePath="$PROJECT_DIR$/apps/converter/src/main/kotlin/no/iktdev/mediaprocessing/converter/tasks/ConvertService.kt" beforeDir="false" afterPath="$PROJECT_DIR$/apps/converter/src/main/kotlin/no/iktdev/mediaprocessing/converter/tasks/ConvertService.kt" afterDir="false" />
<change beforePath="$PROJECT_DIR$/apps/coordinator/src/main/kotlin/no/iktdev/mediaprocessing/coordinator/EventsManager.kt" beforeDir="false" /> <change beforePath="$PROJECT_DIR$/apps/coordinator/build.gradle.kts" beforeDir="false" afterPath="$PROJECT_DIR$/apps/coordinator/build.gradle.kts" afterDir="false" />
<change beforePath="$PROJECT_DIR$/apps/ui/build.gradle.kts" beforeDir="false" afterPath="$PROJECT_DIR$/apps/ui/build.gradle.kts" afterDir="false" /> <change beforePath="$PROJECT_DIR$/apps/coordinator/src/main/kotlin/no/iktdev/mediaprocessing/coordinator/tasksV2/listeners/CompletedTaskListener.kt" beforeDir="false" afterPath="$PROJECT_DIR$/apps/coordinator/src/main/kotlin/no/iktdev/mediaprocessing/coordinator/tasksV2/listeners/CompletedTaskListener.kt" afterDir="false" />
<change beforePath="$PROJECT_DIR$/apps/ui/src/main/kotlin/no/iktdev/mediaprocessing/ui/Configuration.kt" beforeDir="false" afterPath="$PROJECT_DIR$/apps/ui/src/main/kotlin/no/iktdev/mediaprocessing/ui/Configuration.kt" afterDir="false" /> <change beforePath="$PROJECT_DIR$/apps/coordinator/src/main/kotlin/no/iktdev/mediaprocessing/coordinator/tasksV2/listeners/ConvertWorkTaskListener.kt" beforeDir="false" afterPath="$PROJECT_DIR$/apps/coordinator/src/main/kotlin/no/iktdev/mediaprocessing/coordinator/tasksV2/listeners/ConvertWorkTaskListener.kt" afterDir="false" />
<change beforePath="$PROJECT_DIR$/apps/ui/src/main/kotlin/no/iktdev/mediaprocessing/ui/UIApplication.kt" beforeDir="false" afterPath="$PROJECT_DIR$/apps/ui/src/main/kotlin/no/iktdev/mediaprocessing/ui/UIApplication.kt" afterDir="false" /> <change beforePath="$PROJECT_DIR$/apps/coordinator/src/main/kotlin/no/iktdev/mediaprocessing/coordinator/tasksV2/listeners/CoverDownloadTaskListener.kt" beforeDir="false" afterPath="$PROJECT_DIR$/apps/coordinator/src/main/kotlin/no/iktdev/mediaprocessing/coordinator/tasksV2/listeners/CoverDownloadTaskListener.kt" afterDir="false" />
<change beforePath="$PROJECT_DIR$/apps/ui/src/main/kotlin/no/iktdev/mediaprocessing/ui/UIEnv.kt" beforeDir="false" afterPath="$PROJECT_DIR$/apps/ui/src/main/kotlin/no/iktdev/mediaprocessing/ui/UIEnv.kt" afterDir="false" /> <change beforePath="$PROJECT_DIR$/apps/coordinator/src/main/kotlin/no/iktdev/mediaprocessing/coordinator/tasksV2/listeners/CoverFromMetadataTaskListener.kt" beforeDir="false" afterPath="$PROJECT_DIR$/apps/coordinator/src/main/kotlin/no/iktdev/mediaprocessing/coordinator/tasksV2/listeners/CoverFromMetadataTaskListener.kt" afterDir="false" />
<change beforePath="$PROJECT_DIR$/apps/ui/src/main/kotlin/no/iktdev/mediaprocessing/ui/dto/EventSummary.kt" beforeDir="false" afterPath="$PROJECT_DIR$/apps/ui/src/main/kotlin/no/iktdev/mediaprocessing/ui/dto/EventSummary.kt" afterDir="false" /> <change beforePath="$PROJECT_DIR$/apps/coordinator/src/main/kotlin/no/iktdev/mediaprocessing/coordinator/tasksV2/listeners/EncodeWorkArgumentsTaskListener.kt" beforeDir="false" afterPath="$PROJECT_DIR$/apps/coordinator/src/main/kotlin/no/iktdev/mediaprocessing/coordinator/tasksV2/listeners/EncodeWorkArgumentsTaskListener.kt" afterDir="false" />
<change beforePath="$PROJECT_DIR$/apps/ui/src/main/kotlin/no/iktdev/mediaprocessing/ui/dto/ExplorerAttr.kt" beforeDir="false" afterPath="$PROJECT_DIR$/apps/ui/src/main/kotlin/no/iktdev/mediaprocessing/ui/dto/explore/ExplorerAttr.kt" afterDir="false" /> <change beforePath="$PROJECT_DIR$/apps/coordinator/src/main/kotlin/no/iktdev/mediaprocessing/coordinator/tasksV2/listeners/ExtractWorkArgumentsTaskListener.kt" beforeDir="false" afterPath="$PROJECT_DIR$/apps/coordinator/src/main/kotlin/no/iktdev/mediaprocessing/coordinator/tasksV2/listeners/ExtractWorkArgumentsTaskListener.kt" afterDir="false" />
<change beforePath="$PROJECT_DIR$/apps/ui/src/main/kotlin/no/iktdev/mediaprocessing/ui/dto/ExplorerCursor.kt" beforeDir="false" afterPath="$PROJECT_DIR$/apps/ui/src/main/kotlin/no/iktdev/mediaprocessing/ui/dto/explore/ExplorerCursor.kt" afterDir="false" /> <change beforePath="$PROJECT_DIR$/apps/coordinator/src/main/kotlin/no/iktdev/mediaprocessing/coordinator/tasksV2/listeners/MediaOutInformationTaskListener.kt" beforeDir="false" afterPath="$PROJECT_DIR$/apps/coordinator/src/main/kotlin/no/iktdev/mediaprocessing/coordinator/tasksV2/listeners/MediaOutInformationTaskListener.kt" afterDir="false" />
<change beforePath="$PROJECT_DIR$/apps/ui/src/main/kotlin/no/iktdev/mediaprocessing/ui/explorer/ExplorerCore.kt" beforeDir="false" afterPath="$PROJECT_DIR$/apps/ui/src/main/kotlin/no/iktdev/mediaprocessing/ui/explorer/ExplorerCore.kt" afterDir="false" /> <change beforePath="$PROJECT_DIR$/apps/coordinator/src/main/kotlin/no/iktdev/mediaprocessing/coordinator/tasksV2/mapping/EncodeWorkArgumentsMapping.kt" beforeDir="false" afterPath="$PROJECT_DIR$/apps/coordinator/src/main/kotlin/no/iktdev/mediaprocessing/coordinator/tasksV2/mapping/EncodeWorkArgumentsMapping.kt" afterDir="false" />
<change beforePath="$PROJECT_DIR$/apps/ui/src/main/kotlin/no/iktdev/mediaprocessing/ui/socket/EventsTableTopic.kt" beforeDir="false" afterPath="$PROJECT_DIR$/apps/ui/src/main/kotlin/no/iktdev/mediaprocessing/ui/socket/EventsTableTopic.kt" afterDir="false" /> <change beforePath="$PROJECT_DIR$/apps/coordinator/src/main/kotlin/no/iktdev/mediaprocessing/coordinator/tasksV2/mapping/ExtractWorkArgumentsMapping.kt" beforeDir="false" afterPath="$PROJECT_DIR$/apps/coordinator/src/main/kotlin/no/iktdev/mediaprocessing/coordinator/tasksV2/mapping/ExtractWorkArgumentsMapping.kt" afterDir="false" />
<change beforePath="$PROJECT_DIR$/apps/ui/src/main/kotlin/no/iktdev/mediaprocessing/ui/socket/ExplorerTopic.kt" beforeDir="false" afterPath="$PROJECT_DIR$/apps/ui/src/main/kotlin/no/iktdev/mediaprocessing/ui/socket/ExplorerTopic.kt" afterDir="false" /> <change beforePath="$PROJECT_DIR$/apps/coordinator/src/test/kotlin/no/iktdev/mediaprocessing/coordinator/reader/BaseInfoFromFileTest.kt" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/apps/ui/src/main/kotlin/no/iktdev/mediaprocessing/ui/socket/TopicSupport.kt" beforeDir="false" /> <change beforePath="$PROJECT_DIR$/apps/coordinator/src/test/kotlin/no/iktdev/mediaprocessing/coordinator/tasksV2/mapping/EncodeWorkArgumentsMappingTest.kt" beforeDir="false" afterPath="$PROJECT_DIR$/apps/coordinator/src/test/kotlin/no/iktdev/mediaprocessing/coordinator/tasksV2/mapping/EncodeWorkArgumentsMappingTest.kt" afterDir="false" />
<change beforePath="$PROJECT_DIR$/apps/processer/src/main/kotlin/no/iktdev/mediaprocessing/processer/ffmpeg/FfmpegTaskService.kt" beforeDir="false" afterPath="$PROJECT_DIR$/apps/processer/src/main/kotlin/no/iktdev/mediaprocessing/processer/ffmpeg/FfmpegTaskService.kt" afterDir="false" />
<change beforePath="$PROJECT_DIR$/apps/processer/src/main/kotlin/no/iktdev/mediaprocessing/processer/services/EncodeService.kt" beforeDir="false" afterPath="$PROJECT_DIR$/apps/processer/src/main/kotlin/no/iktdev/mediaprocessing/processer/services/EncodeService.kt" afterDir="false" />
<change beforePath="$PROJECT_DIR$/apps/processer/src/main/kotlin/no/iktdev/mediaprocessing/processer/services/ExtractService.kt" beforeDir="false" afterPath="$PROJECT_DIR$/apps/processer/src/main/kotlin/no/iktdev/mediaprocessing/processer/services/ExtractService.kt" afterDir="false" />
<change beforePath="$PROJECT_DIR$/apps/ui/web/package-lock.json" beforeDir="false" afterPath="$PROJECT_DIR$/apps/ui/web/package-lock.json" afterDir="false" /> <change beforePath="$PROJECT_DIR$/apps/ui/web/package-lock.json" beforeDir="false" afterPath="$PROJECT_DIR$/apps/ui/web/package-lock.json" afterDir="false" />
<change beforePath="$PROJECT_DIR$/apps/ui/web/package.json" beforeDir="false" afterPath="$PROJECT_DIR$/apps/ui/web/package.json" afterDir="false" /> <change beforePath="$PROJECT_DIR$/apps/ui/web/package.json" beforeDir="false" afterPath="$PROJECT_DIR$/apps/ui/web/package.json" afterDir="false" />
<change beforePath="$PROJECT_DIR$/apps/ui/web/src/App.tsx" beforeDir="false" afterPath="$PROJECT_DIR$/apps/ui/web/src/App.tsx" afterDir="false" /> <change beforePath="$PROJECT_DIR$/apps/ui/web/src/app/page/EventsChainPage.tsx" beforeDir="false" afterPath="$PROJECT_DIR$/apps/ui/web/src/app/page/EventsChainPage.tsx" afterDir="false" />
<change beforePath="$PROJECT_DIR$/apps/ui/web/src/app/page/ExplorePage.tsx" beforeDir="false" afterPath="$PROJECT_DIR$/apps/ui/web/src/app/page/ExplorePage.tsx" afterDir="false" /> <change beforePath="$PROJECT_DIR$/shared/common/src/main/kotlin/no/iktdev/mediaprocessing/shared/common/SharedConfig.kt" beforeDir="false" afterPath="$PROJECT_DIR$/shared/common/src/main/kotlin/no/iktdev/mediaprocessing/shared/common/SharedConfig.kt" afterDir="false" />
<change beforePath="$PROJECT_DIR$/apps/ui/web/src/app/page/LaunchPage.tsx" beforeDir="false" afterPath="$PROJECT_DIR$/apps/ui/web/src/app/page/LaunchPage.tsx" afterDir="false" /> <change beforePath="$PROJECT_DIR$/shared/common/src/main/kotlin/no/iktdev/mediaprocessing/shared/common/Utils.kt" beforeDir="false" afterPath="$PROJECT_DIR$/shared/common/src/main/kotlin/no/iktdev/mediaprocessing/shared/common/Utils.kt" afterDir="false" />
<change beforePath="$PROJECT_DIR$/shared/common/src/main/kotlin/no/iktdev/mediaprocessing/shared/common/contract/EventToClazzTable.kt" beforeDir="false" afterPath="$PROJECT_DIR$/shared/common/src/main/kotlin/no/iktdev/mediaprocessing/shared/common/contract/EventToClazzTable.kt" afterDir="false" /> <change beforePath="$PROJECT_DIR$/shared/common/src/main/kotlin/no/iktdev/mediaprocessing/shared/common/contract/data/ConvertWorkCreatedEvent.kt" beforeDir="false" afterPath="$PROJECT_DIR$/shared/common/src/main/kotlin/no/iktdev/mediaprocessing/shared/common/contract/data/ConvertWorkCreatedEvent.kt" afterDir="false" />
<change beforePath="$PROJECT_DIR$/shared/common/src/main/kotlin/no/iktdev/mediaprocessing/shared/common/database/cal/EventsManager.kt" beforeDir="false" afterPath="$PROJECT_DIR$/shared/common/src/main/kotlin/no/iktdev/mediaprocessing/shared/common/database/cal/EventsManager.kt" afterDir="false" /> <change beforePath="$PROJECT_DIR$/shared/common/src/main/kotlin/no/iktdev/mediaprocessing/shared/common/contract/data/ConvertWorkPerformed.kt" beforeDir="false" afterPath="$PROJECT_DIR$/shared/common/src/main/kotlin/no/iktdev/mediaprocessing/shared/common/contract/data/ConvertWorkPerformed.kt" afterDir="false" />
<change beforePath="$PROJECT_DIR$/shared/eventi/src/main/kotlin/no/iktdev/eventi/implementations/EventCoordinator.kt" beforeDir="false" afterPath="$PROJECT_DIR$/shared/eventi/src/main/kotlin/no/iktdev/eventi/implementations/EventCoordinator.kt" afterDir="false" /> <change beforePath="$PROJECT_DIR$/shared/common/src/main/kotlin/no/iktdev/mediaprocessing/shared/common/contract/data/EncodeArgumentCreatedEvent.kt" beforeDir="false" afterPath="$PROJECT_DIR$/shared/common/src/main/kotlin/no/iktdev/mediaprocessing/shared/common/contract/data/EncodeArgumentCreatedEvent.kt" afterDir="false" />
<change beforePath="$PROJECT_DIR$/shared/common/src/main/kotlin/no/iktdev/mediaprocessing/shared/common/contract/data/ExtractArgumentCreatedEvent.kt" beforeDir="false" afterPath="$PROJECT_DIR$/shared/common/src/main/kotlin/no/iktdev/mediaprocessing/shared/common/contract/data/ExtractArgumentCreatedEvent.kt" afterDir="false" />
<change beforePath="$PROJECT_DIR$/shared/common/src/main/kotlin/no/iktdev/mediaprocessing/shared/common/contract/data/ExtractWorkPerformedEvent.kt" beforeDir="false" afterPath="$PROJECT_DIR$/shared/common/src/main/kotlin/no/iktdev/mediaprocessing/shared/common/contract/data/ExtractWorkPerformedEvent.kt" afterDir="false" />
<change beforePath="$PROJECT_DIR$/shared/common/src/main/kotlin/no/iktdev/mediaprocessing/shared/common/contract/data/MediaCoverInfoReceivedEvent.kt" beforeDir="false" afterPath="$PROJECT_DIR$/shared/common/src/main/kotlin/no/iktdev/mediaprocessing/shared/common/contract/data/MediaCoverInfoReceivedEvent.kt" afterDir="false" />
<change beforePath="$PROJECT_DIR$/shared/common/src/main/kotlin/no/iktdev/mediaprocessing/shared/common/contract/data/MediaOutInformationConstructedEvent.kt" beforeDir="false" afterPath="$PROJECT_DIR$/shared/common/src/main/kotlin/no/iktdev/mediaprocessing/shared/common/contract/data/MediaOutInformationConstructedEvent.kt" afterDir="false" />
<change beforePath="$PROJECT_DIR$/shared/eventi/src/test/kotlin/no/iktdev/eventi/mock/MockEventManager.kt" beforeDir="false" afterPath="$PROJECT_DIR$/shared/eventi/src/test/kotlin/no/iktdev/eventi/mock/MockEventManager.kt" afterDir="false" />
</list> </list>
<option name="SHOW_DIALOG" value="false" /> <option name="SHOW_DIALOG" value="false" />
<option name="HIGHLIGHT_CONFLICTS" value="true" /> <option name="HIGHLIGHT_CONFLICTS" value="true" />
@ -43,7 +50,7 @@
</component> </component>
<component name="ChangesViewManager"> <component name="ChangesViewManager">
<option name="groupingKeys"> <option name="groupingKeys">
<option value="module" /> <option value="directory" />
</option> </option>
</component> </component>
<component name="ExternalProjectsData"> <component name="ExternalProjectsData">
@ -87,10 +94,16 @@
<list> <list>
<option value="JUnit5 Test Class" /> <option value="JUnit5 Test Class" />
<option value="Kotlin Class" /> <option value="Kotlin Class" />
<option value="Kotlin Object" />
</list> </list>
</option> </option>
</component> </component>
<component name="Git.Settings"> <component name="Git.Settings">
<option name="RECENT_BRANCH_BY_REPOSITORY">
<map>
<entry key="$PROJECT_DIR$" value="v3" />
</map>
</option>
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" /> <option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
</component> </component>
<component name="MarkdownSettingsMigration"> <component name="MarkdownSettingsMigration">
@ -105,28 +118,32 @@
<option name="hideEmptyMiddlePackages" value="true" /> <option name="hideEmptyMiddlePackages" value="true" />
<option name="showLibraryContents" value="true" /> <option name="showLibraryContents" value="true" />
</component> </component>
<component name="PropertiesComponent">{ <component name="PropertiesComponent"><![CDATA[{
&quot;keyToString&quot;: { "keyToString": {
&quot;Gradle.AudioArgumentsTest.executor&quot;: &quot;Run&quot;, "Gradle.AudioArgumentsTest.executor": "Run",
&quot;Gradle.AudioArgumentsTest.validateChecks1.executor&quot;: &quot;Run&quot;, "Gradle.AudioArgumentsTest.validateChecks1.executor": "Run",
&quot;Gradle.AudioArgumentsTest.validateChecks3.executor&quot;: &quot;Run&quot;, "Gradle.AudioArgumentsTest.validateChecks3.executor": "Run",
&quot;Gradle.Build MediaProcessing2.executor&quot;: &quot;Run&quot;, "Gradle.Build MediaProcessing2.executor": "Run",
&quot;Gradle.EncodeArgumentCreatorTaskTest.executor&quot;: &quot;Run&quot;, "Gradle.EncodeArgumentCreatorTaskTest.executor": "Run",
&quot;Gradle.MediaProcessing2 [build].executor&quot;: &quot;Run&quot;, "Gradle.FileNameParserTest.assertDotRemoval.executor": "Run",
&quot;Kotlin.UIApplicationKt.executor&quot;: &quot;Run&quot;, "Gradle.FileNameParserTest.assertTitleFails.executor": "Run",
&quot;RunOnceActivity.OpenProjectViewOnStart&quot;: &quot;true&quot;, "Gradle.FileNameParserTest.executor": "Run",
&quot;RunOnceActivity.ShowReadmeOnStart&quot;: &quot;true&quot;, "Gradle.MediaProcessing2 [build].executor": "Run",
&quot;com.intellij.testIntegration.createTest.CreateTestDialog.defaultLibrary&quot;: &quot;JUnit5&quot;, "Kotlin.Env - CoordinatorApplicationKt.executor": "Run",
&quot;com.intellij.testIntegration.createTest.CreateTestDialog.defaultLibrarySuperClass.JUnit5&quot;: &quot;&quot;, "Kotlin.UIApplicationKt.executor": "Run",
&quot;git-widget-placeholder&quot;: &quot;v3&quot;, "RunOnceActivity.OpenProjectViewOnStart": "true",
&quot;ignore.virus.scanning.warn.message&quot;: &quot;true&quot;, "RunOnceActivity.ShowReadmeOnStart": "true",
&quot;kotlin-language-version-configured&quot;: &quot;true&quot;, "com.intellij.testIntegration.createTest.CreateTestDialog.defaultLibrary": "JUnit5",
&quot;last_opened_file_path&quot;: &quot;D:/Workspace/MediaProcessing2/shared/common/src/main/kotlin/no/iktdev/mediaprocessing/shared/common/contract&quot;, "com.intellij.testIntegration.createTest.CreateTestDialog.defaultLibrarySuperClass.JUnit5": "",
&quot;project.structure.last.edited&quot;: &quot;Modules&quot;, "git-widget-placeholder": "v4",
&quot;project.structure.proportion&quot;: &quot;0.0&quot;, "ignore.virus.scanning.warn.message": "true",
&quot;project.structure.side.proportion&quot;: &quot;0.0&quot; "kotlin-language-version-configured": "true",
"last_opened_file_path": "D:/Workspace/MediaProcessing2/shared/common/src/main/kotlin/no/iktdev/mediaprocessing/shared/common/contract",
"project.structure.last.edited": "Modules",
"project.structure.proportion": "0.0",
"project.structure.side.proportion": "0.0"
} }
}</component> }]]></component>
<component name="RecentsManager"> <component name="RecentsManager">
<key name="CopyFile.RECENT_KEYS"> <key name="CopyFile.RECENT_KEYS">
<recent name="D:\Workspace\MediaProcessing2\shared\common\src\main\kotlin\no\iktdev\mediaprocessing\shared\common\contract" /> <recent name="D:\Workspace\MediaProcessing2\shared\common\src\main\kotlin\no\iktdev\mediaprocessing\shared\common\contract" />
@ -154,8 +171,8 @@
<recent name="no.iktdev.mediaprocessing.shared.common" /> <recent name="no.iktdev.mediaprocessing.shared.common" />
</key> </key>
</component> </component>
<component name="RunManager" selected="Kotlin.UIApplicationKt"> <component name="RunManager" selected="Gradle.MediaProcessing2 [build]">
<configuration name="AudioArgumentsTest" type="GradleRunConfiguration" factoryName="Gradle" temporary="true"> <configuration name="FileNameParserTest" type="GradleRunConfiguration" factoryName="Gradle" temporary="true">
<ExternalSystemSettings> <ExternalSystemSettings>
<option name="executionName" /> <option name="executionName" />
<option name="externalProjectPath" value="$PROJECT_DIR$" /> <option name="externalProjectPath" value="$PROJECT_DIR$" />
@ -166,9 +183,9 @@
</option> </option>
<option name="taskNames"> <option name="taskNames">
<list> <list>
<option value=":apps:coordinator:test" /> <option value=":shared:common:test" />
<option value="--tests" /> <option value="--tests" />
<option value="&quot;no.iktdev.mediaprocessing.coordinator.tasksV2.mapping.streams.AudioArgumentsTest&quot;" /> <option value="&quot;no.iktdev.mediaprocessing.shared.common.parsing.FileNameParserTest&quot;" />
</list> </list>
</option> </option>
<option name="vmOptions" /> <option name="vmOptions" />
@ -179,7 +196,7 @@
<RunAsTest>true</RunAsTest> <RunAsTest>true</RunAsTest>
<method v="2" /> <method v="2" />
</configuration> </configuration>
<configuration name="AudioArgumentsTest.validateChecks3" type="GradleRunConfiguration" factoryName="Gradle" temporary="true"> <configuration name="FileNameParserTest.assertDotRemoval" type="GradleRunConfiguration" factoryName="Gradle" temporary="true">
<ExternalSystemSettings> <ExternalSystemSettings>
<option name="executionName" /> <option name="executionName" />
<option name="externalProjectPath" value="$PROJECT_DIR$" /> <option name="externalProjectPath" value="$PROJECT_DIR$" />
@ -190,9 +207,9 @@
</option> </option>
<option name="taskNames"> <option name="taskNames">
<list> <list>
<option value=":apps:coordinator:test" /> <option value=":shared:common:test" />
<option value="--tests" /> <option value="--tests" />
<option value="&quot;no.iktdev.mediaprocessing.coordinator.tasksV2.mapping.streams.AudioArgumentsTest.validateChecks3&quot;" /> <option value="&quot;no.iktdev.mediaprocessing.shared.common.parsing.FileNameParserTest.assertDotRemoval&quot;" />
</list> </list>
</option> </option>
<option name="vmOptions" /> <option name="vmOptions" />
@ -203,7 +220,7 @@
<RunAsTest>true</RunAsTest> <RunAsTest>true</RunAsTest>
<method v="2" /> <method v="2" />
</configuration> </configuration>
<configuration name="EncodeArgumentCreatorTaskTest" type="GradleRunConfiguration" factoryName="Gradle" temporary="true"> <configuration name="FileNameParserTest.assertTitleFails" type="GradleRunConfiguration" factoryName="Gradle" temporary="true">
<ExternalSystemSettings> <ExternalSystemSettings>
<option name="executionName" /> <option name="executionName" />
<option name="externalProjectPath" value="$PROJECT_DIR$" /> <option name="externalProjectPath" value="$PROJECT_DIR$" />
@ -214,9 +231,9 @@
</option> </option>
<option name="taskNames"> <option name="taskNames">
<list> <list>
<option value=":apps:coordinator:test" /> <option value=":shared:common:test" />
<option value="--tests" /> <option value="--tests" />
<option value="&quot;no.iktdev.mediaprocessing.coordinator.tasks.event.ffmpeg.EncodeArgumentCreatorTaskTest&quot;" /> <option value="&quot;no.iktdev.mediaprocessing.shared.common.parsing.FileNameParserTest.assertTitleFails&quot;" />
</list> </list>
</option> </option>
<option name="vmOptions" /> <option name="vmOptions" />
@ -272,20 +289,20 @@
</method> </method>
</configuration> </configuration>
<list> <list>
<item itemvalue="Gradle.AudioArgumentsTest" /> <item itemvalue="Gradle.FileNameParserTest" />
<item itemvalue="Gradle.AudioArgumentsTest.validateChecks3" /> <item itemvalue="Gradle.FileNameParserTest.assertDotRemoval" />
<item itemvalue="Gradle.EncodeArgumentCreatorTaskTest" /> <item itemvalue="Gradle.FileNameParserTest.assertTitleFails" />
<item itemvalue="Gradle.MediaProcessing2 [build]" /> <item itemvalue="Gradle.MediaProcessing2 [build]" />
<item itemvalue="Kotlin.Env - CoordinatorApplicationKt" /> <item itemvalue="Kotlin.Env - CoordinatorApplicationKt" />
<item itemvalue="Kotlin.UIApplicationKt" /> <item itemvalue="Kotlin.UIApplicationKt" />
</list> </list>
<recent_temporary> <recent_temporary>
<list> <list>
<item itemvalue="Kotlin.UIApplicationKt" />
<item itemvalue="Gradle.MediaProcessing2 [build]" /> <item itemvalue="Gradle.MediaProcessing2 [build]" />
<item itemvalue="Gradle.AudioArgumentsTest" /> <item itemvalue="Gradle.FileNameParserTest.assertDotRemoval" />
<item itemvalue="Gradle.AudioArgumentsTest.validateChecks3" /> <item itemvalue="Gradle.FileNameParserTest" />
<item itemvalue="Gradle.EncodeArgumentCreatorTaskTest" /> <item itemvalue="Kotlin.UIApplicationKt" />
<item itemvalue="Gradle.FileNameParserTest.assertTitleFails" />
</list> </list>
</recent_temporary> </recent_temporary>
</component> </component>
@ -298,8 +315,201 @@
<option name="presentableId" value="Default" /> <option name="presentableId" value="Default" />
<updated>1722029797420</updated> <updated>1722029797420</updated>
</task> </task>
<task id="LOCAL00001" summary="Exception on missing title..">
<option name="closed" value="true" />
<created>1723584577549</created>
<option name="number" value="LOCAL00001" />
<option name="presentableId" value="LOCAL00001" />
<updated>1723584577549</updated>
</task>
<task id="LOCAL00002" summary="Reduced logging and replaced with controller">
<option name="closed" value="true" />
<created>1724005100820</created>
<option name="number" value="LOCAL00002" />
<option name="presentableId" value="LOCAL00002" />
<updated>1724005100820</updated>
</task>
<task id="LOCAL00003" summary="Reduced logging and replaced with controller">
<option name="closed" value="true" />
<created>1724015265497</created>
<option name="number" value="LOCAL00003" />
<option name="presentableId" value="LOCAL00003" />
<updated>1724015265497</updated>
</task>
<task id="LOCAL00004" summary="Fix">
<option name="closed" value="true" />
<created>1724018515384</created>
<option name="number" value="LOCAL00004" />
<option name="presentableId" value="LOCAL00004" />
<updated>1724018515384</updated>
</task>
<task id="LOCAL00005" summary="Fix">
<option name="closed" value="true" />
<created>1724018747714</created>
<option name="number" value="LOCAL00005" />
<option name="presentableId" value="LOCAL00005" />
<updated>1724018747714</updated>
</task>
<task id="LOCAL00006" summary="Small tweaks">
<option name="closed" value="true" />
<created>1724197436532</created>
<option name="number" value="LOCAL00006" />
<option name="presentableId" value="LOCAL00006" />
<updated>1724197436532</updated>
</task>
<task id="LOCAL00007" summary="Log suppressing">
<option name="closed" value="true" />
<created>1724281074492</created>
<option name="number" value="LOCAL00007" />
<option name="presentableId" value="LOCAL00007" />
<updated>1724281074492</updated>
</task>
<task id="LOCAL00008" summary="Log suppressing">
<option name="closed" value="true" />
<created>1724283312428</created>
<option name="number" value="LOCAL00008" />
<option name="presentableId" value="LOCAL00008" />
<updated>1724283312428</updated>
</task>
<task id="LOCAL00009" summary="Database connect if not connected">
<option name="closed" value="true" />
<created>1728410705301</created>
<option name="number" value="LOCAL00009" />
<option name="presentableId" value="LOCAL00009" />
<updated>1728410705301</updated>
</task>
<task id="LOCAL00010" summary="Updated py">
<option name="closed" value="true" />
<created>1729126953490</created>
<option name="number" value="LOCAL00010" />
<option name="presentableId" value="LOCAL00010" />
<updated>1729126953490</updated>
</task>
<task id="LOCAL00011" summary="Default">
<option name="closed" value="true" />
<created>1729127287407</created>
<option name="number" value="LOCAL00011" />
<option name="presentableId" value="LOCAL00011" />
<updated>1729127287407</updated>
</task>
<task id="LOCAL00012" summary="Fix for locked loop">
<option name="closed" value="true" />
<created>1729127765449</created>
<option name="number" value="LOCAL00012" />
<option name="presentableId" value="LOCAL00012" />
<updated>1729127765449</updated>
</task>
<task id="LOCAL00013" summary="Fix for missing assignment">
<option name="closed" value="true" />
<created>1729128050503</created>
<option name="number" value="LOCAL00013" />
<option name="presentableId" value="LOCAL00013" />
<updated>1729128050503</updated>
</task>
<task id="LOCAL00014" summary="Removing database closer">
<option name="closed" value="true" />
<created>1729128276481</created>
<option name="number" value="LOCAL00014" />
<option name="presentableId" value="LOCAL00014" />
<updated>1729128276481</updated>
</task>
<task id="LOCAL00015" summary="Logging">
<option name="closed" value="true" />
<created>1729180125525</created>
<option name="number" value="LOCAL00015" />
<option name="presentableId" value="LOCAL00015" />
<updated>1729180125525</updated>
</task>
<task id="LOCAL00016" summary="Logging">
<option name="closed" value="true" />
<created>1729722098624</created>
<option name="number" value="LOCAL00016" />
<option name="presentableId" value="LOCAL00016" />
<updated>1729722098624</updated>
</task>
<task id="LOCAL00017" summary="Logging">
<option name="closed" value="true" />
<created>1729722590315</created>
<option name="number" value="LOCAL00017" />
<option name="presentableId" value="LOCAL00017" />
<updated>1729722590315</updated>
</task>
<task id="LOCAL00018" summary="Logging">
<option name="closed" value="true" />
<created>1729723155814</created>
<option name="number" value="LOCAL00018" />
<option name="presentableId" value="LOCAL00018" />
<updated>1729723155814</updated>
</task>
<task id="LOCAL00019" summary="Disabling metadata timeout">
<option name="closed" value="true" />
<created>1730902509789</created>
<option name="number" value="LOCAL00019" />
<option name="presentableId" value="LOCAL00019" />
<updated>1730902509789</updated>
</task>
<task id="LOCAL00020" summary="Logging and ping">
<option name="closed" value="true" />
<created>1730935750213</created>
<option name="number" value="LOCAL00020" />
<option name="presentableId" value="LOCAL00020" />
<updated>1730935750213</updated>
</task>
<task id="LOCAL00021" summary="Error handling">
<option name="closed" value="true" />
<created>1730936604956</created>
<option name="number" value="LOCAL00021" />
<option name="presentableId" value="LOCAL00021" />
<updated>1730936604956</updated>
</task>
<task id="LOCAL00022" summary="Fixed placement">
<option name="closed" value="true" />
<created>1730937000591</created>
<option name="number" value="LOCAL00022" />
<option name="presentableId" value="LOCAL00022" />
<updated>1730937000591</updated>
</task>
<task id="LOCAL00023" summary="Closing connection after every query">
<option name="closed" value="true" />
<created>1734554387826</created>
<option name="number" value="LOCAL00023" />
<option name="presentableId" value="LOCAL00023" />
<updated>1734554387826</updated>
</task>
<option name="localTasksCounter" value="24" />
<servers /> <servers />
</component> </component>
<component name="Vcs.Log.Tabs.Properties">
<option name="TAB_STATES">
<map>
<entry key="MAIN">
<value>
<State />
</value>
</entry>
</map>
</option>
</component>
<component name="VcsManagerConfiguration">
<MESSAGE value="Exception on missing title.." />
<MESSAGE value="Reduced logging and replaced with controller" />
<MESSAGE value="Fix" />
<MESSAGE value="Small tweaks" />
<MESSAGE value="Log suppressing" />
<MESSAGE value="Database connect if not connected" />
<MESSAGE value="Updated py" />
<MESSAGE value="Default" />
<MESSAGE value="Fix for locked loop" />
<MESSAGE value="Fix for missing assignment" />
<MESSAGE value="Removing database closer" />
<MESSAGE value="Logging" />
<MESSAGE value="Disabling metadata timeout" />
<MESSAGE value="Logging and ping" />
<MESSAGE value="Error handling" />
<MESSAGE value="Fixed placement" />
<MESSAGE value="Closing connection after every query" />
<option name="LAST_COMMIT_MESSAGE" value="Closing connection after every query" />
</component>
<component name="XDebuggerManager"> <component name="XDebuggerManager">
<breakpoint-manager> <breakpoint-manager>
<breakpoints> <breakpoints>
@ -308,6 +518,61 @@
<line>23</line> <line>23</line>
<option name="timeStamp" value="1" /> <option name="timeStamp" value="1" />
</line-breakpoint> </line-breakpoint>
<line-breakpoint enabled="true" type="kotlin-line">
<url>file://$PROJECT_DIR$/shared/eventi/src/main/kotlin/no/iktdev/eventi/implementations/EventCoordinator.kt</url>
<line>133</line>
<option name="timeStamp" value="17" />
</line-breakpoint>
<line-breakpoint enabled="true" type="kotlin-line">
<url>file://$PROJECT_DIR$/shared/eventi/src/main/kotlin/no/iktdev/eventi/implementations/EventCoordinator.kt</url>
<line>130</line>
<option name="timeStamp" value="18" />
</line-breakpoint>
<line-breakpoint enabled="true" type="kotlin-line">
<url>file://$PROJECT_DIR$/shared/eventi/src/main/kotlin/no/iktdev/eventi/implementations/EventCoordinator.kt</url>
<line>131</line>
<option name="timeStamp" value="19" />
</line-breakpoint>
<line-breakpoint enabled="true" type="kotlin-line">
<url>file://$PROJECT_DIR$/shared/common/src/test/kotlin/no/iktdev/mediaprocessing/shared/common/parsing/FileNameParserTest.kt</url>
<line>44</line>
<option name="timeStamp" value="20" />
</line-breakpoint>
<line-breakpoint enabled="true" type="kotlin-line">
<url>file://$PROJECT_DIR$/shared/common/src/test/kotlin/no/iktdev/mediaprocessing/shared/common/parsing/FileNameParserTest.kt</url>
<line>43</line>
<option name="timeStamp" value="21" />
</line-breakpoint>
<line-breakpoint enabled="true" type="kotlin-line">
<url>file://$PROJECT_DIR$/shared/common/src/main/kotlin/no/iktdev/mediaprocessing/shared/common/parsing/FileNameParser.kt</url>
<line>22</line>
<option name="timeStamp" value="22" />
</line-breakpoint>
<line-breakpoint enabled="true" type="kotlin-line">
<url>file://$PROJECT_DIR$/shared/common/src/main/kotlin/no/iktdev/mediaprocessing/shared/common/parsing/FileNameParser.kt</url>
<line>23</line>
<option name="timeStamp" value="23" />
</line-breakpoint>
<line-breakpoint enabled="true" type="kotlin-line">
<url>file://$PROJECT_DIR$/shared/common/src/main/kotlin/no/iktdev/mediaprocessing/shared/common/parsing/FileNameParser.kt</url>
<line>24</line>
<option name="timeStamp" value="25" />
</line-breakpoint>
<line-breakpoint enabled="true" type="kotlin-line">
<url>file://$PROJECT_DIR$/shared/common/src/main/kotlin/no/iktdev/mediaprocessing/shared/common/parsing/FileNameParser.kt</url>
<line>25</line>
<option name="timeStamp" value="26" />
</line-breakpoint>
<line-breakpoint enabled="true" type="kotlin-line">
<url>file://$PROJECT_DIR$/shared/common/src/main/kotlin/no/iktdev/mediaprocessing/shared/common/parsing/FileNameParser.kt</url>
<line>40</line>
<option name="timeStamp" value="27" />
</line-breakpoint>
<line-breakpoint enabled="true" type="kotlin-line">
<url>file://$PROJECT_DIR$/shared/common/src/main/kotlin/no/iktdev/mediaprocessing/shared/common/parsing/FileNameParser.kt</url>
<line>41</line>
<option name="timeStamp" value="28" />
</line-breakpoint>
</breakpoints> </breakpoints>
</breakpoint-manager> </breakpoint-manager>
</component> </component>

View File

@ -72,6 +72,7 @@ class ConvertService(
override fun onCompleted(inputFile: String, outputFiles: List<String>) { override fun onCompleted(inputFile: String, outputFiles: List<String>) {
val task = assignedTask ?: return val task = assignedTask ?: return
val taskData: ConvertData = task.data as ConvertData
log.info { "Convert completed for ${task.referenceId}" } log.info { "Convert completed for ${task.referenceId}" }
val claimSuccessful = taskManager.markTaskAsCompleted(task.referenceId, task.eventId) val claimSuccessful = taskManager.markTaskAsCompleted(task.referenceId, task.eventId)
@ -95,6 +96,7 @@ class ConvertService(
source = getProducerName() source = getProducerName()
), ),
data = ConvertedData( data = ConvertedData(
language = taskData.language,
outputFiles = outputFiles outputFiles = outputFiles
) )
)) ))

View File

@ -37,7 +37,7 @@ dependencies {
implementation("org.json:json:20210307") implementation("org.json:json:20210307")
implementation("no.iktdev:exfl:0.0.16-SNAPSHOT") implementation("no.iktdev:exfl:0.0.16-SNAPSHOT")
implementation("no.iktdev.streamit.library:streamit-library-db:0.0.6-alpha27") implementation("no.iktdev.streamit.library:streamit-library-db:1.0.0-alpha11")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.1") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.1")

View File

@ -7,22 +7,17 @@ import no.iktdev.mediaprocessing.coordinator.Coordinator
import no.iktdev.mediaprocessing.coordinator.CoordinatorEventListener import no.iktdev.mediaprocessing.coordinator.CoordinatorEventListener
import no.iktdev.mediaprocessing.coordinator.getStoreDatabase import no.iktdev.mediaprocessing.coordinator.getStoreDatabase
import no.iktdev.eventi.database.executeOrException import no.iktdev.eventi.database.executeOrException
import no.iktdev.eventi.database.executeWithStatus
import no.iktdev.eventi.database.withTransaction import no.iktdev.eventi.database.withTransaction
import no.iktdev.mediaprocessing.coordinator.tasksV2.mapping.store.*
import no.iktdev.mediaprocessing.coordinator.tasksV2.validator.CompletionValidator
import no.iktdev.mediaprocessing.shared.common.parsing.NameHelper import no.iktdev.mediaprocessing.shared.common.parsing.NameHelper
import no.iktdev.mediaprocessing.shared.common.contract.Events import no.iktdev.mediaprocessing.shared.common.contract.Events
import no.iktdev.mediaprocessing.shared.common.contract.data.* import no.iktdev.mediaprocessing.shared.common.contract.data.*
import no.iktdev.mediaprocessing.shared.common.contract.dto.StartOperationEvents
import no.iktdev.mediaprocessing.shared.common.contract.dto.SubtitleFormats
import no.iktdev.mediaprocessing.shared.common.contract.reader.* import no.iktdev.mediaprocessing.shared.common.contract.reader.*
import no.iktdev.streamit.library.db.query.CatalogQuery
import no.iktdev.streamit.library.db.query.GenreQuery
import no.iktdev.streamit.library.db.query.SubtitleQuery
import no.iktdev.streamit.library.db.query.SummaryQuery import no.iktdev.streamit.library.db.query.SummaryQuery
import no.iktdev.streamit.library.db.tables.catalog import no.iktdev.streamit.library.db.tables.catalog
import no.iktdev.streamit.library.db.tables.titles import no.iktdev.streamit.library.db.tables.titles
import org.jetbrains.exposed.exceptions.ExposedSQLException import org.jetbrains.exposed.exceptions.ExposedSQLException
import org.jetbrains.exposed.sql.and
import org.jetbrains.exposed.sql.insertIgnore import org.jetbrains.exposed.sql.insertIgnore
import org.jetbrains.exposed.sql.select import org.jetbrains.exposed.sql.select
import org.jetbrains.exposed.sql.update import org.jetbrains.exposed.sql.update
@ -32,7 +27,7 @@ import java.io.File
import java.sql.SQLIntegrityConstraintViolationException import java.sql.SQLIntegrityConstraintViolationException
@Service @Service
class CompletedTaskListener: CoordinatorEventListener() { class CompletedTaskListener : CoordinatorEventListener() {
val log = KotlinLogging.logger {} val log = KotlinLogging.logger {}
var doNotProduceComplete = System.getenv("DISABLE_COMPLETE").toBoolean() ?: false var doNotProduceComplete = System.getenv("DISABLE_COMPLETE").toBoolean() ?: false
@ -64,104 +59,6 @@ class CompletedTaskListener: CoordinatorEventListener() {
Events.EventWorkExtractPerformed Events.EventWorkExtractPerformed
) )
/**
* Checks whether it requires encode or extract or both, and it has created events with args
*/
fun req1(started: MediaProcessStartEvent, events: List<Event>): Boolean {
val encodeFulfilledOrSkipped = if (started.data?.operations?.contains(StartOperationEvents.ENCODE) == true) {
events.any { it.eventType == Events.EventMediaParameterEncodeCreated }
} else true
val extractFulfilledOrSkipped = if (started.data?.operations?.contains(StartOperationEvents.EXTRACT) == true) {
events.any { it.eventType == Events.EventMediaParameterExtractCreated }
} else true
if (!encodeFulfilledOrSkipped || !extractFulfilledOrSkipped) {
return false
} else return true
}
/**
* Checks whether work that was supposed to be created has been created.
* Checks if all subtitles that can be processed has been created if convert is set.
*/
fun req2(operations: List<StartOperationEvents>, events: List<Event>): Boolean {
if (StartOperationEvents.ENCODE in operations) {
val encodeParamter = events.find { it.eventType == Events.EventMediaParameterEncodeCreated }?.az<EncodeArgumentCreatedEvent>()
val encodeWork = events.find { it.eventType == Events.EventWorkEncodeCreated }
if (encodeParamter?.isSuccessful() == true && (encodeWork == null))
return false
}
val extractParamter = events.find { it.eventType == Events.EventMediaParameterExtractCreated }?.az<ExtractArgumentCreatedEvent>()
val extractWork = events.filter { it.eventType == Events.EventWorkExtractCreated }
if (StartOperationEvents.EXTRACT in operations) {
if (extractParamter?.isSuccessful() == true && extractParamter.data?.size != extractWork.size)
return false
}
if (StartOperationEvents.CONVERT in operations) {
val convertWork = events.filter { it.eventType == Events.EventWorkConvertCreated }
val supportedSubtitleFormats = SubtitleFormats.entries.map { it.name }
val eventsSupportsConvert = extractWork.filter { it.data is ExtractArgumentData }
.filter { (it.dataAs<ExtractArgumentData>()?.outputFile?.let { f -> File(f).extension.uppercase() } in supportedSubtitleFormats) }
if (convertWork.size != eventsSupportsConvert.size)
return false
}
return true
}
/**
* Checks whether all work that has been created has been completed
*/
fun req3(operations: List<StartOperationEvents>, events: List<Event>): Boolean {
if (StartOperationEvents.ENCODE in operations) {
val encodeWork = events.filter { it.eventType == Events.EventWorkEncodeCreated }
val encodePerformed = events.filter { it.eventType == Events.EventWorkEncodePerformed }
if (encodePerformed.size < encodeWork.size)
return false
}
if (StartOperationEvents.EXTRACT in operations) {
val extractWork = events.filter { it.eventType == Events.EventWorkExtractCreated }
val extractPerformed = events.filter { it.eventType == Events.EventWorkExtractPerformed }
if (extractPerformed.size < extractWork.size)
return false
}
if (StartOperationEvents.CONVERT in operations) {
val convertWork = events.filter { it.eventType == Events.EventWorkConvertCreated }
val convertPerformed = events.filter { it.eventType == Events.EventWorkConvertPerformed }
if (convertPerformed.size < convertWork.size)
return false
}
return true
}
/**
* Checks if metadata has cover, if so, 2 events are expected
*/
fun req4(events: List<Event>): Boolean {
val metadata = events.find { it.eventType == Events.EventMediaMetadataSearchPerformed }
if (metadata?.isSuccessful() != true) {
return true
}
val hasCover = metadata.dataAs<pyMetadata>()?.cover != null
if (hasCover == false) {
return true
}
if (events.any { it.eventType == Events.EventMediaReadOutCover } && events.any { it.eventType == Events.EventWorkDownloadCoverPerformed }) {
return true
}
return false
}
override fun isPrerequisitesFulfilled(incomingEvent: Event, events: List<Event>): Boolean { override fun isPrerequisitesFulfilled(incomingEvent: Event, events: List<Event>): Boolean {
val started = events.find { it.eventType == Events.EventMediaProcessStarted }?.az<MediaProcessStartEvent>() val started = events.find { it.eventType == Events.EventMediaProcessStarted }?.az<MediaProcessStartEvent>()
@ -171,104 +68,28 @@ class CompletedTaskListener: CoordinatorEventListener() {
} }
val viableEvents = events.filter { it.isSuccessful() } val viableEvents = events.filter { it.isSuccessful() }
if (!CompletionValidator.req1(started, events)) {
if (!req1(started, events)) {
//log.info { "${this::class.java.simpleName} Failed Req1" }
return false return false
} }
if (!req2(started.data?.operations ?: emptyList(), viableEvents)) { if (!CompletionValidator.req2(started.data?.operations ?: emptyList(), viableEvents)) {
//log.info { "${this::class.java.simpleName} Failed Req2" }
return false return false
} }
if (!req3(started.data?.operations ?: emptyList(), events)) { if (!CompletionValidator.req3(started.data?.operations ?: emptyList(), events)) {
//log.info { "${this::class.java.simpleName} Failed Req3" }
return false return false
} }
if (!req4(events)) { if (!CompletionValidator.req4(events)) {
log.info { "${this::class.java.simpleName} Failed Req4" }
return false return false
} }
return super.isPrerequisitesFulfilled(incomingEvent, events) return super.isPrerequisitesFulfilled(incomingEvent, events)
} }
fun getMetadata(events: List<Event>): MetadataDto? {
val baseInfo = events.find { it.eventType == Events.EventMediaReadBaseInfoPerformed }?.az<BaseInfoEvent>()
val mediaInfo = events.find { it.eventType == Events.EventMediaReadOutNameAndType }?.az<MediaOutInformationConstructedEvent>()
val metadataInfo = events.find { it.eventType == Events.EventMediaMetadataSearchPerformed }?.az<MediaMetadataReceivedEvent>()
val coverInfo = events.find { it.eventType == Events.EventWorkDownloadCoverPerformed }?.az<MediaCoverDownloadedEvent>()
val coverTask = events.find { it.eventType == Events.EventMediaReadOutCover }?.az<MediaCoverInfoReceivedEvent>()
if (baseInfo == null) {
log.info { "Cant find BaseInfoEvent on ${Events.EventMediaReadBaseInfoPerformed}" }
return null
}
if (mediaInfo == null) {
log.info { "Cant find MediaOutInformationConstructedEvent on ${Events.EventMediaReadOutNameAndType}" }
return null
}
if (metadataInfo == null) {
log.info { "Cant find MediaMetadataReceivedEvent on ${Events.EventMediaMetadataSearchPerformed}" }
return null
}
if (coverTask?.isSkipped() == false && coverInfo == null) {
log.info { "Cant find MediaCoverDownloadedEvent on ${Events.EventWorkDownloadCoverPerformed}" }
}
val mediaInfoData = mediaInfo.data?.toValueObject()
val baseInfoData = baseInfo.data
val metadataInfoData = metadataInfo.data
val collection = mediaInfo.data?.outDirectory?.let { File(it).name } ?: baseInfoData?.title
val coverFileName = coverInfo?.data?.absoluteFilePath?.let {
File(it).name
}
return MetadataDto(
title = mediaInfoData?.title ?: baseInfoData?.title ?: metadataInfoData?.title ?: return null,
collection = collection ?: return null,
cover = coverFileName,
type = metadataInfoData?.type ?: mediaInfoData?.type ?: return null,
summary = metadataInfoData?.summary?.filter {it.summary != null }?.map { SummaryInfo(language = it.language, summary = it.summary!! ) } ?: emptyList(),
genres = metadataInfoData?.genres ?: emptyList(),
titles = (metadataInfoData?.altTitle ?: emptyList()) + listOfNotNull(mediaInfoData?.title, baseInfoData?.title)
)
}
fun getGenres(events: List<Event>): List<String> {
val metadataInfo = events.find { it.eventType == Events.EventMediaMetadataSearchPerformed }?.az<MediaMetadataReceivedEvent>()
return metadataInfo?.data?.genres ?: emptyList()
}
fun getSubtitles(metadataDto: MetadataDto?, events: List<Event>): List<SubtitlesDto> {
val extracted = events.filter { it.eventType == Events.EventWorkExtractPerformed }.mapNotNull { it.dataAs<ExtractedData>() }
val converted = events.filter { it.eventType == Events.EventWorkConvertPerformed }.mapNotNull { it.dataAs<ConvertedData>() }
val outFiles = extracted.map { it.outputFile } + converted.flatMap { it.outputFiles }
return outFiles.map {
val subtitleFile = File(it)
SubtitlesDto(
collection = metadataDto?.collection ?: subtitleFile.parentFile.parentFile.name,
language = subtitleFile.parentFile.name,
subtitleFile = subtitleFile.name,
format = subtitleFile.extension.uppercase(),
associatedWithVideo = subtitleFile.nameWithoutExtension,
)
}
}
fun getVideo(events: List<Event>): VideoDetails? { fun getVideo(events: List<Event>): VideoDetails? {
val mediaInfo = events.find { it.eventType == Events.EventMediaReadOutNameAndType }?.az<MediaOutInformationConstructedEvent>() val mediaInfo = events.find { it.eventType == Events.EventMediaReadOutNameAndType }
?.az<MediaOutInformationConstructedEvent>()
val encoded = events.find { it.eventType == Events.EventWorkEncodePerformed }?.dataAs<EncodedData>()?.outputFile val encoded = events.find { it.eventType == Events.EventWorkEncodePerformed }?.dataAs<EncodedData>()?.outputFile
if (encoded == null) { if (encoded == null) {
log.warn { "No encode no video details!" } log.warn { "No encode no video details!" }
@ -290,120 +111,8 @@ class CompletedTaskListener: CoordinatorEventListener() {
return details return details
} }
fun storeSubtitles(subtitles: List<SubtitlesDto>) {
subtitles.forEach { subtitle ->
subtitle to executeWithStatus(getStoreDatabase()) {
SubtitleQuery(
collection = subtitle.collection,
associatedWithVideo = subtitle.associatedWithVideo,
language = subtitle.language,
format = subtitle.format,
file = subtitle.subtitleFile
).insert()
}
}
}
fun storeTitles(usedTitle: String, metadata: MetadataDto) {
try {
withTransaction(getStoreDatabase()) {
titles.insertIgnore {
it[masterTitle] = metadata.collection
it[title] = NameHelper.normalize(usedTitle)
it[type] = 1
}
titles.insertIgnore {
it[masterTitle] = usedTitle
it[title] = NameHelper.normalize(usedTitle)
it[type] = 2
}
metadata.titles.forEach { title ->
titles.insertIgnore {
it[masterTitle] = usedTitle
it[titles.title] = title
}
}
}
} catch (e: Exception) {
e.printStackTrace()
}
}
fun storeMetadata(catalogId: Int, metadata: MetadataDto) {
if (!metadata.cover.isNullOrBlank()) {
withTransaction(getStoreDatabase()) {
val storedCatalogCover = catalog.select {
(catalog.id eq catalogId)
}.map { it[catalog.cover] }.firstOrNull()
if (storedCatalogCover.isNullOrBlank()) {
catalog.update({
catalog.id eq catalogId
}) {
it[catalog.cover] = metadata.cover
}
}
}
}
metadata.summary.forEach {
val result = executeOrException(getStoreDatabase().database) {
SummaryQuery(
cid = catalogId,
language = it.language,
description = it.summary
).insert()
}
val ignoreException = result?.cause is SQLIntegrityConstraintViolationException && (result as ExposedSQLException).errorCode == 1062
if (!ignoreException) {
result?.printStackTrace()
}
}
}
fun storeAndGetGenres(genres: List<String>): String? {
return withTransaction(getStoreDatabase()) {
val gq = GenreQuery( *genres.toTypedArray() )
gq.insertAndGetIds()
gq.getIds().joinToString(",")
}
}
fun storeCatalog(metadata: MetadataDto, videoDetails: VideoDetails, genres: String?): Int? {
val precreatedCatalogQuery = CatalogQuery(
title = NameHelper.normalize(metadata.title),
cover = metadata.cover,
type = metadata.type,
collection = metadata.collection,
genres = genres
)
val result = when (videoDetails.type) {
"serie" -> {
val serieInfo = videoDetails.serieInfo ?: throw RuntimeException("SerieInfo missing in VideoDetails for Serie! ${videoDetails.fileName}")
executeOrException {
precreatedCatalogQuery.insertWithSerie(
episodeTitle = serieInfo.episodeTitle ?: "",
videoFile = videoDetails.fileName,
episode = serieInfo.episodeNumber,
season = serieInfo.seasonNumber
)
}
}
"movie" -> {
executeOrException {
precreatedCatalogQuery.insertWithMovie(videoDetails.fileName)
}
}
else -> throw RuntimeException("${videoDetails.type} is not supported!")
}
val ignoreException = result?.cause is SQLIntegrityConstraintViolationException && (result as ExposedSQLException).errorCode == 1062
return withTransaction(getStoreDatabase()) {
precreatedCatalogQuery.getId()
}
}
override fun shouldIProcessAndHandleEvent(incomingEvent: Event, events: List<Event>): Boolean { override fun shouldIProcessAndHandleEvent(incomingEvent: Event, events: List<Event>): Boolean {
val result = super.shouldIProcessAndHandleEvent(incomingEvent, events) val result = super.shouldIProcessAndHandleEvent(incomingEvent, events)
return result return result
@ -413,27 +122,63 @@ class CompletedTaskListener: CoordinatorEventListener() {
val event = incomingEvent.consume() ?: return val event = incomingEvent.consume() ?: return
active = true active = true
val metadata = getMetadata(events) val mediaInfo: ComposedMediaInfo = composeMediaInfo(events) ?: run {
val genres = getGenres(events) log.error { "Unable to compose media info for ${event.referenceId()}" }
val subtitles = getSubtitles(metadata, events) return
val video = getVideo(events) }
val existingTitles = ContentTitleStore.findMasterTitles(mediaInfo.titles)
val usableCollection: String = if (existingTitles.isNotEmpty())
ContentCatalogStore.getCollectionByTitleAndType(mediaInfo.type, existingTitles) ?: run {
log.warn { "Did not receive collection based on titles provided in list ${existingTitles.joinToString(",")}, falling back to fallbackCollection: ${mediaInfo.fallbackCollection}" }
mediaInfo.fallbackCollection
} else mediaInfo.fallbackCollection
val mover = ContentCompletionMover(usableCollection, events)
val newVideoPath = mover.moveVideo()
val newCoverPath = mover.moveCover()
val newSubtitles = mover.moveSubtitles()
val genreIdsForCatalog = ContentGenresStore.storeAndGetIds(mediaInfo.genres)
val storedGenres = storeAndGetGenres(genres) val catalogId = ContentCatalogStore.storeCatalog(
val catalogId = if (metadata != null && video != null) { title = mediaInfo.title,
storeCatalog(metadata, video, storedGenres) collection = usableCollection,
} else null type = mediaInfo.type,
cover = newCoverPath?.second?.let { dp -> File(dp).name },
genres = genreIdsForCatalog,
)
getVideo(events)?.let { video ->
ContentCatalogStore.storeMedia(
title = mediaInfo.title,
collection = usableCollection,
type = mediaInfo.type,
videoDetails = video
)
}
val storedSubtitles = newSubtitles?.let { subtitles ->
storeSubtitles(subtitles) subtitles.mapNotNull {
metadata?.let { ContentSubtitleStore.storeSubtitles(
storeTitles(metadata = metadata, usedTitle = metadata.title) collection = usableCollection,
catalogId?.let { id -> language = it.language,
storeMetadata(catalogId = id, metadata = it) destinationFile = it.destination
)
} }
} }
catalogId?.let { cid ->
mediaInfo.summaries.forEach {
ContentMetadataStore.storeSummary(cid, it)
}
ContentTitleStore.store(mediaInfo.title, mediaInfo.titles)
}
if (!doNotProduceComplete) { if (!doNotProduceComplete) {
onProduceEvent(MediaProcessCompletedEvent( onProduceEvent(MediaProcessCompletedEvent(
metadata = event.makeDerivedEventInfo(EventStatus.Success, getProducerName()), metadata = event.makeDerivedEventInfo(EventStatus.Success, getProducerName()),
@ -448,4 +193,57 @@ class CompletedTaskListener: CoordinatorEventListener() {
active = false active = false
} }
internal data class ComposedMediaInfo(
val title: String,
val fallbackCollection: String,
val titles: List<String>,
val type: String,
val summaries: List<SummaryInfo>,
val genres: List<String>
)
private fun composeMediaInfo(events: List<Event>): ComposedMediaInfo? {
val baseInfo =
events.find { it.eventType == Events.EventMediaReadBaseInfoPerformed }?.az<BaseInfoEvent>()?.let {
it.data
} ?: run {
log.info { "Cant find BaseInfoEvent on ${Events.EventMediaReadBaseInfoPerformed}" }
return null
}
val metadataInfo =
events.find { it.eventType == Events.EventMediaMetadataSearchPerformed }?.az<MediaMetadataReceivedEvent>()?.data
?: run {
log.info { "Cant find MediaMetadataReceivedEvent on ${Events.EventMediaMetadataSearchPerformed}" }
null
}
val mediaInfo: MediaInfo = events.find { it.eventType == Events.EventMediaReadOutNameAndType }
?.az<MediaOutInformationConstructedEvent>()?.let {
it.data?.toValueObject()
} ?: run {
log.info { "Cant find MediaOutInformationConstructedEvent on ${Events.EventMediaReadOutNameAndType}" }
return null
}
val summaries = metadataInfo?.summary?.filter { it.summary != null }
?.map { SummaryInfo(language = it.language, summary = it.summary!!) } ?: emptyList()
val titles: MutableList<String> = mutableListOf(mediaInfo.title)
metadataInfo?.let {
titles.addAll(it.altTitle)
titles.add(it.title)
titles.add(NameHelper.normalize(it.title))
}
return ComposedMediaInfo(
title = NameHelper.normalize(metadataInfo?.title ?: mediaInfo.title),
fallbackCollection = baseInfo.title,
titles = titles,
type = metadataInfo?.type ?: mediaInfo.type,
summaries = summaries,
genres = metadataInfo?.genres ?: emptyList()
)
}
} }

View File

@ -63,8 +63,13 @@ class ConvertWorkTaskListener: WorkTaskListener() {
return return
} }
var language: String? = null
val file = if (event.eventType == Events.EventWorkExtractPerformed) { val file = if (event.eventType == Events.EventWorkExtractPerformed) {
event.az<ExtractWorkPerformedEvent>()?.data?.outputFile val foundEvent = event.az<ExtractWorkPerformedEvent>()?.data
language = foundEvent?.language
foundEvent?.outputFile
} else if (event.eventType == Events.EventMediaProcessStarted) { } else if (event.eventType == Events.EventMediaProcessStarted) {
val startEvent = event.az<MediaProcessStartEvent>()?.data val startEvent = event.az<MediaProcessStartEvent>()?.data
if (startEvent?.operations?.isOnly(StartOperationEvents.CONVERT) == true) { if (startEvent?.operations?.isOnly(StartOperationEvents.CONVERT) == true) {
@ -77,6 +82,15 @@ class ConvertWorkTaskListener: WorkTaskListener() {
val convertFile = file?.let { File(it) } val convertFile = file?.let { File(it) }
if (language.isNullOrEmpty()) {
convertFile?.parentFile?.nameWithoutExtension?.let {
if (it.length == 3) {
language = it.lowercase()
}
}
}
if (convertFile == null || !convertFile.exists()) { if (convertFile == null || !convertFile.exists()) {
onProduceEvent(ConvertWorkCreatedEvent( onProduceEvent(ConvertWorkCreatedEvent(
metadata = event.makeDerivedEventInfo(EventStatus.Failed, getProducerName()) metadata = event.makeDerivedEventInfo(EventStatus.Failed, getProducerName())
@ -84,6 +98,7 @@ class ConvertWorkTaskListener: WorkTaskListener() {
return return
} else { } else {
val convertData = ConvertData( val convertData = ConvertData(
language = language ?: "unk",
inputFile = convertFile.absolutePath, inputFile = convertFile.absolutePath,
outputFileName = convertFile.nameWithoutExtension, outputFileName = convertFile.nameWithoutExtension,
outputDirectory = convertFile.parentFile.absolutePath, outputDirectory = convertFile.parentFile.absolutePath,

View File

@ -6,9 +6,11 @@ import no.iktdev.eventi.core.ConsumableEvent
import no.iktdev.eventi.core.WGson import no.iktdev.eventi.core.WGson
import no.iktdev.eventi.data.EventStatus import no.iktdev.eventi.data.EventStatus
import no.iktdev.eventi.implementations.EventCoordinator import no.iktdev.eventi.implementations.EventCoordinator
import no.iktdev.exfl.using
import no.iktdev.mediaprocessing.coordinator.Coordinator import no.iktdev.mediaprocessing.coordinator.Coordinator
import no.iktdev.mediaprocessing.coordinator.CoordinatorEventListener import no.iktdev.mediaprocessing.coordinator.CoordinatorEventListener
import no.iktdev.mediaprocessing.shared.common.DownloadClient import no.iktdev.mediaprocessing.shared.common.DownloadClient
import no.iktdev.mediaprocessing.shared.common.SharedConfig
import no.iktdev.mediaprocessing.shared.common.contract.Events import no.iktdev.mediaprocessing.shared.common.contract.Events
import no.iktdev.mediaprocessing.shared.common.contract.EventsListenerContract import no.iktdev.mediaprocessing.shared.common.contract.EventsListenerContract
import no.iktdev.mediaprocessing.shared.common.contract.EventsManagerContract import no.iktdev.mediaprocessing.shared.common.contract.EventsManagerContract
@ -49,24 +51,13 @@ class CoverDownloadTaskListener : CoordinatorEventListener() {
return return
} }
val outDir = File(data.outDir) val client = DownloadClient(data.url, SharedConfig.cachedContent, data.outFileBaseName)
.also {
if (!it.exists()) {
it.mkdirs()
}
}
if (!outDir.exists()) {
log.error { "Check for output directory for cover storage failed for ${event.metadata.eventId} " }
onProduceEvent(failedEventDefault)
}
val client = DownloadClient(data.url, File(data.outDir), data.outFileBaseName)
val outFile = runBlocking { val outFile = runBlocking {
client.getOutFile() client.getOutFile()
} }
val coversInDifferentFormats = outDir.listFiles { it -> it.isFile && it.extension.lowercase() in client.contentTypeToExtension().values } ?: emptyArray() val coversInDifferentFormats = SharedConfig.cachedContent.listFiles { it -> it.isFile && it.extension.lowercase() in client.contentTypeToExtension().values } ?: emptyArray()
val result = if (outFile?.exists() == true) { val result = if (outFile?.exists() == true) {
outFile outFile

View File

@ -86,7 +86,6 @@ class CoverFromMetadataTaskListener: CoordinatorEventListener() {
data = CoverDetails( data = CoverDetails(
url = coverUrl, url = coverUrl,
outFileBaseName = NameHelper.normalize(coverTitle), outFileBaseName = NameHelper.normalize(coverTitle),
outDir = mediaOutInfo.outDirectory,
) )
) )
} }

View File

@ -84,7 +84,6 @@ class EncodeWorkArgumentsTaskListener: CoordinatorEventListener() {
val mapper = EncodeWorkArgumentsMapping( val mapper = EncodeWorkArgumentsMapping(
inputFile = inputFile, inputFile = inputFile,
outFileFullName = mediaInfoData.fullName, outFileFullName = mediaInfoData.fullName,
outFileAbsolutePathFile = mediaInfo.data?.outDirectory?.let { File(it) } ?: return,
streams = streams, streams = streams,
preference = preference.encodePreference preference = preference.encodePreference
) )

View File

@ -77,7 +77,6 @@ class ExtractWorkArgumentsTaskListener: CoordinatorEventListener() {
val mapper = ExtractWorkArgumentsMapping( val mapper = ExtractWorkArgumentsMapping(
inputFile = inputFile, inputFile = inputFile,
outFileFullName = mediaInfoData.fullName, outFileFullName = mediaInfoData.fullName,
outFileAbsolutePathFile = mediaInfo.data?.outDirectory?.let { File(it) } ?: return,
streams = streams streams = streams
) )

View File

@ -7,12 +7,10 @@ import no.iktdev.eventi.data.EventStatus
import no.iktdev.exfl.using import no.iktdev.exfl.using
import no.iktdev.mediaprocessing.coordinator.Coordinator import no.iktdev.mediaprocessing.coordinator.Coordinator
import no.iktdev.mediaprocessing.coordinator.CoordinatorEventListener import no.iktdev.mediaprocessing.coordinator.CoordinatorEventListener
import no.iktdev.mediaprocessing.coordinator.utils.log import no.iktdev.mediaprocessing.coordinator.log
import no.iktdev.mediaprocessing.shared.common.SharedConfig import no.iktdev.mediaprocessing.shared.common.SharedConfig
import no.iktdev.mediaprocessing.shared.common.parsing.FileNameDeterminate import no.iktdev.mediaprocessing.shared.common.parsing.FileNameDeterminate
import no.iktdev.mediaprocessing.shared.common.parsing.NameHelper import no.iktdev.mediaprocessing.shared.common.parsing.NameHelper
import no.iktdev.mediaprocessing.shared.common.parsing.Regexes
import no.iktdev.mediaprocessing.shared.common.parsing.isCharOnlyUpperCase
import no.iktdev.mediaprocessing.shared.common.contract.Events import no.iktdev.mediaprocessing.shared.common.contract.Events
import no.iktdev.mediaprocessing.shared.common.contract.data.* import no.iktdev.mediaprocessing.shared.common.contract.data.*
import no.iktdev.mediaprocessing.shared.common.contract.data.EpisodeInfo import no.iktdev.mediaprocessing.shared.common.contract.data.EpisodeInfo
@ -68,7 +66,6 @@ class MediaOutInformationTaskListener: CoordinatorEventListener() {
val result = if (vi != null) { val result = if (vi != null) {
MediaInfoReceived( MediaInfoReceived(
outDirectory = pm.getOutputDirectory().absolutePath,
info = vi info = vi
).let { MediaOutInformationConstructedEvent( ).let { MediaOutInformationConstructedEvent(
metadata = event.makeDerivedEventInfo(EventStatus.Success, getProducerName()), metadata = event.makeDerivedEventInfo(EventStatus.Success, getProducerName()),
@ -135,7 +132,6 @@ class MediaOutInformationTaskListener: CoordinatorEventListener() {
val filteredMetaTitles = metaTitles.filter { it.lowercase().contains(baseInfo.title.lowercase()) || NameHelper.normalize(it).lowercase().contains(baseInfo.title.lowercase()) } val filteredMetaTitles = metaTitles.filter { it.lowercase().contains(baseInfo.title.lowercase()) || NameHelper.normalize(it).lowercase().contains(baseInfo.title.lowercase()) }
//val viableFileTitles = filteredMetaTitles.filter { !it.isCharOnlyUpperCase() }
return if (collection == baseInfo.title) { return if (collection == baseInfo.title) {
collection collection
@ -162,10 +158,6 @@ class MediaOutInformationTaskListener: CoordinatorEventListener() {
} }
} }
fun getOutputDirectory() = SharedConfig.outgoingContent.using(NameHelper.normalize(getCollection()))
} }

View File

@ -13,13 +13,11 @@ import java.io.File
class EncodeWorkArgumentsMapping( class EncodeWorkArgumentsMapping(
val inputFile: String, val inputFile: String,
val outFileFullName: String, val outFileFullName: String,
val outFileAbsolutePathFile: File,
val streams: ParsedMediaStreams, val streams: ParsedMediaStreams,
val preference: EncodingPreference val preference: EncodingPreference
) { ) {
fun getArguments(): EncodeArgumentData? { fun getArguments(): EncodeArgumentData? {
val outVideoFileAbsolutePath = outFileAbsolutePathFile.using("${outFileFullName}.mp4").absolutePath
val vaas = VideoAndAudioSelector(streams, preference) val vaas = VideoAndAudioSelector(streams, preference)
val vArg = vaas.getVideoStream() val vArg = vaas.getVideoStream()
?.let { VideoArguments(it, streams, preference.video).getVideoArguments() } ?.let { VideoArguments(it, streams, preference.video).getVideoArguments() }
@ -32,7 +30,7 @@ class EncodeWorkArgumentsMapping(
} else { } else {
EncodeArgumentData( EncodeArgumentData(
inputFile = inputFile, inputFile = inputFile,
outputFile = outVideoFileAbsolutePath, outputFileName = "${outFileFullName}.mp4",
arguments = vaArgs arguments = vaArgs
) )
} }

View File

@ -9,19 +9,18 @@ import java.io.File
class ExtractWorkArgumentsMapping( class ExtractWorkArgumentsMapping(
val inputFile: String, val inputFile: String,
val outFileFullName: String, val outFileFullName: String,
val outFileAbsolutePathFile: File,
val streams: ParsedMediaStreams val streams: ParsedMediaStreams
) { ) {
fun getArguments(): List<ExtractArgumentData> { fun getArguments(): List<ExtractArgumentData> {
val subDir = outFileAbsolutePathFile.using("sub")
val sArg = SubtitleArguments(streams.subtitleStream).getSubtitleArguments() val sArg = SubtitleArguments(streams.subtitleStream).getSubtitleArguments()
val entries = sArg.map { val entries = sArg.map {
ExtractArgumentData( ExtractArgumentData(
inputFile = inputFile, inputFile = inputFile,
language = it.language,
arguments = it.codecParameters + it.optionalParameters + listOf("-map", "0:s:${it.index}"), arguments = it.codecParameters + it.optionalParameters + listOf("-map", "0:s:${it.index}"),
outputFile = subDir.using(it.language, "${outFileFullName}.${it.format}").absolutePath outputFileName = "${outFileFullName}.${it.language}.${it.format}"
) )
} }

View File

@ -0,0 +1,129 @@
package no.iktdev.mediaprocessing.coordinator.tasksV2.mapping.store
import mu.KotlinLogging
import no.iktdev.eventi.database.executeOrException
import no.iktdev.eventi.database.withTransaction
import no.iktdev.mediaprocessing.coordinator.getStoreDatabase
import no.iktdev.mediaprocessing.shared.common.contract.reader.MetadataDto
import no.iktdev.mediaprocessing.shared.common.contract.reader.VideoDetails
import no.iktdev.mediaprocessing.shared.common.parsing.NameHelper
import no.iktdev.streamit.library.db.insertWithSuccess
import no.iktdev.streamit.library.db.query.CatalogQuery
import no.iktdev.streamit.library.db.query.MovieQuery
import no.iktdev.streamit.library.db.query.SerieQuery
import no.iktdev.streamit.library.db.tables.catalog
import no.iktdev.streamit.library.db.tables.serie
import org.jetbrains.exposed.exceptions.ExposedSQLException
import org.jetbrains.exposed.sql.*
import java.sql.SQLIntegrityConstraintViolationException
object ContentCatalogStore {
val log = KotlinLogging.logger {}
/**
* Given a list of titles and type,
* the codes purpose is to find the matching collection in the catalog by title
*/
fun getCollectionByTitleAndType(type: String, titles: List<String>): String? {
return withTransaction(getStoreDatabase()) {
catalog.select {
(catalog.type eq type) and
((catalog.title inList titles) or
(catalog.collection inList titles))
}.map {
it[catalog.collection]
}.firstOrNull()
}
}
private fun getCover(collection: String, type: String): String? {
return withTransaction(getStoreDatabase()) {
catalog.select {
(catalog.collection eq collection) and
(catalog.type eq type)
}.map { it[catalog.cover] }.firstOrNull()
}
}
fun storeCatalog(title: String, collection: String, type: String, cover: String?, genres: String?): Int? {
withTransaction(getStoreDatabase()) {
val existingRow = catalog.select {
(catalog.collection eq collection) and
(catalog.type eq type)
}.firstOrNull()
if (existingRow == null) {
catalog.insertIgnore {
it[catalog.title] = title
it[catalog.cover] = cover
it[catalog.type] = type
it[catalog.collection] = collection
it[catalog.genres] = genres
}
} else {
val id = existingRow[catalog.id]
val storedTitle = existingRow[catalog.title]
val useCover = existingRow[catalog.cover] ?: cover
val useGenres = existingRow[catalog.genres] ?: genres
catalog.update({
(catalog.id eq id) and
(catalog.collection eq collection)
}) {
it[catalog.cover] = useCover
it[catalog.genres] = useGenres
}
}
}
return getId(title, collection, type)
}
private fun storeMovie(catalogId: Int, videoDetails: VideoDetails) {
val iid = MovieQuery(videoDetails.fileName).insertAndGetId() ?: run {
log.error { "Movie id was not returned!" }
return
}
withTransaction(getStoreDatabase()) {
catalog.update({
(catalog.id eq catalogId)
}) {
it[catalog.iid] = iid
}
}
}
private fun storeSerie(collection: String, videoDetails: VideoDetails) {
val serieInfo = videoDetails.serieInfo ?: run {
log.error { "serieInfo in videoDetails is null!" }
return
}
val insert = withTransaction(getStoreDatabase()) {
serie.insertIgnore {
it[title] = serieInfo.episodeTitle
it[episode] = serieInfo.episodeNumber
it[season] = serieInfo.seasonNumber
it[video] = videoDetails.fileName
it[serie.collection] = collection
}
}
}
fun storeMedia(title: String, collection: String, type: String, videoDetails: VideoDetails) {
val catalogId = getId(title, collection, type) ?: return
when (type) {
"movie" -> storeMovie(catalogId, videoDetails)
"serie" -> storeSerie(collection, videoDetails)
}
}
fun getId(title: String, collection: String, type: String): Int? {
return no.iktdev.streamit.library.db.withTransaction {
catalog.select { catalog.title eq title }.andWhere {
catalog.type eq type
}.map { it[catalog.id].value }.firstOrNull()
}
}
}

View File

@ -0,0 +1,102 @@
package no.iktdev.mediaprocessing.coordinator.tasksV2.mapping.store
import mu.KotlinLogging
import no.iktdev.eventi.data.dataAs
import no.iktdev.exfl.using
import no.iktdev.mediaprocessing.shared.common.SharedConfig
import no.iktdev.mediaprocessing.shared.common.contract.Events
import no.iktdev.mediaprocessing.shared.common.contract.data.*
import no.iktdev.mediaprocessing.shared.common.moveTo
import no.iktdev.mediaprocessing.shared.common.notExist
import java.io.File
class ContentCompletionMover(val collection: String, val events: List<Event>) {
val log = KotlinLogging.logger {}
val storeFolder = SharedConfig.outgoingContent.using(collection)
init {
if (storeFolder.notExist()) {
log.info { "Creating missing folders for path ${storeFolder.absolutePath}" }
storeFolder.mkdirs()
}
}
/**
* @return Pair<OldPath, NewPath> or null if no file found
*/
fun moveVideo(): Pair<String, String>? {
val encodedFile = events.find { it.eventType == Events.EventWorkEncodePerformed }?.dataAs<EncodedData>()?.outputFile?.let {
File(it)
} ?: return null
if (!encodedFile.exists()) {
log.error { "Provided file ${encodedFile.absolutePath} does not exist at the given location" }
return null
}
val storeFile = storeFolder.using(encodedFile.name)
val result = encodedFile.moveTo(storeFile) {
}
return if (result) Pair(encodedFile.absolutePath, storeFile.absolutePath) else null
}
fun moveCover(): Pair<String, String>? {
val coverFile = events.find { it.eventType == Events.EventWorkDownloadCoverPerformed }?.
az<MediaCoverDownloadedEvent>()?.data?.absoluteFilePath?.let {
File(it)
} ?: return null
if (coverFile.notExist()) {
log.error { "Provided file ${coverFile.absolutePath} does not exist at the given location" }
return null
}
val storeFile = storeFolder.using(coverFile.name)
val result = coverFile.moveTo(storeFile)
return if (result) Pair(coverFile.absolutePath, storeFile.absolutePath) else null
}
fun getMovableSubtitles(): Map<String, List<File>> {
val extracted =
events.filter { it.eventType == Events.EventWorkExtractPerformed }.mapNotNull { it.dataAs<ExtractedData>() }
val converted =
events.filter { it.eventType == Events.EventWorkConvertPerformed }.mapNotNull { it.dataAs<ConvertedData>() }
return extracted.groupBy { it.language }.mapValues { v -> v.value.map { File(it.outputFile) } } +
converted.groupBy { it.language }.mapValues { v -> v.value.flatMap { it.outputFiles }.map { File(it) } }
}
data class MovedSubtitle(
val language: String,
val source: File,
val destination: File
)
fun moveSubtitles(): List<MovedSubtitle>? {
val subtitleFolder = storeFolder.using("sub")
val moved: MutableList<MovedSubtitle> = mutableListOf()
val subtitles = getMovableSubtitles()
if (subtitles.isEmpty() || subtitles.values.isEmpty()) {
return null
}
for ((lang, files) in subtitles) {
val languageFolder = subtitleFolder.using(lang).also {
if (it.notExist()) {
it.mkdirs()
}
}
for (file in files) {
val storeFile = languageFolder.using(file.name)
val success = file.moveTo(storeFile)
if (success) {
moved.add(MovedSubtitle(lang, file, storeFile))
}
}
}
return moved
}
}

View File

@ -0,0 +1,20 @@
package no.iktdev.mediaprocessing.coordinator.tasksV2.mapping.store
import no.iktdev.eventi.database.withTransaction
import no.iktdev.mediaprocessing.coordinator.getStoreDatabase
import no.iktdev.streamit.library.db.query.GenreQuery
object ContentGenresStore {
fun storeAndGetIds(genres: List<String>): String? {
return try {
withTransaction(getStoreDatabase()) {
val gq = GenreQuery( *genres.toTypedArray() )
gq.insertAndGetIds()
gq.getIds().joinToString(",")
}
} catch (e: Exception) {
e.printStackTrace()
return null
}
}
}

View File

@ -0,0 +1,20 @@
package no.iktdev.mediaprocessing.coordinator.tasksV2.mapping.store
import no.iktdev.eventi.database.executeOrException
import no.iktdev.eventi.database.withTransaction
import no.iktdev.mediaprocessing.coordinator.getStoreDatabase
import no.iktdev.mediaprocessing.shared.common.contract.reader.SummaryInfo
import no.iktdev.streamit.library.db.query.SummaryQuery
object ContentMetadataStore {
fun storeSummary(catalogId: Int, summaryInfo: SummaryInfo) {
val result = executeOrException(getStoreDatabase().database) {
SummaryQuery(
cid = catalogId,
language = summaryInfo.language,
description = summaryInfo.summary
).insert()
}
}
}

View File

@ -0,0 +1,24 @@
package no.iktdev.mediaprocessing.coordinator.tasksV2.mapping.store
import no.iktdev.eventi.database.executeWithStatus
import no.iktdev.mediaprocessing.coordinator.getStoreDatabase
import no.iktdev.streamit.library.db.query.SubtitleQuery
import no.iktdev.streamit.library.db.tables.subtitle
import org.jetbrains.exposed.sql.insert
import java.io.File
object ContentSubtitleStore {
fun storeSubtitles(collection: String, language: String, destinationFile: File): Boolean {
return executeWithStatus (getStoreDatabase()) {
subtitle.insert {
it[this.associatedWithVideo] = destinationFile.nameWithoutExtension
it[this.language] = language
it[this.collection] = collection
it[this.format] = destinationFile.extension.uppercase()
it[this.subtitle] = destinationFile.name
}
}
}
}

View File

@ -0,0 +1,42 @@
package no.iktdev.mediaprocessing.coordinator.tasksV2.mapping.store
import no.iktdev.eventi.database.withTransaction
import no.iktdev.mediaprocessing.coordinator.getStoreDatabase
import no.iktdev.mediaprocessing.shared.common.parsing.NameHelper
import no.iktdev.streamit.library.db.tables.titles
import org.jetbrains.exposed.sql.insertIgnore
import org.jetbrains.exposed.sql.or
import org.jetbrains.exposed.sql.select
object ContentTitleStore {
fun store(mainTitle: String, otherTitles: List<String>) {
try {
withTransaction(getStoreDatabase()) {
val titlesToUse = otherTitles + listOf(
NameHelper.normalize(mainTitle)
).filter { it != mainTitle }
titlesToUse.forEach { t ->
titles.insertIgnore {
it[masterTitle] = mainTitle
it[alternativeTitle] = t
}
}
}
} catch (e: Exception) {
e.printStackTrace()
}
}
fun findMasterTitles(titleList: List<String>): List<String> {
return withTransaction(getStoreDatabase()) {
titles.select {
(titles.alternativeTitle inList titleList) or
(titles.masterTitle inList titleList)
}.map {
it[titles.masterTitle]
}.distinctBy { it }
} ?: emptyList()
}
}

View File

@ -0,0 +1,113 @@
package no.iktdev.mediaprocessing.coordinator.tasksV2.validator
import no.iktdev.eventi.data.dataAs
import no.iktdev.eventi.data.isSuccessful
import no.iktdev.mediaprocessing.shared.common.contract.Events
import no.iktdev.mediaprocessing.shared.common.contract.data.*
import no.iktdev.mediaprocessing.shared.common.contract.dto.StartOperationEvents
import no.iktdev.mediaprocessing.shared.common.contract.dto.SubtitleFormats
import java.io.File
/**
* Validates whether all the required work has been created and processed in accordance with expected behaviour and sequence
*/
object CompletionValidator {
/**
* Checks whether it requires encode or extract or both, and it has created events with args
*/
fun req1(started: MediaProcessStartEvent, events: List<Event>): Boolean {
val encodeFulfilledOrSkipped = if (started.data?.operations?.contains(StartOperationEvents.ENCODE) == true) {
events.any { it.eventType == Events.EventMediaParameterEncodeCreated }
} else true
val extractFulfilledOrSkipped = if (started.data?.operations?.contains(StartOperationEvents.EXTRACT) == true) {
events.any { it.eventType == Events.EventMediaParameterExtractCreated }
} else true
if (!encodeFulfilledOrSkipped || !extractFulfilledOrSkipped) {
return false
} else return true
}
/**
* Checks whether work that was supposed to be created has been created.
* Checks if all subtitles that can be processed has been created if convert is set.
*/
fun req2(operations: List<StartOperationEvents>, events: List<Event>): Boolean {
if (StartOperationEvents.ENCODE in operations) {
val encodeParamter = events.find { it.eventType == Events.EventMediaParameterEncodeCreated }?.az<EncodeArgumentCreatedEvent>()
val encodeWork = events.find { it.eventType == Events.EventWorkEncodeCreated }
if (encodeParamter?.isSuccessful() == true && (encodeWork == null))
return false
}
val extractParamter = events.find { it.eventType == Events.EventMediaParameterExtractCreated }?.az<ExtractArgumentCreatedEvent>()
val extractWork = events.filter { it.eventType == Events.EventWorkExtractCreated }
if (StartOperationEvents.EXTRACT in operations) {
if (extractParamter?.isSuccessful() == true && extractParamter.data?.size != extractWork.size)
return false
}
if (StartOperationEvents.CONVERT in operations) {
val convertWork = events.filter { it.eventType == Events.EventWorkConvertCreated }
val supportedSubtitleFormats = SubtitleFormats.entries.map { it.name }
val eventsSupportsConvert = extractWork.filter { it.data is ExtractArgumentData }
.filter { (it.dataAs<ExtractArgumentData>()?.outputFileName?.let { f -> File(f).extension.uppercase() } in supportedSubtitleFormats) }
if (convertWork.size != eventsSupportsConvert.size)
return false
}
return true
}
/**
* Checks whether all work that has been created has been completed
*/
fun req3(operations: List<StartOperationEvents>, events: List<Event>): Boolean {
if (StartOperationEvents.ENCODE in operations) {
val encodeWork = events.filter { it.eventType == Events.EventWorkEncodeCreated }
val encodePerformed = events.filter { it.eventType == Events.EventWorkEncodePerformed }
if (encodePerformed.size < encodeWork.size)
return false
}
if (StartOperationEvents.EXTRACT in operations) {
val extractWork = events.filter { it.eventType == Events.EventWorkExtractCreated }
val extractPerformed = events.filter { it.eventType == Events.EventWorkExtractPerformed }
if (extractPerformed.size < extractWork.size)
return false
}
if (StartOperationEvents.CONVERT in operations) {
val convertWork = events.filter { it.eventType == Events.EventWorkConvertCreated }
val convertPerformed = events.filter { it.eventType == Events.EventWorkConvertPerformed }
if (convertPerformed.size < convertWork.size)
return false
}
return true
}
/**
* Checks if metadata has cover, if so, 2 events are expected
*/
fun req4(events: List<Event>): Boolean {
val metadata = events.find { it.eventType == Events.EventMediaMetadataSearchPerformed }
if (metadata?.isSuccessful() != true) {
return true
}
val hasCover = metadata.dataAs<pyMetadata>()?.cover != null
if (hasCover == false) {
return true
}
if (events.any { it.eventType == Events.EventMediaReadOutCover } && events.any { it.eventType == Events.EventWorkDownloadCoverPerformed }) {
return true
}
return false
}
}

View File

@ -1,100 +0,0 @@
package no.iktdev.mediaprocessing.coordinator.reader
/*
import com.google.gson.Gson
import no.iktdev.mediaprocessing.shared.kafka.core.CoordinatorProducer
import no.iktdev.mediaprocessing.shared.common.contract.ProcessType
import no.iktdev.mediaprocessing.shared.kafka.dto.events_result.BaseInfoPerformed
import no.iktdev.mediaprocessing.shared.kafka.dto.events_result.ProcessStarted
import no.iktdev.mediaprocessing.shared.kafka.dto.Status
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
import org.mockito.Mock
import org.skyscreamer.jsonassert.JSONAssert
import org.springframework.beans.factory.annotation.Autowired
import java.io.File
class BaseInfoFromFileTest {
@Autowired
private lateinit var testBase: KafkaTestBase
@Mock
lateinit var coordinatorProducer: CoordinatorProducer
val baseInfoFromFile = BaseInfoFromFile(coordinatorProducer)
@Test
fun testReadFileInfo() {
val input = ProcessStarted(
Status.COMPLETED, ProcessType.FLOW,
File("/var/cache/[POTATO] Kage no Jitsuryokusha ni Naritakute! S2 - 01 [h265].mkv").absolutePath
)
val result = baseInfoFromFile.readFileInfo(input)
assertThat(result).isInstanceOf(BaseInfoPerformed::class.java)
val asResult = result as BaseInfoPerformed
assertThat(result.status).isEqualTo(Status.COMPLETED)
assertThat(asResult.title).isEqualTo("Kage no Jitsuryokusha ni Naritakute!")
assertThat(asResult.sanitizedName).isEqualTo("Kage no Jitsuryokusha ni Naritakute! S2 - 01")
}
@ParameterizedTest
@MethodSource("names")
fun test(data: TestInfo) {
val gson = Gson()
val result = baseInfoFromFile.readFileInfo(data.input)
JSONAssert.assertEquals(
data.expected,
gson.toJson(result),
false
)
}
data class TestInfo(
val input: ProcessStarted,
val expected: String
)
companion object {
@JvmStatic
private fun names(): List<Named<TestInfo>> {
return listOf(
Named.of(
"Potato", TestInfo(
ProcessStarted(
Status.COMPLETED, ProcessType.FLOW,
"E:\\input\\Top Clown Findout.1080p.H264.AAC5.1.mkv"
),
"""
{
"status": "COMPLETED",
"title": "Top Clown Findout",
"sanitizedName": "Top Clown Findout"
}
""".trimIndent()
)
),
Named.of("Filename with UHD wild tag", TestInfo(
ProcessStarted(
Status.COMPLETED, ProcessType.FLOW,
"E:\\input\\Wicked.Potato.Chapter.1.2023.UHD.BluRay.2160p.DDP.7.1.DV.HDR.x265.mp4"
),
"""
{
"status": "COMPLETED",
"title": "Wicked Potato Chapter 1",
"sanitizedName": "Wicked Potato Chapter 1"
}
""".trimIndent()
)
)
)
}
}
}*/

View File

@ -20,7 +20,6 @@ class EncodeWorkArgumentsMappingTest {
val parser = EncodeWorkArgumentsMapping( val parser = EncodeWorkArgumentsMapping(
"potato.mkv", "potato.mkv",
"potato.mp4", "potato.mp4",
File(".\\potato.mp4"),
event.az<MediaFileStreamsParsedEvent>()!!.data!!, event.az<MediaFileStreamsParsedEvent>()!!.data!!,
EncodingPreference(VideoPreference(), AudioPreference()) EncodingPreference(VideoPreference(), AudioPreference())
) )

View File

@ -2,8 +2,10 @@ package no.iktdev.mediaprocessing.processer.ffmpeg
import kotlinx.coroutines.cancel import kotlinx.coroutines.cancel
import mu.KLogger import mu.KLogger
import no.iktdev.exfl.using
import no.iktdev.mediaprocessing.processer.taskManager import no.iktdev.mediaprocessing.processer.taskManager
import no.iktdev.mediaprocessing.shared.common.ClaimableTask import no.iktdev.mediaprocessing.shared.common.ClaimableTask
import no.iktdev.mediaprocessing.shared.common.SharedConfig
import no.iktdev.mediaprocessing.shared.common.TaskQueueListener import no.iktdev.mediaprocessing.shared.common.TaskQueueListener
import no.iktdev.mediaprocessing.shared.common.getComputername import no.iktdev.mediaprocessing.shared.common.getComputername
import no.iktdev.mediaprocessing.shared.common.services.TaskService import no.iktdev.mediaprocessing.shared.common.services.TaskService
@ -18,6 +20,10 @@ abstract class FfmpegTaskService: TaskService(), FfmpegListener {
protected var runner: FfmpegRunner? = null protected var runner: FfmpegRunner? = null
fun getTemporaryStoreFile(fileName: String): File {
return SharedConfig.cachedContent.using(fileName)
}
override fun onTaskAvailable(data: ClaimableTask) { override fun onTaskAvailable(data: ClaimableTask) {
if (runner?.isWorking() == true) { if (runner?.isWorking() == true) {
//log.info { "Worker is already running.., will not consume" } //log.info { "Worker is already running.., will not consume" }

View File

@ -62,7 +62,7 @@ class EncodeService(
fun startEncode(event: Task) { fun startEncode(event: Task) {
val ffwrc = event.data as EncodeArgumentData val ffwrc = event.data as EncodeArgumentData
val outFile = File(ffwrc.outputFile) val outFile = getTemporaryStoreFile(ffwrc.outputFileName)
outFile.parentFile.mkdirs() outFile.parentFile.mkdirs()
if (!logDir.exists()) { if (!logDir.exists()) {
logDir.mkdirs() logDir.mkdirs()
@ -75,7 +75,7 @@ class EncodeService(
log.info { "Claim successful for ${event.referenceId} encode" } log.info { "Claim successful for ${event.referenceId} encode" }
runner = FfmpegRunner( runner = FfmpegRunner(
inputFile = ffwrc.inputFile, inputFile = ffwrc.inputFile,
outputFile = ffwrc.outputFile, outputFile = outFile.absolutePath,
arguments = ffwrc.arguments, arguments = ffwrc.arguments,
logDir = logDir, listener = this logDir = logDir, listener = this
) )
@ -83,7 +83,7 @@ class EncodeService(
if (ffwrc.arguments.firstOrNull() != "-y") { if (ffwrc.arguments.firstOrNull() != "-y") {
this.onError( this.onError(
ffwrc.inputFile, ffwrc.inputFile,
"${this::class.java.simpleName} identified the file as already existing, either allow overwrite or delete the offending file: ${ffwrc.outputFile}" "${this::class.java.simpleName} identified the file as already existing, either allow overwrite or delete the offending file: ${outFile.absolutePath}"
) )
// Setting consumed to prevent spamming // Setting consumed to prevent spamming
taskManager.markTaskAsCompleted(event.referenceId, event.eventId, Status.ERROR) taskManager.markTaskAsCompleted(event.referenceId, event.eventId, Status.ERROR)

View File

@ -15,6 +15,7 @@ import no.iktdev.mediaprocessing.shared.common.limitedWhile
import no.iktdev.mediaprocessing.shared.common.database.cal.Status import no.iktdev.mediaprocessing.shared.common.database.cal.Status
import no.iktdev.mediaprocessing.shared.common.task.Task import no.iktdev.mediaprocessing.shared.common.task.Task
import no.iktdev.mediaprocessing.shared.common.contract.data.ExtractArgumentData import no.iktdev.mediaprocessing.shared.common.contract.data.ExtractArgumentData
import no.iktdev.mediaprocessing.shared.common.contract.data.ExtractWorkCreatedEvent
import no.iktdev.mediaprocessing.shared.common.contract.data.ExtractWorkPerformedEvent import no.iktdev.mediaprocessing.shared.common.contract.data.ExtractWorkPerformedEvent
import no.iktdev.mediaprocessing.shared.common.contract.data.ExtractedData import no.iktdev.mediaprocessing.shared.common.contract.data.ExtractedData
import no.iktdev.mediaprocessing.shared.common.contract.dto.ProcesserEventInfo import no.iktdev.mediaprocessing.shared.common.contract.dto.ProcesserEventInfo
@ -60,9 +61,7 @@ class ExtractService(
fun startExtract(event: Task) { fun startExtract(event: Task) {
val ffwrc = event.data as ExtractArgumentData val ffwrc = event.data as ExtractArgumentData
val outFile = File(ffwrc.outputFile).also { val outputFile = getTemporaryStoreFile(ffwrc.outputFileName)
it.parentFile.mkdirs()
}
if (!logDir.exists()) { if (!logDir.exists()) {
logDir.mkdirs() logDir.mkdirs()
} }
@ -72,16 +71,16 @@ class ExtractService(
log.info { "Claim successful for ${event.referenceId} extract" } log.info { "Claim successful for ${event.referenceId} extract" }
runner = FfmpegRunner( runner = FfmpegRunner(
inputFile = ffwrc.inputFile, inputFile = ffwrc.inputFile,
outputFile = ffwrc.outputFile, outputFile = outputFile.absolutePath,
arguments = ffwrc.arguments, arguments = ffwrc.arguments,
logDir = logDir, logDir = logDir,
listener = this listener = this
) )
if (outFile.exists()) { if (outputFile.exists()) {
if (ffwrc.arguments.firstOrNull() != "-y") { if (ffwrc.arguments.firstOrNull() != "-y") {
this.onError( this.onError(
ffwrc.inputFile, ffwrc.inputFile,
"${this::class.java.simpleName} identified the file as already existing, either allow overwrite or delete the offending file: ${ffwrc.outputFile}" "${this::class.java.simpleName} identified the file as already existing, either allow overwrite or delete the offending file: ${outputFile.absolutePath}"
) )
// Setting consumed to prevent spamming // Setting consumed to prevent spamming
taskManager.markTaskAsCompleted(event.referenceId, event.eventId, Status.ERROR) taskManager.markTaskAsCompleted(event.referenceId, event.eventId, Status.ERROR)
@ -102,6 +101,8 @@ class ExtractService(
override fun onCompleted(inputFile: String, outputFile: String) { override fun onCompleted(inputFile: String, outputFile: String) {
val task = assignedTask ?: return val task = assignedTask ?: return
assert(task.data is ExtractArgumentData) { "Wrong data type found!" }
val taskData = task.data as ExtractArgumentData
log.info { "Extract completed for ${task.referenceId}" } log.info { "Extract completed for ${task.referenceId}" }
runBlocking { runBlocking {
var successfulComplete = false var successfulComplete = false
@ -119,6 +120,7 @@ class ExtractService(
source = getProducerName() source = getProducerName()
), ),
data = ExtractedData( data = ExtractedData(
taskData.language,
outputFile outputFile
) )
) )

View File

@ -6,6 +6,7 @@ import java.io.File
object SharedConfig { object SharedConfig {
var incomingContent: File = if (!System.getenv("DIRECTORY_CONTENT_INCOMING").isNullOrBlank()) File(System.getenv("DIRECTORY_CONTENT_INCOMING")) else File("/src/input") var incomingContent: File = if (!System.getenv("DIRECTORY_CONTENT_INCOMING").isNullOrBlank()) File(System.getenv("DIRECTORY_CONTENT_INCOMING")) else File("/src/input")
var cachedContent: File = if (!System.getenv("DIRECTORY_CONTENT_CACHE").isNullOrBlank()) File(System.getenv("DIRECTORY_CONTENT_CACHE")) else File("/src/cache")
val outgoingContent: File = if (!System.getenv("DIRECTORY_CONTENT_OUTGOING").isNullOrBlank()) File(System.getenv("DIRECTORY_CONTENT_OUTGOING")) else File("/src/output") val outgoingContent: File = if (!System.getenv("DIRECTORY_CONTENT_OUTGOING").isNullOrBlank()) File(System.getenv("DIRECTORY_CONTENT_OUTGOING")) else File("/src/output")
val ffprobe: String = System.getenv("SUPPORTING_EXECUTABLE_FFPROBE") ?: "ffprobe" val ffprobe: String = System.getenv("SUPPORTING_EXECUTABLE_FFPROBE") ?: "ffprobe"

View File

@ -5,9 +5,14 @@ import mu.KotlinLogging
import java.io.File import java.io.File
import java.io.RandomAccessFile import java.io.RandomAccessFile
import java.net.InetAddress import java.net.InetAddress
import java.util.zip.CRC32
private val logger = KotlinLogging.logger {} private val logger = KotlinLogging.logger {}
fun File.notExist(): Boolean {
return !this.exists()
}
fun isFileAvailable(file: File): Boolean { fun isFileAvailable(file: File): Boolean {
if (!file.exists()) return false if (!file.exists()) return false
var stream: RandomAccessFile? = null var stream: RandomAccessFile? = null
@ -59,4 +64,66 @@ fun silentTry(code: () -> Unit) {
try { try {
code.invoke() code.invoke()
} catch (_: Exception) {} } catch (_: Exception) {}
}
fun File.getCRC32(): Long {
val crc = CRC32()
this.inputStream().use { input ->
val buffer = ByteArray(1024)
var bytesRead: Int
while (input.read(buffer).also { bytesRead = it } != -1) {
crc.update(buffer, 0, bytesRead)
}
}
return crc.value
}
fun File.moveTo(destinationFile: File, onProgress: (Double) -> Unit = {}): Boolean {
require(this.exists())
require(destinationFile.notExist())
val tempDestinationFile = File(destinationFile.parentFile, "${destinationFile.name}.tmp")
val success: Boolean = run {
try {
val totalBytes = this.length()
var copiedBytes = 0L
this.inputStream().use { input ->
tempDestinationFile.outputStream().use { output ->
val buffer = ByteArray(1024)
var bytesRead: Int
while (input.read(buffer).also { bytesRead = it } != -1) {
output.write(buffer, 0, bytesRead)
copiedBytes += bytesRead
onProgress(copiedBytes.toDouble() / totalBytes * 100)
}
}
}
true
} catch (e: Exception) {
e.printStackTrace()
false
}
}
if (!success) {
return false
}
val sourceHash = this.getCRC32()
val tempFileHash = tempDestinationFile.getCRC32()
if (sourceHash == tempFileHash) {
if (!tempDestinationFile.renameTo(destinationFile)) {
logger.error { "${tempDestinationFile.name} failed to rename to ${destinationFile.name}" }
return false
}
this.delete()
} else {
logger.error { "${tempDestinationFile.name} failed integrity check" }
return false
}
return true
} }

View File

@ -14,6 +14,7 @@ data class ConvertWorkCreatedEvent(
data class ConvertData( data class ConvertData(
override val inputFile: String, override val inputFile: String,
val language: String,
val outputDirectory: String, val outputDirectory: String,
val outputFileName: String, val outputFileName: String,
val formats: List<SubtitleFormats> = emptyList(), val formats: List<SubtitleFormats> = emptyList(),

View File

@ -12,5 +12,6 @@ class ConvertWorkPerformed(
} }
data class ConvertedData( data class ConvertedData(
val language: String,
val outputFiles: List<String> val outputFiles: List<String>
) )

View File

@ -14,6 +14,6 @@ data class EncodeArgumentCreatedEvent(
data class EncodeArgumentData( data class EncodeArgumentData(
val arguments: List<String>, val arguments: List<String>,
val outputFile: String, val outputFileName: String,
override val inputFile: String override val inputFile: String
): TaskData() ): TaskData()

View File

@ -13,6 +13,7 @@ data class ExtractArgumentCreatedEvent(
data class ExtractArgumentData( data class ExtractArgumentData(
val arguments: List<String>, val arguments: List<String>,
val outputFile: String, val language: String,
val outputFileName: String,
override val inputFile: String override val inputFile: String
): TaskData() ): TaskData()

View File

@ -12,5 +12,6 @@ data class ExtractWorkPerformedEvent(
} }
data class ExtractedData( data class ExtractedData(
val language: String,
val outputFile: String val outputFile: String
) )

View File

@ -12,6 +12,5 @@ data class MediaCoverInfoReceivedEvent(
data class CoverDetails( data class CoverDetails(
val url: String, val url: String,
val outDir: String,
val outFileBaseName: String, val outFileBaseName: String,
) )

View File

@ -14,7 +14,6 @@ data class MediaOutInformationConstructedEvent(
data class MediaInfoReceived( data class MediaInfoReceived(
val info: JsonObject, val info: JsonObject,
val outDirectory: String,
) { ) {
fun toValueObject(): MediaInfo? { fun toValueObject(): MediaInfo? {
val type = info.get("type").asString val type = info.get("type").asString

View File

@ -7,6 +7,10 @@ import org.springframework.stereotype.Component
@Component @Component
class MockEventManager(dataSource: MockDataSource = MockDataSource()) : EventsManagerImpl<EventImpl>(dataSource) { class MockEventManager(dataSource: MockDataSource = MockDataSource()) : EventsManagerImpl<EventImpl>(dataSource) {
val events: MutableList<EventImpl> = mutableListOf() val events: MutableList<EventImpl> = mutableListOf()
override fun getAvailableReferenceIds(): List<String> {
throw RuntimeException("Not implemented")
}
override fun readAvailableEvents(): List<List<EventImpl>> { override fun readAvailableEvents(): List<List<EventImpl>> {
return listOf(events) return listOf(events)
} }