Compare commits
57 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
350c7684e3 | ||
|
6f504132ac | ||
|
d3fb5ff8d7 | ||
|
df28ef8be7 | ||
|
25d6700c1a | ||
|
71c2c69605 | ||
|
390ff41ac3 | ||
|
d9973ed90a | ||
|
36825a898d | ||
|
74fd8f3f75 | ||
|
e1420027c0 | ||
|
89f996100c | ||
|
61739a1afd | ||
|
3e92d0f6f5 | ||
|
01a6d855f9 | ||
|
6bb92eda01 | ||
|
f9611eae78 | ||
|
8f46af69e4 | ||
|
b4fdf14f26 | ||
|
9893aac067 | ||
|
e68eaf8c0e | ||
|
f32408109d | ||
|
97840f2a7d | ||
|
9ff8b19ab5 | ||
|
2653c0a314 | ||
|
80bbc0fa00 | ||
|
e9582d431c | ||
|
40474b2d3b | ||
|
073245a890 | ||
|
bd2b6d4bca | ||
|
45279e0a85 | ||
|
7de97ae7da | ||
|
a9baea5c16 | ||
|
06f69e6602 | ||
|
45d9e4b632 | ||
|
4d7dbf7f0f | ||
|
fe57d4defa | ||
|
3445bbaee4 | ||
|
38bc136088 | ||
|
50ac67113a | ||
|
627ae05def | ||
|
7198dfdfca | ||
|
acb902824d | ||
|
fef3b4d178 | ||
|
8627203748 | ||
|
8d0f17e18b | ||
|
6387d0e6a2 | ||
|
bdc7c9914c | ||
|
e1ad284fab | ||
|
e59bc24965 | ||
|
12d82da4fa | ||
|
7327f3f46e | ||
|
84801c8634 | ||
|
2b87b122c2 | ||
|
4cdef4f228 | ||
|
7264b7b8f0 | ||
|
d1bdf764d1 |
@ -192,3 +192,5 @@ csharp_space_between_method_call_empty_parameter_list_parentheses = false
|
|||||||
# Wrapping preferences
|
# Wrapping preferences
|
||||||
csharp_preserve_single_line_statements = true
|
csharp_preserve_single_line_statements = true
|
||||||
csharp_preserve_single_line_blocks = true
|
csharp_preserve_single_line_blocks = true
|
||||||
|
|
||||||
|
file_header_template = Copyright (C) 2024 Intro-Skipper contributors <intro-skipper.org>\nSPDX-License-Identifier: GPL-3.0-only.
|
67
.github/workflows/build.yml
vendored
67
.github/workflows/build.yml
vendored
@ -2,15 +2,8 @@ name: "Build Plugin"
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: ["master"]
|
branches:
|
||||||
paths-ignore:
|
- '*' # Triggers on any branch push
|
||||||
- "**/README.md"
|
|
||||||
- ".github/ISSUE_TEMPLATE/**"
|
|
||||||
- "docs/**"
|
|
||||||
- "images/**"
|
|
||||||
- "manifest.json"
|
|
||||||
pull_request:
|
|
||||||
branches: ["master"]
|
|
||||||
paths-ignore:
|
paths-ignore:
|
||||||
- "**/README.md"
|
- "**/README.md"
|
||||||
- ".github/ISSUE_TEMPLATE/**"
|
- ".github/ISSUE_TEMPLATE/**"
|
||||||
@ -39,6 +32,22 @@ jobs:
|
|||||||
|
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Read version from VERSION.txt
|
||||||
|
id: read-version
|
||||||
|
run: |
|
||||||
|
MAIN_VERSION=$(cat VERSION.txt)
|
||||||
|
echo "MAIN_VERSION=${MAIN_VERSION}"
|
||||||
|
echo "MAIN_VERSION=$MAIN_VERSION" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: Check for BETA file
|
||||||
|
id: check-beta
|
||||||
|
run: |
|
||||||
|
if [ -f "BETA" ]; then
|
||||||
|
echo "IS_BETA=true" >> $GITHUB_ENV
|
||||||
|
else
|
||||||
|
echo "IS_BETA=false" >> $GITHUB_ENV
|
||||||
|
fi
|
||||||
|
|
||||||
- name: Setup .NET
|
- name: Setup .NET
|
||||||
uses: actions/setup-dotnet@v4
|
uses: actions/setup-dotnet@v4
|
||||||
with:
|
with:
|
||||||
@ -51,17 +60,25 @@ jobs:
|
|||||||
|
|
||||||
- name: Minify HTML
|
- name: Minify HTML
|
||||||
run: |
|
run: |
|
||||||
npx html-minifier-terser --collapse-boolean-attributes --collapse-whitespace --collapse-inline-tag-whitespace --remove-comments --remove-optional-tags --remove-redundant-attributes --remove-script-type-attributes --remove-tag-whitespace --use-short-doctype --minify-css true --minify-js true -o ConfusedPolarBear.Plugin.IntroSkipper/Configuration/configPage.html ConfusedPolarBear.Plugin.IntroSkipper/Configuration/configPage.html
|
npx html-minifier-terser --collapse-boolean-attributes --collapse-whitespace --remove-comments --remove-optional-tags --remove-redundant-attributes --remove-script-type-attributes --remove-tag-whitespace --use-short-doctype --minify-css true --minify-js true -o IntroSkipper/Configuration/configPage.html IntroSkipper/Configuration/configPage.html
|
||||||
npx terser ConfusedPolarBear.Plugin.IntroSkipper/Configuration/inject.js -o ConfusedPolarBear.Plugin.IntroSkipper/Configuration/inject.js -c -m
|
npx terser IntroSkipper/Configuration/inject.js -o IntroSkipper/Configuration/inject.js -c -m
|
||||||
npx terser ConfusedPolarBear.Plugin.IntroSkipper/Configuration/visualizer.js -o ConfusedPolarBear.Plugin.IntroSkipper/Configuration/visualizer.js -c -m
|
npx terser IntroSkipper/Configuration/visualizer.js -o IntroSkipper/Configuration/visualizer.js -c -m
|
||||||
|
|
||||||
|
- name: Restore Beta dependencies
|
||||||
|
if: ${{env.IS_BETA == 'true' }}
|
||||||
|
run: |
|
||||||
|
dotnet nuget add source --username ${{ github.repository_owner }} --password ${{ secrets.GITHUB_TOKEN }} --store-password-in-clear-text --name jellyfin-pre "https://nuget.pkg.github.com/jellyfin/index.json"
|
||||||
|
dotnet tool install --global dotnet-outdated-tool
|
||||||
|
dotnet outdated -pre Always -u -inc Jellyfin
|
||||||
|
|
||||||
- name: Restore dependencies
|
- name: Restore dependencies
|
||||||
|
if: ${{env.IS_BETA == 'false' }}
|
||||||
run: dotnet restore
|
run: dotnet restore
|
||||||
|
|
||||||
- name: Embed version info
|
- name: Embed version info
|
||||||
run: |
|
run: |
|
||||||
GITHUB_SHA=${{ github.sha }}
|
GITHUB_SHA=${{ github.sha }}
|
||||||
sed -i "s/string\.Empty/\"$GITHUB_SHA\"/g" ConfusedPolarBear.Plugin.IntroSkipper/Helper/Commit.cs
|
sed -i "s/string\.Empty/\"$GITHUB_SHA\"/g" IntroSkipper/Helper/Commit.cs
|
||||||
|
|
||||||
- name: Retrieve commit identification
|
- name: Retrieve commit identification
|
||||||
run: |
|
run: |
|
||||||
@ -73,29 +90,19 @@ jobs:
|
|||||||
|
|
||||||
- name: Upload artifact
|
- name: Upload artifact
|
||||||
uses: actions/upload-artifact@v4.3.6
|
uses: actions/upload-artifact@v4.3.6
|
||||||
if: github.event_name != 'pull_request'
|
|
||||||
with:
|
with:
|
||||||
name: ConfusedPolarBear.Plugin.IntroSkipper-${{ env.GIT_HASH }}.dll
|
name: IntroSkipper-${{ env.GIT_HASH }}.dll
|
||||||
path: ConfusedPolarBear.Plugin.IntroSkipper/bin/Debug/net8.0/ConfusedPolarBear.Plugin.IntroSkipper.dll
|
path: IntroSkipper/bin/Debug/net8.0/IntroSkipper.dll
|
||||||
if-no-files-found: error
|
|
||||||
|
|
||||||
- name: Upload artifact
|
|
||||||
uses: actions/upload-artifact@v4.3.6
|
|
||||||
if: github.event_name == 'pull_request'
|
|
||||||
with:
|
|
||||||
name: ConfusedPolarBear.Plugin.IntroSkipper-${{ env.SANITIZED_BRANCH_NAME }}.dll
|
|
||||||
path: ConfusedPolarBear.Plugin.IntroSkipper/bin/Debug/net8.0/ConfusedPolarBear.Plugin.IntroSkipper.dll
|
|
||||||
retention-days: 7
|
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
|
|
||||||
- name: Create archive
|
- name: Create archive
|
||||||
if: github.event_name != 'pull_request'
|
if: startsWith(github.ref_name, '10.') # only do a preview release on 10.x branches
|
||||||
run: zip -j "intro-skipper-${{ env.GIT_HASH }}.zip" ConfusedPolarBear.Plugin.IntroSkipper/bin/Debug/net8.0/ConfusedPolarBear.Plugin.IntroSkipper.dll
|
run: zip -j "intro-skipper-${{ env.GIT_HASH }}.zip" IntroSkipper/bin/Debug/net8.0/IntroSkipper.dll
|
||||||
|
|
||||||
- name: Create/replace the preview release and upload artifacts
|
- name: Create/replace the preview release and upload artifacts
|
||||||
if: github.event_name != 'pull_request'
|
if: startsWith(github.ref_name, '10.') # only do a preview release on 10.x branches
|
||||||
run: |
|
run: |
|
||||||
gh release delete '10.9/preview' --cleanup-tag --yes || true
|
gh release delete "${{ env.MAIN_VERSION }}/preview" --cleanup-tag --yes || true
|
||||||
gh release create '10.9/preview' "intro-skipper-${{ env.GIT_HASH }}.zip" --prerelease --title "intro-skipper-${{ env.GIT_HASH }}" --notes "This is a prerelease version."
|
gh release create "${{ env.MAIN_VERSION }}/preview" "intro-skipper-${{ env.GIT_HASH }}.zip" --prerelease --title "intro-skipper-${{ env.GIT_HASH }}" --notes "This is a prerelease version." --target ${{ env.MAIN_VERSION }}
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
27
.github/workflows/codeql.yml
vendored
27
.github/workflows/codeql.yml
vendored
@ -2,15 +2,8 @@ name: "CodeQL"
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [master]
|
branches:
|
||||||
paths-ignore:
|
- '*' # Triggers on any branch push
|
||||||
- "**/README.md"
|
|
||||||
- ".github/ISSUE_TEMPLATE/**"
|
|
||||||
- "docs/**"
|
|
||||||
- "images/**"
|
|
||||||
- "manifest.json"
|
|
||||||
pull_request:
|
|
||||||
branches: [master]
|
|
||||||
paths-ignore:
|
paths-ignore:
|
||||||
- "**/README.md"
|
- "**/README.md"
|
||||||
- ".github/ISSUE_TEMPLATE/**"
|
- ".github/ISSUE_TEMPLATE/**"
|
||||||
@ -36,17 +29,31 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Check for BETA file
|
||||||
|
id: check-beta
|
||||||
|
run: |
|
||||||
|
if [ -f "BETA" ]; then
|
||||||
|
echo "IS_BETA=true" >> $GITHUB_ENV
|
||||||
|
else
|
||||||
|
echo "IS_BETA=false" >> $GITHUB_ENV
|
||||||
|
fi
|
||||||
|
|
||||||
- name: Setup .NET
|
- name: Setup .NET
|
||||||
uses: actions/setup-dotnet@v4
|
uses: actions/setup-dotnet@v4
|
||||||
with:
|
with:
|
||||||
dotnet-version: 8.0.x
|
dotnet-version: 8.0.x
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Restore Beta dependencies
|
||||||
|
if: ${{env.IS_BETA == 'true' }}
|
||||||
run: |
|
run: |
|
||||||
dotnet nuget add source --username ${{ github.repository_owner }} --password ${{ secrets.GITHUB_TOKEN }} --store-password-in-clear-text --name jellyfin-pre "https://nuget.pkg.github.com/jellyfin/index.json"
|
dotnet nuget add source --username ${{ github.repository_owner }} --password ${{ secrets.GITHUB_TOKEN }} --store-password-in-clear-text --name jellyfin-pre "https://nuget.pkg.github.com/jellyfin/index.json"
|
||||||
dotnet tool install --global dotnet-outdated-tool
|
dotnet tool install --global dotnet-outdated-tool
|
||||||
dotnet outdated -pre Always -u -inc Jellyfin
|
dotnet outdated -pre Always -u -inc Jellyfin
|
||||||
|
|
||||||
|
- name: Restore dependencies
|
||||||
|
if: ${{env.IS_BETA == 'false' }}
|
||||||
|
run: dotnet restore
|
||||||
|
|
||||||
- name: Initialize CodeQL
|
- name: Initialize CodeQL
|
||||||
uses: github/codeql-action/init@6db8d6351fd0be61f9ed8ebd12ccd35dcec51fea # v3.26.11
|
uses: github/codeql-action/init@6db8d6351fd0be61f9ed8ebd12ccd35dcec51fea # v3.26.11
|
||||||
with:
|
with:
|
||||||
|
42
.github/workflows/release.yml
vendored
42
.github/workflows/release.yml
vendored
@ -5,6 +5,7 @@ on:
|
|||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
|
packages: write
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
@ -13,6 +14,22 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Read version from VERSION.txt
|
||||||
|
id: read-version
|
||||||
|
run: |
|
||||||
|
MAIN_VERSION=$(cat VERSION.txt)
|
||||||
|
echo "MAIN_VERSION=${MAIN_VERSION}"
|
||||||
|
echo "MAIN_VERSION=$MAIN_VERSION" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: Check for BETA file
|
||||||
|
id: check-beta
|
||||||
|
run: |
|
||||||
|
if [ -f "BETA" ]; then
|
||||||
|
echo "IS_BETA=true" >> $GITHUB_ENV
|
||||||
|
else
|
||||||
|
echo "IS_BETA=false" >> $GITHUB_ENV
|
||||||
|
fi
|
||||||
|
|
||||||
- name: Setup .NET
|
- name: Setup .NET
|
||||||
uses: actions/setup-dotnet@v4
|
uses: actions/setup-dotnet@v4
|
||||||
with:
|
with:
|
||||||
@ -25,11 +42,19 @@ jobs:
|
|||||||
|
|
||||||
- name: Minify HTML
|
- name: Minify HTML
|
||||||
run: |
|
run: |
|
||||||
npx html-minifier-terser --collapse-boolean-attributes --collapse-whitespace --collapse-inline-tag-whitespace --remove-comments --remove-optional-tags --remove-redundant-attributes --remove-script-type-attributes --remove-tag-whitespace --use-short-doctype --minify-css true --minify-js true -o ConfusedPolarBear.Plugin.IntroSkipper/Configuration/configPage.html ConfusedPolarBear.Plugin.IntroSkipper/Configuration/configPage.html
|
npx html-minifier-terser --collapse-boolean-attributes --collapse-whitespace --remove-comments --remove-optional-tags --remove-redundant-attributes --remove-script-type-attributes --remove-tag-whitespace --use-short-doctype --minify-css true --minify-js true -o IntroSkipper/Configuration/configPage.html IntroSkipper/Configuration/configPage.html
|
||||||
npx terser ConfusedPolarBear.Plugin.IntroSkipper/Configuration/inject.js -o ConfusedPolarBear.Plugin.IntroSkipper/Configuration/inject.js -c -m
|
npx terser IntroSkipper/Configuration/inject.js -o IntroSkipper/Configuration/inject.js -c -m
|
||||||
npx terser ConfusedPolarBear.Plugin.IntroSkipper/Configuration/visualizer.js -o ConfusedPolarBear.Plugin.IntroSkipper/Configuration/visualizer.js -c -m
|
npx terser IntroSkipper/Configuration/visualizer.js -o IntroSkipper/Configuration/visualizer.js -c -m
|
||||||
|
|
||||||
|
- name: Restore Beta dependencies
|
||||||
|
if: ${{env.IS_BETA == 'true' }}
|
||||||
|
run: |
|
||||||
|
dotnet nuget add source --username ${{ github.repository_owner }} --password ${{ secrets.GITHUB_TOKEN }} --store-password-in-clear-text --name jellyfin-pre "https://nuget.pkg.github.com/jellyfin/index.json"
|
||||||
|
dotnet tool install --global dotnet-outdated-tool
|
||||||
|
dotnet outdated -pre Always -u -inc Jellyfin
|
||||||
|
|
||||||
- name: Restore dependencies
|
- name: Restore dependencies
|
||||||
|
if: ${{env.IS_BETA == 'false' }}
|
||||||
run: dotnet restore
|
run: dotnet restore
|
||||||
|
|
||||||
- name: Run update version
|
- name: Run update version
|
||||||
@ -40,23 +65,23 @@ jobs:
|
|||||||
- name: Embed version info
|
- name: Embed version info
|
||||||
run: |
|
run: |
|
||||||
GITHUB_SHA=${{ github.sha }}
|
GITHUB_SHA=${{ github.sha }}
|
||||||
sed -i "s/string\.Empty/\"$GITHUB_SHA\"/g" ConfusedPolarBear.Plugin.IntroSkipper/Helper/Commit.cs
|
sed -i "s/string\.Empty/\"$GITHUB_SHA\"/g" IntroSkipper/Helper/Commit.cs
|
||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
run: dotnet build --configuration Release --no-restore
|
run: dotnet build --configuration Release --no-restore
|
||||||
|
|
||||||
- name: Create archive
|
- name: Create archive
|
||||||
run: zip -j "intro-skipper-v${{ env.NEW_FILE_VERSION }}.zip" ConfusedPolarBear.Plugin.IntroSkipper/bin/Release/net8.0/ConfusedPolarBear.Plugin.IntroSkipper.dll
|
run: zip -j "intro-skipper-v${{ env.NEW_FILE_VERSION }}.zip" IntroSkipper/bin/Release/net8.0/IntroSkipper.dll
|
||||||
|
|
||||||
- name: Remove old release if exits
|
- name: Remove old release if exits
|
||||||
if: ${{ github.repository == 'intro-skipper/intro-skipper-test' }}
|
if: ${{ github.repository == 'intro-skipper/intro-skipper-test' }}
|
||||||
run: gh release delete "10.9/v${{ env.NEW_FILE_VERSION }}" --cleanup-tag --yes || true
|
run: gh release delete "${{ env.MAIN_VERSION }}/v${{ env.NEW_FILE_VERSION }}" --cleanup-tag --yes || true
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Create new release with tag
|
- name: Create new release with tag
|
||||||
if: github.event_name != 'pull_request'
|
if: github.event_name != 'pull_request'
|
||||||
run: gh release create "10.9/v${{ env.NEW_FILE_VERSION }}" "intro-skipper-v${{ env.NEW_FILE_VERSION }}.zip" --title "v${{ env.NEW_FILE_VERSION }}" --latest --generate-notes
|
run: gh release create "${{ env.MAIN_VERSION }}/v${{ env.NEW_FILE_VERSION }}" "intro-skipper-v${{ env.NEW_FILE_VERSION }}.zip" --title "v${{ env.NEW_FILE_VERSION }}" --latest --generate-notes --target ${{ env.MAIN_VERSION }}
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
@ -66,12 +91,13 @@ jobs:
|
|||||||
task-type: "updateManifest"
|
task-type: "updateManifest"
|
||||||
env:
|
env:
|
||||||
GITHUB_REPO_VISIBILITY: ${{ github.event.repository.visibility }}
|
GITHUB_REPO_VISIBILITY: ${{ github.event.repository.visibility }}
|
||||||
|
MAIN_VERSION: ${{ env.MAIN_VERSION }}
|
||||||
|
|
||||||
- name: Commit changes
|
- name: Commit changes
|
||||||
if: success()
|
if: success()
|
||||||
run: |
|
run: |
|
||||||
git config --global user.name "github-actions[bot]"
|
git config --global user.name "github-actions[bot]"
|
||||||
git config --global user.email "github-actions[bot]@users.noreply.github.com"
|
git config --global user.email "github-actions[bot]@users.noreply.github.com"
|
||||||
git add README.md manifest.json ConfusedPolarBear.Plugin.IntroSkipper/ConfusedPolarBear.Plugin.IntroSkipper.csproj .github/ISSUE_TEMPLATE/bug_report_form.yml
|
git add README.md manifest.json IntroSkipper/IntroSkipper.csproj .github/ISSUE_TEMPLATE/bug_report_form.yml
|
||||||
git commit -m "release v${{ env.NEW_FILE_VERSION }}"
|
git commit -m "release v${{ env.NEW_FILE_VERSION }}"
|
||||||
git push
|
git push
|
||||||
|
3
.gitignore
vendored
3
.gitignore
vendored
@ -8,3 +8,6 @@ docker/dist
|
|||||||
|
|
||||||
# Visual Studio
|
# Visual Studio
|
||||||
.vs/
|
.vs/
|
||||||
|
|
||||||
|
# JetBrains Rider
|
||||||
|
.idea/
|
||||||
|
11
.vscode/extensions.json
vendored
Normal file
11
.vscode/extensions.json
vendored
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"recommendations": [
|
||||||
|
"ms-dotnettools.csharp",
|
||||||
|
"editorconfig.editorconfig",
|
||||||
|
"github.vscode-github-actions",
|
||||||
|
"ms-dotnettools.vscode-dotnet-runtime",
|
||||||
|
"ms-dotnettools.csdevkit",
|
||||||
|
"eamodio.gitlens"
|
||||||
|
],
|
||||||
|
"unwantedRecommendations": []
|
||||||
|
}
|
9
.vscode/settings.json
vendored
Normal file
9
.vscode/settings.json
vendored
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"editor.insertSpaces": true,
|
||||||
|
"editor.tabSize": 4,
|
||||||
|
"files.encoding": "utf8",
|
||||||
|
"files.trimTrailingWhitespace": true,
|
||||||
|
"files.insertFinalNewline": true,
|
||||||
|
"editor.rulers": [],
|
||||||
|
"dotnet.defaultSolution": "IntroSkipper.sln"
|
||||||
|
}
|
@ -1,471 +0,0 @@
|
|||||||
const introSkipper = {
|
|
||||||
originalFetch: window.fetch.bind(window),
|
|
||||||
d: msg => console.debug("[intro skipper] ", msg),
|
|
||||||
setup() {
|
|
||||||
this.initializeState();
|
|
||||||
this.initializeObserver();
|
|
||||||
this.currentOption = localStorage.getItem('introskipperOption') || 'Show Button';
|
|
||||||
document.addEventListener("viewshow", this.viewShow.bind(this));
|
|
||||||
window.fetch = this.fetchWrapper.bind(this);
|
|
||||||
this.videoPositionChanged = this.videoPositionChanged.bind(this);
|
|
||||||
this.handleEscapeKey = this.handleEscapeKey.bind(this);
|
|
||||||
this.d("Registered hooks");
|
|
||||||
},
|
|
||||||
initializeState() {
|
|
||||||
Object.assign(this, { allowEnter: true, skipSegments: {}, videoPlayer: null, skipButton: null, osdElement: null, skipperData: null, currentEpisodeId: null, injectMetadata: false });
|
|
||||||
},
|
|
||||||
initializeObserver() {
|
|
||||||
this.observer = new MutationObserver(mutations => {
|
|
||||||
const actionSheet = mutations[mutations.length - 1].target.querySelector('.actionSheet');
|
|
||||||
if (actionSheet && !actionSheet.querySelector(`[data-id="${'introskipperMenu'}"]`)) this.injectIntroSkipperOptions(actionSheet);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
/** Wrapper around fetch() that retrieves skip segments for the currently playing item or metadata. */
|
|
||||||
async fetchWrapper(resource, options) {
|
|
||||||
const response = await this.originalFetch(resource, options);
|
|
||||||
this.processResource(resource);
|
|
||||||
return response;
|
|
||||||
},
|
|
||||||
async processResource(resource) {
|
|
||||||
try {
|
|
||||||
const url = new URL(resource);
|
|
||||||
const pathname = url.pathname;
|
|
||||||
if (pathname.includes("/PlaybackInfo")) {
|
|
||||||
this.d(`Retrieving skip segments from URL ${pathname}`);
|
|
||||||
const pathArr = pathname.split("/");
|
|
||||||
const id = pathArr[pathArr.indexOf("Items") + 1] || pathArr[3];
|
|
||||||
this.skipSegments = await this.secureFetch(`Episode/${id}/IntroSkipperSegments`);
|
|
||||||
this.d("Retrieved skip segments", this.skipSegments);
|
|
||||||
} else if (this.injectMetadata && pathname.includes("/MetadataEditor")) {
|
|
||||||
this.d(`Metadata editor detected, URL ${pathname}`);
|
|
||||||
const pathArr = pathname.split("/");
|
|
||||||
this.currentEpisodeId = pathArr[pathArr.indexOf("Items") + 1] || pathArr[3];
|
|
||||||
this.skipperData = await this.secureFetch(`Episode/${this.currentEpisodeId}/Timestamps`);
|
|
||||||
if (this.skipperData) {
|
|
||||||
requestAnimationFrame(() => {
|
|
||||||
const metadataFormFields = document.querySelector('.metadataFormFields');
|
|
||||||
metadataFormFields && this.injectSkipperFields(metadataFormFields);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error("Error processing", resource, e);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
/**
|
|
||||||
* Event handler that runs whenever the current view changes.
|
|
||||||
* Used to detect the start of video playback.
|
|
||||||
*/
|
|
||||||
viewShow() {
|
|
||||||
const location = window.location.hash;
|
|
||||||
this.d(`Location changed to ${location}`);
|
|
||||||
this.allowEnter = true;
|
|
||||||
this.injectMetadata = /#\/(tv|details|home|search)/.test(location);
|
|
||||||
if (location === "#/video") {
|
|
||||||
this.injectCss();
|
|
||||||
this.injectButton();
|
|
||||||
this.videoPlayer = document.querySelector("video");
|
|
||||||
if (this.videoPlayer) {
|
|
||||||
this.d("Hooking video timeupdate");
|
|
||||||
this.videoPlayer.addEventListener("timeupdate", this.videoPositionChanged);
|
|
||||||
this.osdElement = document.querySelector("div.videoOsdBottom")
|
|
||||||
this.observer.observe(document.body, { childList: true, subtree: false });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
this.observer.disconnect();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
/**
|
|
||||||
* Injects the CSS used by the skip intro button.
|
|
||||||
* Calling this function is a no-op if the CSS has already been injected.
|
|
||||||
*/
|
|
||||||
injectCss() {
|
|
||||||
if (document.querySelector("style#introSkipperCss")) {
|
|
||||||
this.d("CSS already added");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.d("Adding CSS");
|
|
||||||
const styleElement = document.createElement("style");
|
|
||||||
styleElement.id = "introSkipperCss";
|
|
||||||
styleElement.textContent = `
|
|
||||||
:root {
|
|
||||||
--rounding: 4px;
|
|
||||||
--accent: 0, 164, 220;
|
|
||||||
}
|
|
||||||
#skipIntro.upNextContainer {
|
|
||||||
width: unset;
|
|
||||||
margin: unset;
|
|
||||||
}
|
|
||||||
#skipIntro {
|
|
||||||
position: absolute;
|
|
||||||
bottom: 7.5em;
|
|
||||||
right: 5em;
|
|
||||||
background-color: transparent;
|
|
||||||
}
|
|
||||||
#skipIntro .emby-button {
|
|
||||||
color: #ffffff;
|
|
||||||
font-size: 110%;
|
|
||||||
background: rgba(0, 0, 0, 0.7);
|
|
||||||
border-radius: var(--rounding);
|
|
||||||
box-shadow: 0 0 4px rgba(0, 0, 0, 0.6);
|
|
||||||
transition: opacity 0.3s cubic-bezier(0.4,0,0.2,1),
|
|
||||||
transform 0.3s cubic-bezier(0.4,0,0.2,1),
|
|
||||||
background-color 0.2s ease-out,
|
|
||||||
box-shadow 0.2s ease-out;
|
|
||||||
opacity: 0;
|
|
||||||
transform: translateY(50%);
|
|
||||||
}
|
|
||||||
#skipIntro.show .emby-button {
|
|
||||||
opacity: 1;
|
|
||||||
transform: translateY(0);
|
|
||||||
}
|
|
||||||
#skipIntro .emby-button:hover {
|
|
||||||
background: rgb(var(--accent));
|
|
||||||
box-shadow: 0 0 8px rgba(var(--accent), 0.6);
|
|
||||||
filter: brightness(1.2);
|
|
||||||
}
|
|
||||||
#skipIntro .emby-button:focus {
|
|
||||||
background: rgb(var(--accent));
|
|
||||||
box-shadow: 0 0 8px rgba(var(--accent), 0.6);
|
|
||||||
}
|
|
||||||
#btnSkipSegmentText {
|
|
||||||
letter-spacing: 0.5px;
|
|
||||||
padding: 0 5px 0 5px;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
document.querySelector("head").appendChild(styleElement);
|
|
||||||
},
|
|
||||||
/**
|
|
||||||
* Inject the skip intro button into the video player.
|
|
||||||
* Calling this function is a no-op if the CSS has already been injected.
|
|
||||||
*/
|
|
||||||
async injectButton() {
|
|
||||||
// Ensure the button we're about to inject into the page doesn't conflict with a pre-existing one
|
|
||||||
const preExistingButton = document.querySelector("div.skipIntro");
|
|
||||||
if (preExistingButton) {
|
|
||||||
preExistingButton.style.display = "none";
|
|
||||||
}
|
|
||||||
if (document.querySelector(".btnSkipIntro.injected")) {
|
|
||||||
this.d("Button already added");
|
|
||||||
this.skipButton = document.querySelector("#skipIntro");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const config = await this.secureFetch("Intros/UserInterfaceConfiguration");
|
|
||||||
if (!config.SkipButtonVisible) {
|
|
||||||
this.d("Not adding button: not visible");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.d("Adding button");
|
|
||||||
this.skipButton = document.createElement("div");
|
|
||||||
this.skipButton.id = "skipIntro";
|
|
||||||
this.skipButton.classList.add("hide", "upNextContainer");
|
|
||||||
this.skipButton.addEventListener("click", this.doSkip.bind(this));
|
|
||||||
this.skipButton.addEventListener("keydown", this.eventHandler.bind(this));
|
|
||||||
this.skipButton.innerHTML = `
|
|
||||||
<button is="emby-button" type="button" class="btnSkipIntro injected">
|
|
||||||
<span id="btnSkipSegmentText"></span>
|
|
||||||
<span class="material-icons skip_next"></span>
|
|
||||||
</button>
|
|
||||||
`;
|
|
||||||
this.skipButton.dataset.Introduction = config.SkipButtonIntroText;
|
|
||||||
this.skipButton.dataset.Credits = config.SkipButtonEndCreditsText;
|
|
||||||
const controls = document.querySelector("div#videoOsdPage");
|
|
||||||
controls.appendChild(this.skipButton);
|
|
||||||
},
|
|
||||||
/** Tests if the OSD controls are visible. */
|
|
||||||
osdVisible() {
|
|
||||||
return this.osdElement ? !this.osdElement.classList.contains("hide") : false;
|
|
||||||
},
|
|
||||||
/** Get the currently playing skippable segment. */
|
|
||||||
getCurrentSegment(position) {
|
|
||||||
for (const [key, segment] of Object.entries(this.skipSegments)) {
|
|
||||||
if ((position > segment.ShowSkipPromptAt && position < segment.HideSkipPromptAt - 1) ||
|
|
||||||
(this.osdVisible() && position > segment.IntroStart && position < segment.IntroEnd - 1)) {
|
|
||||||
segment.SegmentType = key;
|
|
||||||
return segment;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return { SegmentType: "None" };
|
|
||||||
},
|
|
||||||
overrideBlur(button) {
|
|
||||||
if (!button.originalBlur) {
|
|
||||||
button.originalBlur = button.blur;
|
|
||||||
button.blur = function() {
|
|
||||||
if (!this.contains(document.activeElement)) {
|
|
||||||
this.originalBlur();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
},
|
|
||||||
/** Playback position changed, check if the skip button needs to be displayed. */
|
|
||||||
videoPositionChanged() {
|
|
||||||
if (!this.skipButton) return;
|
|
||||||
const embyButton = this.skipButton.querySelector(".emby-button");
|
|
||||||
const segmentType = this.getCurrentSegment(this.videoPlayer.currentTime).SegmentType;
|
|
||||||
if (segmentType === "None" || this.currentOption === "Off" || !this.allowEnter) {
|
|
||||||
if (this.skipButton.classList.contains('show')) {
|
|
||||||
this.skipButton.classList.remove('show');
|
|
||||||
embyButton.addEventListener("transitionend", () => {
|
|
||||||
this.skipButton.classList.add("hide");
|
|
||||||
if (this.osdVisible()) {
|
|
||||||
this.osdElement.querySelector('button.btnPause').focus();
|
|
||||||
} else {
|
|
||||||
embyButton.originalBlur();
|
|
||||||
}
|
|
||||||
}, { once: true });
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (this.currentOption === "Automatically Skip" || (this.currentOption === "Button w/ auto PiP" && document.pictureInPictureElement)) {
|
|
||||||
this.doSkip();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.skipButton.querySelector("#btnSkipSegmentText").textContent = this.skipButton.dataset[segmentType];
|
|
||||||
if (!this.skipButton.classList.contains("hide")) {
|
|
||||||
if (!this.osdVisible() && !embyButton.contains(document.activeElement)) embyButton.focus();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
requestAnimationFrame(() => {
|
|
||||||
this.skipButton.classList.remove("hide");
|
|
||||||
requestAnimationFrame(() => {
|
|
||||||
this.skipButton.classList.add('show');
|
|
||||||
this.overrideBlur(embyButton);
|
|
||||||
embyButton.focus();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
},
|
|
||||||
/** Seeks to the end of the intro. */
|
|
||||||
doSkip() {
|
|
||||||
if (!this.allowEnter) return;
|
|
||||||
const segment = this.getCurrentSegment(this.videoPlayer.currentTime);
|
|
||||||
if (segment.SegmentType === "None") {
|
|
||||||
console.warn("[intro skipper] doSkip() called without an active segment");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.d(`Skipping ${segment.SegmentType}`);
|
|
||||||
this.allowEnter = false;
|
|
||||||
const seekedHandler = () => {
|
|
||||||
this.videoPlayer.removeEventListener('seeked', seekedHandler);
|
|
||||||
setTimeout(() => {
|
|
||||||
this.allowEnter = true;
|
|
||||||
}, 500);
|
|
||||||
};
|
|
||||||
this.videoPlayer.addEventListener('seeked', seekedHandler);
|
|
||||||
this.videoPlayer.currentTime = segment.SegmentType === "Credits" && this.videoPlayer.duration - segment.IntroEnd < 3
|
|
||||||
? this.videoPlayer.duration + 10
|
|
||||||
: segment.IntroEnd;
|
|
||||||
},
|
|
||||||
createButton(ref, id, innerHTML, clickHandler) {
|
|
||||||
const button = ref.cloneNode(true);
|
|
||||||
button.setAttribute('data-id', id);
|
|
||||||
button.innerHTML = innerHTML;
|
|
||||||
button.addEventListener('click', clickHandler);
|
|
||||||
return button;
|
|
||||||
},
|
|
||||||
closeSubmenu(fullscreen) {
|
|
||||||
document.querySelector('.dialogContainer').remove();
|
|
||||||
document.querySelector('.dialogBackdrop').remove()
|
|
||||||
document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Control' }));
|
|
||||||
if (!fullscreen) return;
|
|
||||||
document.removeEventListener('keydown', this.handleEscapeKey);
|
|
||||||
document.querySelector('.btnVideoOsdSettings').focus();
|
|
||||||
},
|
|
||||||
openSubmenu(ref, menu) {
|
|
||||||
const options = ['Show Button', 'Button w/ auto PiP', 'Automatically Skip', 'Off'];
|
|
||||||
const submenu = menu.cloneNode(true);
|
|
||||||
const scroller = submenu.querySelector('.actionSheetScroller');
|
|
||||||
scroller.innerHTML = '';
|
|
||||||
options.forEach(option => {
|
|
||||||
if (option !== 'Button w/ auto PiP' || document.pictureInPictureEnabled) {
|
|
||||||
const button = this.createButton(ref, `introskipper-${option.toLowerCase().replace(' ', '-')}`,
|
|
||||||
`<span class="actionsheetMenuItemIcon listItemIcon listItemIcon-transparent material-icons check" aria-hidden="true" style="visibility:${option === this.currentOption ? 'visible' : 'hidden'};"></span><div class="listItemBody actionsheetListItemBody"><div class="listItemBodyText actionSheetItemText">${option}</div></div>`,
|
|
||||||
() => this.selectOption(option));
|
|
||||||
scroller.appendChild(button);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
const backdrop = document.createElement('div');
|
|
||||||
backdrop.className = 'dialogBackdrop dialogBackdropOpened';
|
|
||||||
document.body.append(backdrop, submenu);
|
|
||||||
const actionSheet = submenu.querySelector('.actionSheet');
|
|
||||||
if (actionSheet.classList.contains('actionsheet-not-fullscreen')) {
|
|
||||||
this.adjustPosition(actionSheet, document.querySelector('.btnVideoOsdSettings'));
|
|
||||||
submenu.addEventListener('click', () => this.closeSubmenu(false));
|
|
||||||
} else {
|
|
||||||
submenu.querySelector('.btnCloseActionSheet').addEventListener('click', () => this.closeSubmenu(true))
|
|
||||||
scroller.addEventListener('click', () => this.closeSubmenu(true))
|
|
||||||
document.addEventListener('keydown', this.handleEscapeKey);
|
|
||||||
setTimeout(() => scroller.firstElementChild.focus(), 240);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
selectOption(option) {
|
|
||||||
this.currentOption = option;
|
|
||||||
localStorage.setItem('introskipperOption', option);
|
|
||||||
this.d(`Introskipper option selected and saved: ${option}`);
|
|
||||||
},
|
|
||||||
isAutoSkipLocked(config) {
|
|
||||||
const isAutoSkip = config.AutoSkip && config.AutoSkipCredits;
|
|
||||||
const isAutoSkipClient = new Set(config.ClientList.split(',')).has(ApiClient.appName());
|
|
||||||
return isAutoSkip || (config.SkipButtonVisible && isAutoSkipClient);
|
|
||||||
},
|
|
||||||
async injectIntroSkipperOptions(actionSheet) {
|
|
||||||
if (!this.skipButton) return;
|
|
||||||
const config = await this.secureFetch("Intros/UserInterfaceConfiguration");
|
|
||||||
if (this.isAutoSkipLocked(config)) {
|
|
||||||
this.d("Auto skip enforced by server");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const statsButton = actionSheet.querySelector('[data-id="stats"]');
|
|
||||||
if (!statsButton) return;
|
|
||||||
const menuItem = this.createButton(statsButton, 'introskipperMenu',
|
|
||||||
`<div class="listItemBody actionsheetListItemBody"><div class="listItemBodyText actionSheetItemText">Intro Skipper</div></div><div class="listItemAside actionSheetItemAsideText">${this.currentOption}</div>`,
|
|
||||||
() => this.openSubmenu(statsButton, actionSheet.closest('.dialogContainer')));
|
|
||||||
const originalWidth = actionSheet.offsetWidth;
|
|
||||||
statsButton.before(menuItem);
|
|
||||||
if (actionSheet.classList.contains('actionsheet-not-fullscreen')) this.adjustPosition(actionSheet, menuItem, originalWidth);
|
|
||||||
},
|
|
||||||
adjustPosition(element, reference, originalWidth) {
|
|
||||||
if (originalWidth) {
|
|
||||||
const currentTop = parseInt(element.style.top, 10) || 0;
|
|
||||||
element.style.top = `${currentTop - reference.offsetHeight}px`;
|
|
||||||
const newWidth = Math.max(reference.offsetWidth - originalWidth, 0);
|
|
||||||
const originalLeft = parseInt(element.style.left, 10) || 0;
|
|
||||||
element.style.left = `${originalLeft - newWidth / 2}px`;
|
|
||||||
} else {
|
|
||||||
const rect = reference.getBoundingClientRect();
|
|
||||||
element.style.left = `${Math.min(rect.left - (element.offsetWidth - rect.width) / 2, window.innerWidth - element.offsetWidth - 10)}px`;
|
|
||||||
element.style.top = `${rect.top - element.offsetHeight + rect.height}px`;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
injectSkipperFields(metadataFormFields) {
|
|
||||||
const skipperFields = document.createElement('div');
|
|
||||||
skipperFields.className = 'detailSection introskipperSection';
|
|
||||||
skipperFields.innerHTML = `
|
|
||||||
<h2>Intro Skipper</h2>
|
|
||||||
<div class="inlineForm">
|
|
||||||
<div class="inputContainer">
|
|
||||||
<label class="inputLabel inputLabelUnfocused" for="introStart">Intro Start</label>
|
|
||||||
<input type="text" id="introStartDisplay" class="emby-input custom-time-input" readonly>
|
|
||||||
<input type="number" id="introStartEdit" class="emby-input custom-time-input" style="display: none;" step="any" min="0">
|
|
||||||
</div>
|
|
||||||
<div class="inputContainer">
|
|
||||||
<label class="inputLabel inputLabelUnfocused" for="introEnd">Intro End</label>
|
|
||||||
<input type="text" id="introEndDisplay" class="emby-input custom-time-input" readonly>
|
|
||||||
<input type="number" id="introEndEdit" class="emby-input custom-time-input" style="display: none;" step="any" min="0">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="inlineForm">
|
|
||||||
<div class="inputContainer">
|
|
||||||
<label class="inputLabel inputLabelUnfocused" for="creditsStart">Credits Start</label>
|
|
||||||
<input type="text" id="creditsStartDisplay" class="emby-input custom-time-input" readonly>
|
|
||||||
<input type="number" id="creditsStartEdit" class="emby-input custom-time-input" style="display: none;" step="any" min="0">
|
|
||||||
</div>
|
|
||||||
<div class="inputContainer">
|
|
||||||
<label class="inputLabel inputLabelUnfocused" for="creditsEnd">Credits End</label>
|
|
||||||
<input type="text" id="creditsEndDisplay" class="emby-input custom-time-input" readonly>
|
|
||||||
<input type="number" id="creditsEndEdit" class="emby-input custom-time-input" style="display: none;" step="any" min="0">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
metadataFormFields.querySelector('#metadataSettingsCollapsible').insertAdjacentElement('afterend', skipperFields);
|
|
||||||
this.attachSaveListener(metadataFormFields);
|
|
||||||
this.updateSkipperFields(skipperFields);
|
|
||||||
this.setTimeInputs(skipperFields);
|
|
||||||
},
|
|
||||||
updateSkipperFields(skipperFields) {
|
|
||||||
const { Introduction = {}, Credits = {} } = this.skipperData;
|
|
||||||
skipperFields.querySelector('#introStartEdit').value = Introduction.Start || 0;
|
|
||||||
skipperFields.querySelector('#introEndEdit').value = Introduction.End || 0;
|
|
||||||
skipperFields.querySelector('#creditsStartEdit').value = Credits.Start || 0;
|
|
||||||
skipperFields.querySelector('#creditsEndEdit').value = Credits.End || 0;
|
|
||||||
},
|
|
||||||
attachSaveListener(metadataFormFields) {
|
|
||||||
const saveButton = metadataFormFields.querySelector('.formDialogFooter .btnSave');
|
|
||||||
if (saveButton) {
|
|
||||||
saveButton.addEventListener('click', this.saveSkipperData.bind(this));
|
|
||||||
} else {
|
|
||||||
console.error('Save button not found');
|
|
||||||
}
|
|
||||||
},
|
|
||||||
setTimeInputs(skipperFields) {
|
|
||||||
const inputContainers = skipperFields.querySelectorAll('.inputContainer');
|
|
||||||
inputContainers.forEach(container => {
|
|
||||||
const displayInput = container.querySelector('[id$="Display"]');
|
|
||||||
const editInput = container.querySelector('[id$="Edit"]');
|
|
||||||
displayInput.addEventListener('pointerdown', (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
this.switchToEdit(displayInput, editInput);
|
|
||||||
});
|
|
||||||
editInput.addEventListener('blur', () => this.switchToDisplay(displayInput, editInput));
|
|
||||||
displayInput.value = this.formatTime(parseFloat(editInput.value) || 0);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
formatTime(totalSeconds) {
|
|
||||||
const totalRoundedSeconds = Math.round(totalSeconds);
|
|
||||||
const hours = Math.floor(totalRoundedSeconds / 3600);
|
|
||||||
const minutes = Math.floor((totalRoundedSeconds % 3600) / 60);
|
|
||||||
const seconds = totalRoundedSeconds % 60;
|
|
||||||
let result = [];
|
|
||||||
if (hours > 0) result.push(`${hours} hour${hours !== 1 ? 's' : ''}`);
|
|
||||||
if (minutes > 0) result.push(`${minutes} minute${minutes !== 1 ? 's' : ''}`);
|
|
||||||
if (seconds > 0 || result.length === 0) result.push(`${seconds} second${seconds !== 1 ? 's' : ''}`);
|
|
||||||
return result.join(' ');
|
|
||||||
},
|
|
||||||
switchToEdit(displayInput, editInput) {
|
|
||||||
displayInput.style.display = 'none';
|
|
||||||
editInput.style.display = '';
|
|
||||||
editInput.focus();
|
|
||||||
},
|
|
||||||
switchToDisplay(displayInput, editInput) {
|
|
||||||
editInput.style.display = 'none';
|
|
||||||
displayInput.style.display = '';
|
|
||||||
displayInput.value = this.formatTime(parseFloat(editInput.value) || 0);
|
|
||||||
},
|
|
||||||
async saveSkipperData() {
|
|
||||||
const newTimestamps = {
|
|
||||||
Introduction: {
|
|
||||||
Start: parseFloat(document.getElementById('introStartEdit').value || 0),
|
|
||||||
End: parseFloat(document.getElementById('introEndEdit').value || 0)
|
|
||||||
},
|
|
||||||
Credits: {
|
|
||||||
Start: parseFloat(document.getElementById('creditsStartEdit').value || 0),
|
|
||||||
End: parseFloat(document.getElementById('creditsEndEdit').value || 0)
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const { Introduction = {}, Credits = {} } = this.skipperData;
|
|
||||||
if (newTimestamps.Introduction.Start !== (Introduction.Start || 0) ||
|
|
||||||
newTimestamps.Introduction.End !== (Introduction.End || 0) ||
|
|
||||||
newTimestamps.Credits.Start !== (Credits.Start || 0) ||
|
|
||||||
newTimestamps.Credits.End !== (Credits.End || 0)) {
|
|
||||||
const response = await this.secureFetch(`Episode/${this.currentEpisodeId}/Timestamps`, "POST", JSON.stringify(newTimestamps));
|
|
||||||
this.d(response.ok ? 'Timestamps updated successfully' : 'Failed to update timestamps:', response.status);
|
|
||||||
} else {
|
|
||||||
this.d('Timestamps have not changed, skipping update');
|
|
||||||
}
|
|
||||||
},
|
|
||||||
/** Make an authenticated fetch to the Jellyfin server and parse the response body as JSON. */
|
|
||||||
async secureFetch(url, method = "GET", body = null) {
|
|
||||||
const response = await fetch(`${ApiClient.serverAddress()}/${url}`, {
|
|
||||||
method,
|
|
||||||
headers: Object.assign({ "Authorization": `MediaBrowser Token=${ApiClient.accessToken()}` },
|
|
||||||
method === "POST" ? {"Content-Type": "application/json"} : {}),
|
|
||||||
body });
|
|
||||||
return response.ok ? (method === "POST" ? response : response.json()) :
|
|
||||||
response.status === 404 ? null :
|
|
||||||
console.error(`Error ${response.status} from ${url}`) || null;
|
|
||||||
},
|
|
||||||
/** Handle keydown events. */
|
|
||||||
eventHandler(e) {
|
|
||||||
if (e.key !== "Enter") return;
|
|
||||||
e.stopPropagation();
|
|
||||||
e.preventDefault();
|
|
||||||
this.doSkip();
|
|
||||||
},
|
|
||||||
handleEscapeKey(e) {
|
|
||||||
if (e.key === 'Escape' || e.keyCode === 461 || e.keyCode === 10009) {
|
|
||||||
e.stopPropagation();
|
|
||||||
this.closeSubmenu(true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
introSkipper.setup();
|
|
@ -21,7 +21,7 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\ConfusedPolarBear.Plugin.IntroSkipper\ConfusedPolarBear.Plugin.IntroSkipper.csproj" />
|
<ProjectReference Include="..\IntroSkipper\IntroSkipper.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
@ -1,15 +1,18 @@
|
|||||||
|
// Copyright (C) 2024 Intro-Skipper contributors <intro-skipper.org>
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-only.
|
||||||
|
|
||||||
/* These tests require that the host system has a version of FFmpeg installed
|
/* These tests require that the host system has a version of FFmpeg installed
|
||||||
* which supports both chromaprint and the "-fp_format raw" flag.
|
* which supports both chromaprint and the "-fp_format raw" flag.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using ConfusedPolarBear.Plugin.IntroSkipper.Analyzers;
|
using IntroSkipper.Analyzers;
|
||||||
using ConfusedPolarBear.Plugin.IntroSkipper.Data;
|
using IntroSkipper.Data;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
namespace ConfusedPolarBear.Plugin.IntroSkipper.Tests;
|
namespace IntroSkipper.Tests;
|
||||||
|
|
||||||
public class TestAudioFingerprinting
|
public class TestAudioFingerprinting
|
||||||
{
|
{
|
@ -1,9 +1,12 @@
|
|||||||
namespace ConfusedPolarBear.Plugin.IntroSkipper.Tests;
|
// Copyright (C) 2024 Intro-Skipper contributors <intro-skipper.org>
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-only.
|
||||||
|
|
||||||
|
namespace IntroSkipper.Tests;
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using ConfusedPolarBear.Plugin.IntroSkipper.Analyzers;
|
using IntroSkipper.Analyzers;
|
||||||
using ConfusedPolarBear.Plugin.IntroSkipper.Data;
|
using IntroSkipper.Data;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
@ -1,10 +1,13 @@
|
|||||||
namespace ConfusedPolarBear.Plugin.IntroSkipper.Tests;
|
// Copyright (C) 2024 Intro-Skipper contributors <intro-skipper.org>
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-only.
|
||||||
|
|
||||||
|
namespace IntroSkipper.Tests;
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Collections.ObjectModel;
|
using System.Collections.ObjectModel;
|
||||||
using ConfusedPolarBear.Plugin.IntroSkipper.Analyzers;
|
using IntroSkipper.Analyzers;
|
||||||
using ConfusedPolarBear.Plugin.IntroSkipper.Data;
|
using IntroSkipper.Data;
|
||||||
using MediaBrowser.Model.Entities;
|
using MediaBrowser.Model.Entities;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Xunit;
|
using Xunit;
|
@ -1,7 +1,10 @@
|
|||||||
using ConfusedPolarBear.Plugin.IntroSkipper.Data;
|
// Copyright (C) 2024 Intro-Skipper contributors <intro-skipper.org>
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-only.
|
||||||
|
|
||||||
|
using IntroSkipper.Data;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
namespace ConfusedPolarBear.Plugin.IntroSkipper.Tests;
|
namespace IntroSkipper.Tests;
|
||||||
|
|
||||||
public class TestTimeRanges
|
public class TestTimeRanges
|
||||||
{
|
{
|
@ -1,8 +1,11 @@
|
|||||||
|
// Copyright (C) 2024 Intro-Skipper contributors <intro-skipper.org>
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-only.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using ConfusedPolarBear.Plugin.IntroSkipper.Data;
|
using IntroSkipper.Data;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
namespace ConfusedPolarBear.Plugin.IntroSkipper.Tests;
|
namespace IntroSkipper.Tests;
|
||||||
|
|
||||||
public class TestEdl
|
public class TestEdl
|
||||||
{
|
{
|
@ -1,6 +1,9 @@
|
|||||||
namespace ConfusedPolarBear.Plugin.IntroSkipper.Tests;
|
// Copyright (C) 2024 Intro-Skipper contributors <intro-skipper.org>
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-only.
|
||||||
|
|
||||||
using ConfusedPolarBear.Plugin.IntroSkipper.Data;
|
namespace IntroSkipper.Tests;
|
||||||
|
|
||||||
|
using IntroSkipper.Data;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
public class TestFlags
|
public class TestFlags
|
0
ConfusedPolarBear.Plugin.IntroSkipper.Tests/e2e_tests/build.sh → IntroSkipper.Tests/e2e_tests/build.sh
Executable file → Normal file
0
ConfusedPolarBear.Plugin.IntroSkipper.Tests/e2e_tests/build.sh → IntroSkipper.Tests/e2e_tests/build.sh
Executable file → Normal file
@ -1,8 +1,8 @@
|
|||||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||||
#
|
#
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ConfusedPolarBear.Plugin.IntroSkipper", "ConfusedPolarBear.Plugin.IntroSkipper\ConfusedPolarBear.Plugin.IntroSkipper.csproj", "{D921B930-CF91-406F-ACBC-08914DCD0D34}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "IntroSkipper", "IntroSkipper\IntroSkipper.csproj", "{D921B930-CF91-406F-ACBC-08914DCD0D34}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ConfusedPolarBear.Plugin.IntroSkipper.Tests", "ConfusedPolarBear.Plugin.IntroSkipper.Tests\ConfusedPolarBear.Plugin.IntroSkipper.Tests.csproj", "{9E30DA42-983E-46E0-A3BF-A2BA56FE9718}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "IntroSkipper.Tests", "IntroSkipper.Tests\IntroSkipper.Tests.csproj", "{9E30DA42-983E-46E0-A3BF-A2BA56FE9718}"
|
||||||
EndProject
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
@ -1,11 +1,14 @@
|
|||||||
|
// Copyright (C) 2024 Intro-Skipper contributors <intro-skipper.org>
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-only.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using ConfusedPolarBear.Plugin.IntroSkipper.Configuration;
|
using IntroSkipper.Configuration;
|
||||||
using ConfusedPolarBear.Plugin.IntroSkipper.Data;
|
using IntroSkipper.Data;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace ConfusedPolarBear.Plugin.IntroSkipper;
|
namespace IntroSkipper;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Analyzer Helper.
|
/// Analyzer Helper.
|
@ -1,12 +1,15 @@
|
|||||||
|
// Copyright (C) 2024 Intro-Skipper contributors <intro-skipper.org>
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-only.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using ConfusedPolarBear.Plugin.IntroSkipper.Configuration;
|
using IntroSkipper.Configuration;
|
||||||
using ConfusedPolarBear.Plugin.IntroSkipper.Data;
|
using IntroSkipper.Data;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace ConfusedPolarBear.Plugin.IntroSkipper.Analyzers;
|
namespace IntroSkipper.Analyzers;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Media file analyzer used to detect end credits that consist of text overlaid on a black background.
|
/// Media file analyzer used to detect end credits that consist of text overlaid on a black background.
|
||||||
@ -22,6 +25,8 @@ public class BlackFrameAnalyzer : IMediaFileAnalyzer
|
|||||||
|
|
||||||
private readonly int _maximumCreditsDuration;
|
private readonly int _maximumCreditsDuration;
|
||||||
|
|
||||||
|
private readonly int _maximumMovieCreditsDuration;
|
||||||
|
|
||||||
private readonly int _blackFrameMinimumPercentage;
|
private readonly int _blackFrameMinimumPercentage;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -32,7 +37,8 @@ public class BlackFrameAnalyzer : IMediaFileAnalyzer
|
|||||||
{
|
{
|
||||||
var config = Plugin.Instance?.Configuration ?? new PluginConfiguration();
|
var config = Plugin.Instance?.Configuration ?? new PluginConfiguration();
|
||||||
_minimumCreditsDuration = config.MinimumCreditsDuration;
|
_minimumCreditsDuration = config.MinimumCreditsDuration;
|
||||||
_maximumCreditsDuration = 2 * config.MaximumCreditsDuration;
|
_maximumCreditsDuration = config.MaximumCreditsDuration;
|
||||||
|
_maximumMovieCreditsDuration = config.MaximumMovieCreditsDuration;
|
||||||
_blackFrameMinimumPercentage = config.BlackFrameMinimumPercentage;
|
_blackFrameMinimumPercentage = config.BlackFrameMinimumPercentage;
|
||||||
|
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
@ -66,7 +72,21 @@ public class BlackFrameAnalyzer : IMediaFileAnalyzer
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pre-check to find reasonable starting point.
|
var creditDuration = episode.IsMovie ? _maximumMovieCreditsDuration : _maximumCreditsDuration;
|
||||||
|
|
||||||
|
var chapters = Plugin.Instance!.GetChapters(episode.EpisodeId);
|
||||||
|
var lastSuitableChapter = chapters.LastOrDefault(c =>
|
||||||
|
{
|
||||||
|
var start = TimeSpan.FromTicks(c.StartPositionTicks).TotalSeconds;
|
||||||
|
return start >= _minimumCreditsDuration && start <= creditDuration;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (lastSuitableChapter is not null)
|
||||||
|
{
|
||||||
|
searchStart = TimeSpan.FromTicks(lastSuitableChapter.StartPositionTicks).TotalSeconds;
|
||||||
|
isFirstEpisode = false;
|
||||||
|
}
|
||||||
|
|
||||||
if (isFirstEpisode)
|
if (isFirstEpisode)
|
||||||
{
|
{
|
||||||
var scanTime = episode.Duration - searchStart;
|
var scanTime = episode.Duration - searchStart;
|
||||||
@ -83,9 +103,9 @@ public class BlackFrameAnalyzer : IMediaFileAnalyzer
|
|||||||
|
|
||||||
frames = FFmpegWrapper.DetectBlackFrames(episode, tr, _blackFrameMinimumPercentage);
|
frames = FFmpegWrapper.DetectBlackFrames(episode, tr, _blackFrameMinimumPercentage);
|
||||||
|
|
||||||
if (searchStart > _maximumCreditsDuration)
|
if (searchStart > creditDuration)
|
||||||
{
|
{
|
||||||
searchStart = _maximumCreditsDuration;
|
searchStart = creditDuration;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -104,7 +124,7 @@ public class BlackFrameAnalyzer : IMediaFileAnalyzer
|
|||||||
searchDistance,
|
searchDistance,
|
||||||
_blackFrameMinimumPercentage);
|
_blackFrameMinimumPercentage);
|
||||||
|
|
||||||
if (credit is null)
|
if (credit is null || !credit.Valid)
|
||||||
{
|
{
|
||||||
// If no credits were found, reset the first-episode search logic for the next episode in the sequence.
|
// If no credits were found, reset the first-episode search logic for the next episode in the sequence.
|
||||||
searchStart = _minimumCreditsDuration;
|
searchStart = _minimumCreditsDuration;
|
||||||
@ -143,6 +163,8 @@ public class BlackFrameAnalyzer : IMediaFileAnalyzer
|
|||||||
var end = TimeSpan.FromSeconds(lowerLimit);
|
var end = TimeSpan.FromSeconds(lowerLimit);
|
||||||
var firstFrameTime = 0.0;
|
var firstFrameTime = 0.0;
|
||||||
|
|
||||||
|
var creditDuration = episode.IsMovie ? _maximumMovieCreditsDuration : _maximumCreditsDuration;
|
||||||
|
|
||||||
// Continue bisecting the end of the file until the range that contains the first black
|
// Continue bisecting the end of the file until the range that contains the first black
|
||||||
// frame is smaller than the maximum permitted error.
|
// frame is smaller than the maximum permitted error.
|
||||||
while (start - end > _maximumError)
|
while (start - end > _maximumError)
|
||||||
@ -189,7 +211,7 @@ public class BlackFrameAnalyzer : IMediaFileAnalyzer
|
|||||||
|
|
||||||
if (TimeSpan.FromSeconds(upperLimit) - midpoint < _maximumError)
|
if (TimeSpan.FromSeconds(upperLimit) - midpoint < _maximumError)
|
||||||
{
|
{
|
||||||
upperLimit = Math.Min(upperLimit + (0.5 * searchDistance), _maximumCreditsDuration);
|
upperLimit = Math.Min(upperLimit + (0.5 * searchDistance), creditDuration);
|
||||||
|
|
||||||
// Reset start for a new search with the increased duration
|
// Reset start for a new search with the increased duration
|
||||||
start = TimeSpan.FromSeconds(upperLimit);
|
start = TimeSpan.FromSeconds(upperLimit);
|
@ -1,16 +1,18 @@
|
|||||||
|
// Copyright (C) 2024 Intro-Skipper contributors <intro-skipper.org>
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-only.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Collections.ObjectModel;
|
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using ConfusedPolarBear.Plugin.IntroSkipper.Configuration;
|
using IntroSkipper.Configuration;
|
||||||
using ConfusedPolarBear.Plugin.IntroSkipper.Data;
|
using IntroSkipper.Data;
|
||||||
using MediaBrowser.Model.Entities;
|
using MediaBrowser.Model.Entities;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace ConfusedPolarBear.Plugin.IntroSkipper.Analyzers;
|
namespace IntroSkipper.Analyzers;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Chapter name analyzer.
|
/// Chapter name analyzer.
|
||||||
@ -56,7 +58,7 @@ public class ChapterAnalyzer(ILogger<ChapterAnalyzer> logger) : IMediaFileAnalyz
|
|||||||
expression,
|
expression,
|
||||||
mode);
|
mode);
|
||||||
|
|
||||||
if (skipRange is null)
|
if (skipRange is null || !skipRange.Valid)
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@ -92,9 +94,10 @@ public class ChapterAnalyzer(ILogger<ChapterAnalyzer> logger) : IMediaFileAnalyz
|
|||||||
}
|
}
|
||||||
|
|
||||||
var config = Plugin.Instance?.Configuration ?? new PluginConfiguration();
|
var config = Plugin.Instance?.Configuration ?? new PluginConfiguration();
|
||||||
|
var creditDuration = episode.IsMovie ? config.MaximumMovieCreditsDuration : config.MaximumCreditsDuration;
|
||||||
var reversed = mode != AnalysisMode.Introduction;
|
var reversed = mode != AnalysisMode.Introduction;
|
||||||
var (minDuration, maxDuration) = reversed
|
var (minDuration, maxDuration) = reversed
|
||||||
? (config.MinimumCreditsDuration, config.MaximumCreditsDuration)
|
? (config.MinimumCreditsDuration, creditDuration)
|
||||||
: (config.MinimumIntroDuration, config.MaximumIntroDuration);
|
: (config.MinimumIntroDuration, config.MaximumIntroDuration);
|
||||||
|
|
||||||
// Check all chapters
|
// Check all chapters
|
@ -1,14 +1,17 @@
|
|||||||
|
// Copyright (C) 2024 Intro-Skipper contributors <intro-skipper.org>
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-only.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using ConfusedPolarBear.Plugin.IntroSkipper.Configuration;
|
using IntroSkipper.Configuration;
|
||||||
using ConfusedPolarBear.Plugin.IntroSkipper.Data;
|
using IntroSkipper.Data;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace ConfusedPolarBear.Plugin.IntroSkipper.Analyzers;
|
namespace IntroSkipper.Analyzers;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Chromaprint audio analyzer.
|
/// Chromaprint audio analyzer.
|
@ -1,8 +1,11 @@
|
|||||||
|
// Copyright (C) 2024 Intro-Skipper contributors <intro-skipper.org>
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-only.
|
||||||
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using ConfusedPolarBear.Plugin.IntroSkipper.Data;
|
using IntroSkipper.Data;
|
||||||
|
|
||||||
namespace ConfusedPolarBear.Plugin.IntroSkipper.Analyzers;
|
namespace IntroSkipper.Analyzers;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Media file analyzer interface.
|
/// Media file analyzer interface.
|
@ -1,9 +1,12 @@
|
|||||||
|
// Copyright (C) 2024 Intro-Skipper contributors <intro-skipper.org>
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-only.
|
||||||
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using ConfusedPolarBear.Plugin.IntroSkipper.Data;
|
using IntroSkipper.Data;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace ConfusedPolarBear.Plugin.IntroSkipper.Analyzers;
|
namespace IntroSkipper.Analyzers;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Chapter name analyzer.
|
/// Chapter name analyzer.
|
@ -1,8 +1,11 @@
|
|||||||
|
// Copyright (C) 2024 Intro-Skipper contributors <intro-skipper.org>
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-only.
|
||||||
|
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using ConfusedPolarBear.Plugin.IntroSkipper.Data;
|
using IntroSkipper.Data;
|
||||||
using MediaBrowser.Model.Plugins;
|
using MediaBrowser.Model.Plugins;
|
||||||
|
|
||||||
namespace ConfusedPolarBear.Plugin.IntroSkipper.Configuration;
|
namespace IntroSkipper.Configuration;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Plugin configuration.
|
/// Plugin configuration.
|
||||||
@ -18,11 +21,6 @@ public class PluginConfiguration : BasePluginConfiguration
|
|||||||
|
|
||||||
// ===== Analysis settings =====
|
// ===== Analysis settings =====
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the max degree of parallelism used when analyzing episodes.
|
|
||||||
/// </summary>
|
|
||||||
public int MaxParallelism { get; set; } = 2;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the comma separated list of library names to analyze.
|
/// Gets or sets the comma separated list of library names to analyze.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -33,6 +31,11 @@ public class PluginConfiguration : BasePluginConfiguration
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public bool SelectAllLibraries { get; set; } = true;
|
public bool SelectAllLibraries { get; set; } = true;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets a value indicating whether movies should be analyzed.
|
||||||
|
/// </summary>
|
||||||
|
public bool AnalyzeMovies { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the list of client to auto skip for.
|
/// Gets or sets the list of client to auto skip for.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -107,7 +110,12 @@ public class PluginConfiguration : BasePluginConfiguration
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the upper limit (in seconds) on the length of each episode's audio track that will be analyzed when searching for ending credits.
|
/// Gets or sets the upper limit (in seconds) on the length of each episode's audio track that will be analyzed when searching for ending credits.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int MaximumCreditsDuration { get; set; } = 300;
|
public int MaximumCreditsDuration { get; set; } = 450;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the upper limit (in seconds) on the length of a movie segment that will be analyzed when searching for ending credits.
|
||||||
|
/// </summary>
|
||||||
|
public int MaximumMovieCreditsDuration { get; set; } = 900;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the minimum percentage of a frame that must consist of black pixels before it is considered a black frame.
|
/// Gets or sets the minimum percentage of a frame that must consist of black pixels before it is considered a black frame.
|
||||||
@ -235,12 +243,22 @@ public class PluginConfiguration : BasePluginConfiguration
|
|||||||
public string AutoSkipCreditsNotificationText { get; set; } = "Credits skipped";
|
public string AutoSkipCreditsNotificationText { get; set; } = "Credits skipped";
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the number of threads for an ffmpeg process.
|
/// Gets or sets the max degree of parallelism used when analyzing episodes.
|
||||||
|
/// </summary>
|
||||||
|
public int MaxParallelism { get; set; } = 2;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the number of threads for a ffmpeg process.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int ProcessThreads { get; set; }
|
public int ProcessThreads { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the relative priority for an ffmpeg process.
|
/// Gets or sets the relative priority for a ffmpeg process.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public ProcessPriorityClass ProcessPriority { get; set; } = ProcessPriorityClass.BelowNormal;
|
public ProcessPriorityClass ProcessPriority { get; set; } = ProcessPriorityClass.BelowNormal;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets a value indicating whether the ManifestUrl is self-managed, e.g. for mainland China.
|
||||||
|
/// </summary>
|
||||||
|
public bool OverrideManifestUrl { get; set; }
|
||||||
}
|
}
|
@ -1,4 +1,7 @@
|
|||||||
namespace ConfusedPolarBear.Plugin.IntroSkipper.Configuration;
|
// Copyright (C) 2024 Intro-Skipper contributors <intro-skipper.org>
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-only.
|
||||||
|
|
||||||
|
namespace IntroSkipper.Configuration;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// User interface configuration.
|
/// User interface configuration.
|
@ -32,46 +32,43 @@
|
|||||||
<div class="checkboxContainer checkboxContainer-withDescription">
|
<div class="checkboxContainer checkboxContainer-withDescription">
|
||||||
<label class="emby-checkbox-label">
|
<label class="emby-checkbox-label">
|
||||||
<input id="AutoDetectIntros" type="checkbox" is="emby-checkbox" />
|
<input id="AutoDetectIntros" type="checkbox" is="emby-checkbox" />
|
||||||
<span>Automatically Scan Intros</span>
|
<span>Automatically Analyze Intros</span>
|
||||||
</label>
|
</label>
|
||||||
|
<div class="fieldDescription">If enabled, new media will be automatically analyzed for introductions</div>
|
||||||
<div class="fieldDescription">If enabled, introductions will be automatically analyzed for new media</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="checkboxContainer checkboxContainer-withDescription">
|
<div class="checkboxContainer checkboxContainer-withDescription">
|
||||||
<label class="emby-checkbox-label">
|
<label class="emby-checkbox-label">
|
||||||
<input id="AutoDetectCredits" type="checkbox" is="emby-checkbox" />
|
<input id="AutoDetectCredits" type="checkbox" is="emby-checkbox" />
|
||||||
<span>Automatically Scan Credits</span>
|
<span>Automatically Analyze Credits</span>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<div class="fieldDescription">
|
<div class="fieldDescription">
|
||||||
If enabled, credits will be automatically analyzed for new media
|
If enabled, new media will be automatically analyzed for credits
|
||||||
<br />
|
<br />
|
||||||
<br />
|
<br />
|
||||||
Note: Not selecting at least one automatic detection type will disable automatic scans. To configure the scheduled task, see <a is="emby-linkbutton" class="button-link" href="#/dashboard/tasks">scheduled tasks</a>.
|
Note: Not selecting at least one automatic detection type will disable automatic scans. To configure the scheduled task, see <a is="emby-linkbutton" class="button-link" href="#/dashboard/tasks">scheduled tasks</a>.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="checkboxContainer">
|
||||||
|
<label class="emby-checkbox-label">
|
||||||
|
<input id="AnalyzeMovies" type="checkbox" is="emby-checkbox" />
|
||||||
|
<span>Analyze Movies</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="checkboxContainer checkboxContainer-withDescription">
|
<div class="checkboxContainer checkboxContainer-withDescription">
|
||||||
<label class="emby-checkbox-label">
|
<label class="emby-checkbox-label">
|
||||||
<input id="AnalyzeSeasonZero" type="checkbox" is="emby-checkbox" />
|
<input id="AnalyzeSeasonZero" type="checkbox" is="emby-checkbox" />
|
||||||
<span>Analyze season 0</span>
|
<span>Analyze Season 0 (Specials / Extras)</span>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<div class="fieldDescription">
|
<div class="fieldDescription">
|
||||||
If checked, season 0 (specials / extras) will be included in analysis.
|
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
Note: Shows containing both a specials and extra folder will identify extras as season 0 and ignore specials, regardless of this setting.
|
Note: Shows containing both a specials and extra folder will identify extras as season 0 and ignore specials, regardless of this setting.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="inputContainer">
|
|
||||||
<label class="inputLabel inputLabelUnfocused" for="MaxParallelism"> Maximum degree of parallelism </label>
|
|
||||||
<input id="MaxParallelism" type="number" is="emby-input" min="1" />
|
|
||||||
<div class="fieldDescription">Maximum number of simultaneous async episode analysis operations.</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="checkboxContainer checkboxContainer-withDescription">
|
<div class="checkboxContainer checkboxContainer-withDescription">
|
||||||
<label class="emby-checkbox-label">
|
<label class="emby-checkbox-label">
|
||||||
<input id="SelectAllLibraries" type="checkbox" is="emby-checkbox" />
|
<input id="SelectAllLibraries" type="checkbox" is="emby-checkbox" />
|
||||||
@ -128,13 +125,11 @@
|
|||||||
<div class="fieldDescription">Similar sounding audio which is longer than this duration will not be considered credits.</div>
|
<div class="fieldDescription">Similar sounding audio which is longer than this duration will not be considered credits.</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p>The amount of each episode's audio that will be analyzed is determined using both the percentage of audio and maximum runtime of audio to analyze. The minimum of (episode duration * percent, maximum runtime) is the amount of audio that will be analyzed.</p>
|
<div class="inputContainer" id="movieCreditsDuration">
|
||||||
|
<label class="inputLabel inputLabelUnfocused" for="MaximumMovieCreditsDuration"> Maximum movie credits duration (in seconds) </label>
|
||||||
<p>
|
<input id="MaximumMovieCreditsDuration" type="number" is="emby-input" min="1" />
|
||||||
If the audio percentage or maximum runtime settings are modified, the cached fingerprints and introduction timestamps for each season you want to analyze with the modified settings <strong>will have to be deleted.</strong>
|
<div class="fieldDescription">Segments longer than this duration will not be considered movie credits.</div>
|
||||||
|
</div>
|
||||||
Increasing either of these settings will cause episode analysis to take much longer.
|
|
||||||
</p>
|
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
<details id="edl">
|
<details id="edl">
|
||||||
@ -150,8 +145,6 @@
|
|||||||
|
|
||||||
<option value="Cut">Cut (player will remove the intro from the video)</option>
|
<option value="Cut">Cut (player will remove the intro from the video)</option>
|
||||||
|
|
||||||
<option value="Intro">Intro/Credit (show a skip button, *experimental*)</option>
|
|
||||||
|
|
||||||
<option value="Mute">Mute (audio will be muted)</option>
|
<option value="Mute">Mute (audio will be muted)</option>
|
||||||
|
|
||||||
<option value="SceneMarker">Scene Marker (create a chapter marker)</option>
|
<option value="SceneMarker">Scene Marker (create a chapter marker)</option>
|
||||||
@ -166,13 +159,23 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
The amount of each item's audio or content that will be analyzed is determined using both the percentage of audio and maximum runtime to analyze. The minimum of (duration * percent, maximum runtime) is the amount that will be analyzed.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
If the audio percentage or maximum runtime settings are modified, the cached fingerprints and timestamps for each series, season, or movie you want to analyze with the modified settings <b>will have to be deleted</b>.
|
||||||
|
<br />
|
||||||
|
Increasing either of these settings will cause episode analysis to take much longer.
|
||||||
|
</p>
|
||||||
|
|
||||||
<div class="checkboxContainer checkboxContainer-withDescription">
|
<div class="checkboxContainer checkboxContainer-withDescription">
|
||||||
<label class="emby-checkbox-label">
|
<label class="emby-checkbox-label">
|
||||||
<input id="RegenerateEdlFiles" type="checkbox" is="emby-checkbox" />
|
<input id="RegenerateEdlFiles" type="checkbox" is="emby-checkbox" />
|
||||||
<span>Regenerate EDL files during next scan</span>
|
<span>Regenerate EDL files during next scan</span>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<div class="fieldDescription">If checked, the plugin will <strong>overwrite all EDL files</strong> associated with your episodes with the currently discovered introduction/credit timestamps and EDL action.</div>
|
<div class="fieldDescription">If checked, the plugin will <b>overwrite all EDL files</b> associated with your episodes with the currently discovered introduction/credit timestamps and EDL action.</div>
|
||||||
</div>
|
</div>
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
@ -197,6 +200,12 @@
|
|||||||
<summary>Process Configuration</summary>
|
<summary>Process Configuration</summary>
|
||||||
|
|
||||||
<br />
|
<br />
|
||||||
|
<div class="inputContainer">
|
||||||
|
<label class="inputLabel inputLabelUnfocused" for="MaxParallelism"> Maximum degree of parallelism </label>
|
||||||
|
<input id="MaxParallelism" type="number" is="emby-input" min="1" />
|
||||||
|
<div class="fieldDescription">Maximum number of simultaneous async episode analysis operations.</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="checkboxContainer checkboxContainer-withDescription">
|
<div class="checkboxContainer checkboxContainer-withDescription">
|
||||||
<label class="emby-checkbox-label">
|
<label class="emby-checkbox-label">
|
||||||
<input id="CacheFingerprints" type="checkbox" is="emby-checkbox" />
|
<input id="CacheFingerprints" type="checkbox" is="emby-checkbox" />
|
||||||
@ -206,7 +215,7 @@
|
|||||||
<div class="fieldDescription">
|
<div class="fieldDescription">
|
||||||
If checked, episode fingerprints will be saved on the filesystem to improve analysis speed.
|
If checked, episode fingerprints will be saved on the filesystem to improve analysis speed.
|
||||||
<br />
|
<br />
|
||||||
<strong>WARNING: Disabling the cache will cause all libraries to be re-scanned, which can take a very long time!</strong>
|
<b>WARNING: Disabling the cache will cause all libraries to be re-scanned, which can take a very long time!</b>
|
||||||
<br />
|
<br />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -248,11 +257,11 @@
|
|||||||
<div class="checkboxContainer checkboxContainer-withDescription">
|
<div class="checkboxContainer checkboxContainer-withDescription">
|
||||||
<label class="emby-checkbox-label">
|
<label class="emby-checkbox-label">
|
||||||
<input id="AutoSkip" type="checkbox" is="emby-checkbox" />
|
<input id="AutoSkip" type="checkbox" is="emby-checkbox" />
|
||||||
<span>Automatically skip intros</span>
|
<span>Automatically Skip Intros</span>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<div class="fieldDescription">
|
<div class="fieldDescription">
|
||||||
If checked, intros will be automatically skipped for <strong>all</strong> clients. Note: Clients cannot disable this setting from the player popup (gear icon).<br />
|
If checked, intros will be automatically skipped for <b>all</b> clients. Note: Clients cannot disable this setting from the player popup (gear icon).<br />
|
||||||
If you access Jellyfin through a reverse proxy, it must be configured to proxy websockets.<br />
|
If you access Jellyfin through a reverse proxy, it must be configured to proxy websockets.<br />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -260,7 +269,7 @@
|
|||||||
<div id="divSkipFirstEpisode" class="checkboxContainer checkboxContainer-withDescription">
|
<div id="divSkipFirstEpisode" class="checkboxContainer checkboxContainer-withDescription">
|
||||||
<label class="emby-checkbox-label">
|
<label class="emby-checkbox-label">
|
||||||
<input id="SkipFirstEpisode" type="checkbox" is="emby-checkbox" />
|
<input id="SkipFirstEpisode" type="checkbox" is="emby-checkbox" />
|
||||||
<span>Play intro for first episode of a season</span>
|
<span>Play Intro for First Episode of a Season</span>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<div class="fieldDescription">If checked, auto skip will play the introduction of the first episode in a season.<br /></div>
|
<div class="fieldDescription">If checked, auto skip will play the introduction of the first episode in a season.<br /></div>
|
||||||
@ -274,14 +283,20 @@
|
|||||||
<br />
|
<br />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="inputContainer">
|
||||||
|
<label class="inputLabel inputLabelUnfocused" for="RemainingSecondsOfIntro"> Intro playback duration (in seconds) </label>
|
||||||
|
<input id="RemainingSecondsOfIntro" type="number" is="emby-input" min="0" />
|
||||||
|
<div class="fieldDescription">Seconds of introduction ending that should be played. Defaults to 2.</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="checkboxContainer checkboxContainer-withDescription">
|
<div class="checkboxContainer checkboxContainer-withDescription">
|
||||||
<label class="emby-checkbox-label">
|
<label class="emby-checkbox-label">
|
||||||
<input id="AutoSkipCredits" type="checkbox" is="emby-checkbox" />
|
<input id="AutoSkipCredits" type="checkbox" is="emby-checkbox" />
|
||||||
<span>Automatically skip credits</span>
|
<span>Automatically Skip Credits</span>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<div class="fieldDescription">
|
<div class="fieldDescription">
|
||||||
If checked, credits will be automatically skipped for <strong>all</strong> clients. Note: Clients cannot disable this setting from the player popup (gear icon).<br />
|
If checked, credits will be automatically skipped for <b>all</b> clients. Note: Clients cannot disable this setting from the player popup (gear icon).<br />
|
||||||
If you access Jellyfin through a reverse proxy, it must be configured to proxy websockets.<br />
|
If you access Jellyfin through a reverse proxy, it must be configured to proxy websockets.<br />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -296,16 +311,18 @@
|
|||||||
<div id="SkipButtonContainer" class="checkboxContainer checkboxContainer-withDescription">
|
<div id="SkipButtonContainer" class="checkboxContainer checkboxContainer-withDescription">
|
||||||
<label class="emby-checkbox-label">
|
<label class="emby-checkbox-label">
|
||||||
<input id="SkipButtonVisible" type="checkbox" is="emby-checkbox" />
|
<input id="SkipButtonVisible" type="checkbox" is="emby-checkbox" />
|
||||||
<span id="SkipButtonVisibleLabel">Show skip intro / credit button</span>
|
<span id="SkipButtonVisibleLabel">Show All Skip Buttons</span>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<div class="fieldDescription">
|
<div class="fieldDescription">
|
||||||
If checked, a skip button will be displayed according to the settings below, while clients selected in the Auto Skip Client List will skip <strong>automatically</strong>.
|
If checked, a skip button will be displayed according to the settings below, while clients selected in the Auto Skip Client List will skip <b>automatically</b>.
|
||||||
<br />
|
<br />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="warningMessage" style="color: #721c24; background-color: #f7cf1f; border: 1px solid #f5c6cb; border-radius: 4px; padding: 10px; margin-bottom: 10px">Failed to add skip button to web interface. See <a href="https://github.com/intro-skipper/intro-skipper/wiki/Troubleshooting#skip-button-is-not-visible" target="_blank" rel="noopener noreferrer">troubleshooting guide</a> for the most common issues.</div>
|
<div id="warningMessage" style="color: #721c24; background-color: #f7cf1f; border: 1px solid #f5c6cb; border-radius: 4px; padding: 10px; margin-bottom: 10px">
|
||||||
|
Failed to add skip button to web interface. See <a href="https://github.com/intro-skipper/intro-skipper/wiki/Troubleshooting#skip-button-is-not-visible" target="_blank" rel="noopener noreferrer">troubleshooting guide</a> for the most common issues.
|
||||||
|
</div>
|
||||||
|
|
||||||
<details id="AutoSkipClientList" style="padding-bottom: 1em">
|
<details id="AutoSkipClientList" style="padding-bottom: 1em">
|
||||||
<summary>Auto Skip Client List</summary>
|
<summary>Auto Skip Client List</summary>
|
||||||
@ -315,45 +332,45 @@
|
|||||||
<input id="ClientList" type="hidden" is="emby-input" />
|
<input id="ClientList" type="hidden" is="emby-input" />
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
<div id="SkipButtonSettings">
|
|
||||||
<div id="PersistContainer" class="checkboxContainer checkboxContainer-withDescription">
|
|
||||||
<label class="emby-checkbox-label">
|
|
||||||
<input id="PersistSkipButton" type="checkbox" is="emby-checkbox" />
|
|
||||||
<span>Display button for intro duration</span>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<div class="fieldDescription">
|
|
||||||
If checked, skip button will remain visible throught the intro (offset and timeout are ignored).
|
|
||||||
<br />
|
|
||||||
Note: If unchecked, button will only appear in the player controls after the set timeout.
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="divShowPromptAdjustment" class="inputContainer">
|
|
||||||
<label class="inputLabel inputLabelUnfocused" for="ShowPromptAdjustment"> Skip prompt offset (in seconds) </label>
|
|
||||||
<input id="ShowPromptAdjustment" type="number" is="emby-input" min="0" />
|
|
||||||
<div class="fieldDescription">Seconds to display skip prompt before introduction begins.</div>
|
|
||||||
<br />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="divHidePromptAdjustment" class="inputContainer">
|
|
||||||
<label class="inputLabel inputLabelUnfocused" for="HidePromptAdjustment"> Skip prompt timeout (in seconds) </label>
|
|
||||||
<input id="HidePromptAdjustment" type="number" is="emby-input" min="2" />
|
|
||||||
<div class="fieldDescription">Seconds after introduction before skip prompt is hidden.</div>
|
|
||||||
<br />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="inputContainer">
|
|
||||||
<label class="inputLabel inputLabelUnfocused" for="RemainingSecondsOfIntro"> Intro playback duration (in seconds) </label>
|
|
||||||
<input id="RemainingSecondsOfIntro" type="number" is="emby-input" min="0" />
|
|
||||||
<div class="fieldDescription">Seconds of introduction ending that should be played. Defaults to 2.</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
<summary>User Interface Customization</summary>
|
<summary>User Interface Customization</summary>
|
||||||
|
|
||||||
<br />
|
<br />
|
||||||
|
<div id="SkipButtonSettings">
|
||||||
|
<div id="PersistContainer" class="checkboxContainer checkboxContainer-withDescription">
|
||||||
|
<label class="emby-checkbox-label">
|
||||||
|
<input id="PersistSkipButton" type="checkbox" is="emby-checkbox" />
|
||||||
|
<span>Display Button for Segment Duration</span>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<div class="fieldDescription">
|
||||||
|
If checked, skip button will remain visible for the entire intro (offset and timeout are ignored).
|
||||||
|
<br />
|
||||||
|
Note: If unchecked, button will only appear in the player controls after the set timeout.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="divShowPromptAdjustment" class="inputContainer">
|
||||||
|
<label class="inputLabel inputLabelUnfocused" for="ShowPromptAdjustment"> Skip prompt offset (in seconds) </label>
|
||||||
|
<input id="ShowPromptAdjustment" type="number" is="emby-input" min="0" />
|
||||||
|
<div class="fieldDescription">Seconds to display skip prompt before introduction begins.</div>
|
||||||
|
<br />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="divHidePromptAdjustment" class="inputContainer">
|
||||||
|
<label class="inputLabel inputLabelUnfocused" for="HidePromptAdjustment"> Skip prompt timeout (in seconds) </label>
|
||||||
|
<input id="HidePromptAdjustment" type="number" is="emby-input" min="2" />
|
||||||
|
<div class="fieldDescription">Seconds after introduction before skip prompt is hidden.</div>
|
||||||
|
<br />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="inputContainer">
|
||||||
|
<label class="inputLabel inputLabelUnfocused" for="RemainingSecondsOfIntro"> Intro playback duration (in seconds) </label>
|
||||||
|
<input id="RemainingSecondsOfIntro" type="number" is="emby-input" min="0" />
|
||||||
|
<div class="fieldDescription">Seconds of introduction ending that should be played. Defaults to 2.</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="inputContainer">
|
<div class="inputContainer">
|
||||||
<label class="inputLabel" for="SkipButtonIntroText"> Skip intro button text </label>
|
<label class="inputLabel" for="SkipButtonIntroText"> Skip intro button text </label>
|
||||||
<input id="SkipButtonIntroText" type="text" is="emby-input" />
|
<input id="SkipButtonIntroText" type="text" is="emby-input" />
|
||||||
@ -390,20 +407,16 @@
|
|||||||
<fieldset class="verticalSection-extrabottompadding">
|
<fieldset class="verticalSection-extrabottompadding">
|
||||||
<legend>Advanced</legend>
|
<legend>Advanced</legend>
|
||||||
|
|
||||||
<details id="support">
|
|
||||||
<summary>Support Bundle Info</summary>
|
|
||||||
|
|
||||||
<textarea id="supportBundle" rows="20" cols="75" readonly></textarea>
|
|
||||||
</details>
|
|
||||||
|
|
||||||
<details id="visualizer">
|
<details id="visualizer">
|
||||||
<summary>Manage Timestamps & Fingerprints</summary>
|
<summary>Manage Timestamps & Fingerprints</summary>
|
||||||
|
|
||||||
<br />
|
<br />
|
||||||
<label class="inputLabel" for="troubleshooterShow">Select TV Series to manage</label>
|
<label class="inputLabel" for="troubleshooterShow">Select TV series / movie to manage</label>
|
||||||
<select is="emby-select" id="troubleshooterShow" class="emby-select-withcolor emby-select"></select>
|
<select is="emby-select" id="troubleshooterShow" class="emby-select-withcolor emby-select"></select>
|
||||||
<label class="inputLabel" for="troubleshooterSeason">Select Season to manage</label>
|
<div id="seasonSelection">
|
||||||
<select is="emby-select" id="troubleshooterSeason" class="emby-select-withcolor emby-select"></select>
|
<label class="inputLabel" for="troubleshooterSeason">Select season to manage</label>
|
||||||
|
<select is="emby-select" id="troubleshooterSeason" class="emby-select-withcolor emby-select"></select>
|
||||||
|
</div>
|
||||||
<br />
|
<br />
|
||||||
|
|
||||||
<div id="ignorelistSection" style="display: none">
|
<div id="ignorelistSection" style="display: none">
|
||||||
@ -426,16 +439,18 @@
|
|||||||
</div>
|
</div>
|
||||||
<br />
|
<br />
|
||||||
|
|
||||||
<button id="saveIgnoreListSeries" class="raised button-submit block emby-button">Apply to series</button>
|
<button is="emby-button" id="saveIgnoreListSeries" class="raised button-submit block emby-button">Apply to series / movie</button>
|
||||||
<button id="saveIgnoreListSeason" class="raised button-submit block emby-button" style="display: none">Apply to season</button>
|
<button is="emby-button" id="saveIgnoreListSeason" class="raised button-submit block emby-button" style="display: none">Apply to season</button>
|
||||||
</div>
|
</div>
|
||||||
<br />
|
<br />
|
||||||
|
|
||||||
<label class="inputLabel" for="troubleshooterEpisode1">Select the first episode</label>
|
<div id="episodeSelection">
|
||||||
<select is="emby-select" id="troubleshooterEpisode1" class="emby-select-withcolor emby-select"></select>
|
<label class="inputLabel" for="troubleshooterEpisode1">Select the first episode</label>
|
||||||
<label class="inputLabel" for="troubleshooterEpisode1">Select the second episode</label>
|
<select is="emby-select" id="troubleshooterEpisode1" class="emby-select-withcolor emby-select"></select>
|
||||||
<select is="emby-select" id="troubleshooterEpisode2" class="emby-select-withcolor emby-select"></select>
|
<label class="inputLabel" for="troubleshooterEpisode1">Select the second episode</label>
|
||||||
<br />
|
<select is="emby-select" id="troubleshooterEpisode2" class="emby-select-withcolor emby-select"></select>
|
||||||
|
<br />
|
||||||
|
</div>
|
||||||
|
|
||||||
<div id="timestampEditor" style="display: none">
|
<div id="timestampEditor" style="display: none">
|
||||||
<h3 style="margin: 0">Introduction timestamp editor</h3>
|
<h3 style="margin: 0">Introduction timestamp editor</h3>
|
||||||
@ -467,114 +482,138 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<br />
|
<br />
|
||||||
<h4 style="margin: 0" id="editRightEpisodeTitle"></h4>
|
<div id="rightEpisodeEditor">
|
||||||
<br />
|
<h4 style="margin: 0" id="editRightEpisodeTitle"></h4>
|
||||||
<div class="inlineForm">
|
<br />
|
||||||
<div class="inputContainer">
|
<div class="inlineForm">
|
||||||
<label class="inputLabel inputLabelUnfocused" for="introStart">Intro Start</label>
|
<div class="inputContainer">
|
||||||
<input type="text" id="editRightIntroEpisodeStartDisplay" class="emby-input custom-time-input" readonly />
|
<label class="inputLabel inputLabelUnfocused" for="introStart">Intro Start</label>
|
||||||
<input type="number" id="editRightIntroEpisodeStartEdit" class="emby-input custom-time-input" style="display: none" step="any" min="0" />
|
<input type="text" id="editRightIntroEpisodeStartDisplay" class="emby-input custom-time-input" readonly />
|
||||||
|
<input type="number" id="editRightIntroEpisodeStartEdit" class="emby-input custom-time-input" style="display: none" step="any" min="0" />
|
||||||
|
</div>
|
||||||
|
<div class="inputContainer">
|
||||||
|
<label class="inputLabel inputLabelUnfocused" for="introEnd">Intro End</label>
|
||||||
|
<input type="text" id="editRightIntroEpisodeEndDisplay" class="emby-input custom-time-input" readonly />
|
||||||
|
<input type="number" id="editRightIntroEpisodeEndEdit" class="emby-input custom-time-input" style="display: none" step="any" min="0" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="inputContainer">
|
<div class="inlineForm">
|
||||||
<label class="inputLabel inputLabelUnfocused" for="introEnd">Intro End</label>
|
<div class="inputContainer">
|
||||||
<input type="text" id="editRightIntroEpisodeEndDisplay" class="emby-input custom-time-input" readonly />
|
<label class="inputLabel inputLabelUnfocused" for="creditsStart">Credits Start</label>
|
||||||
<input type="number" id="editRightIntroEpisodeEndEdit" class="emby-input custom-time-input" style="display: none" step="any" min="0" />
|
<input type="text" id="editRightCreditEpisodeStartDisplay" class="emby-input custom-time-input" readonly />
|
||||||
|
<input type="number" id="editRightCreditEpisodeStartEdit" class="emby-input custom-time-input" style="display: none" step="any" min="0" />
|
||||||
|
</div>
|
||||||
|
<div class="inputContainer">
|
||||||
|
<label class="inputLabel inputLabelUnfocused" for="creditsEnd">Credits End</label>
|
||||||
|
<input type="text" id="editRightCreditEpisodeEndDisplay" class="emby-input custom-time-input" readonly />
|
||||||
|
<input type="number" id="editRightCreditEpisodeEndEdit" class="emby-input custom-time-input" style="display: none" step="any" min="0" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<br />
|
||||||
</div>
|
</div>
|
||||||
<div class="inlineForm">
|
<button is="emby-button" id="btnUpdateTimestamps" class="raised button-submit block emby-button" type="button">Update timestamps</button>
|
||||||
<div class="inputContainer">
|
|
||||||
<label class="inputLabel inputLabelUnfocused" for="creditsStart">Credits Start</label>
|
|
||||||
<input type="text" id="editRightCreditEpisodeStartDisplay" class="emby-input custom-time-input" readonly />
|
|
||||||
<input type="number" id="editRightCreditEpisodeStartEdit" class="emby-input custom-time-input" style="display: none" step="any" min="0" />
|
|
||||||
</div>
|
|
||||||
<div class="inputContainer">
|
|
||||||
<label class="inputLabel inputLabelUnfocused" for="creditsEnd">Credits End</label>
|
|
||||||
<input type="text" id="editRightCreditEpisodeEndDisplay" class="emby-input custom-time-input" readonly />
|
|
||||||
<input type="number" id="editRightCreditEpisodeEndEdit" class="emby-input custom-time-input" style="display: none" step="any" min="0" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<br />
|
|
||||||
<button is="emby-select" id="btnUpdateTimestamps" class="raised button-submit block emby-button" type="button">Update timestamps</button>
|
|
||||||
<br />
|
<br />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="timestampErrorDiv" style="display: none">
|
<div id="timestampErrorDiv" style="display: none">
|
||||||
|
<br />
|
||||||
<textarea id="timestampError" rows="2" cols="75" readonly></textarea>
|
<textarea id="timestampError" rows="2" cols="75" readonly></textarea>
|
||||||
<br />
|
<br />
|
||||||
<br />
|
<br />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h3>Fingerprint Visualizer</h3>
|
<div id="fingerprintVisualizer" style="display: none">
|
||||||
<p>
|
|
||||||
Interactively compare the audio fingerprints of two episodes. <br />
|
|
||||||
The blue and red bar to the right of the fingerprint diff turns blue when the corresponding fingerprint points are at least 80% similar.
|
|
||||||
</p>
|
|
||||||
<table>
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<td style="min-width: 100px; font-weight: bold">Key</td>
|
|
||||||
<td style="font-weight: bold">Function</td>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td>Up arrow</td>
|
|
||||||
<td>Shift the left episode up by 0.1238 seconds. Holding control will shift the episode by 10 seconds.</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Down arrow</td>
|
|
||||||
<td>Shift the left episode down by 0.1238 seconds. Holding control will shift the episode by 10 seconds.</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Right arrow</td>
|
|
||||||
<td>Advance to the next pair of episodes.</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Left arrow</td>
|
|
||||||
<td>Go back to the previous pair of episodes.</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
<br />
|
|
||||||
|
|
||||||
<span>Shift amount:</span>
|
<h3>Fingerprint Visualizer</h3>
|
||||||
<input type="number" min="-3000" max="3000" value="0" id="offset" />
|
<p>
|
||||||
<br />
|
Interactively compare the audio fingerprints of two episodes. <br />
|
||||||
<span id="suggestedShifts">Suggested shifts:</span>
|
The blue and red bar to the right of the fingerprint diff turns blue when the corresponding fingerprint points are at least 80% similar.
|
||||||
<br />
|
</p>
|
||||||
<br />
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<td style="min-width: 100px; font-weight: bold">Key</td>
|
||||||
|
<td style="font-weight: bold">Function</td>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td>Up arrow</td>
|
||||||
|
<td>Shift the left episode up by 0.1238 seconds. Holding control will shift the episode by 10 seconds.</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Down arrow</td>
|
||||||
|
<td>Shift the left episode down by 0.1238 seconds. Holding control will shift the episode by 10 seconds.</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Right arrow</td>
|
||||||
|
<td>Advance to the next pair of episodes.</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Left arrow</td>
|
||||||
|
<td>Go back to the previous pair of episodes.</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<br />
|
||||||
|
|
||||||
<canvas id="troubleshooter" style="display: none"></canvas>
|
<span>Shift amount:</span>
|
||||||
<span id="timestampContainer">
|
<input type="number" min="-3000" max="3000" value="0" id="offset" />
|
||||||
<span id="timestamps"></span> <br />
|
<br />
|
||||||
<span id="intros"></span>
|
<span id="suggestedShifts">
|
||||||
</span>
|
<span>Suggested shifts: </span>
|
||||||
<br />
|
</span>
|
||||||
<br />
|
<br />
|
||||||
|
<br />
|
||||||
<div id="eraseSeasonContainer" style="display: none">
|
<canvas id="troubleshooter" style="display: none"></canvas>
|
||||||
<button id="btnEraseSeasonTimestamps" type="button">Erase all timestamps for this season</button>
|
<span id="timestampContainer">
|
||||||
|
<span id="timestamps"></span>
|
||||||
<input type="checkbox" id="eraseSeasonCacheCheckbox" style="margin-left: 10px" />
|
<br />
|
||||||
<label for="eraseSeasonCacheCheckbox" style="margin-left: 5px">Erase cached fingerprint files</label>
|
<span id="intros"></span>
|
||||||
|
</span>
|
||||||
|
<br />
|
||||||
</div>
|
</div>
|
||||||
<br />
|
<br />
|
||||||
|
|
||||||
<button id="btnEraseIntroTimestamps">Erase all introduction timestamps (globally)</button>
|
<div id="eraseSeasonContainer" style="display: none">
|
||||||
|
<button is="emby-button" id="btnEraseSeasonTimestamps" class="button-submit emby-button" type="button">Erase all timestamps for this season</button>
|
||||||
|
|
||||||
|
<input type="checkbox" id="eraseSeasonCacheCheckbox" style="margin-left: 10px" />
|
||||||
|
<label for="eraseSeasonCacheCheckbox" style="margin-left: 5px">Erase cached fingerprint files</label>
|
||||||
|
<br />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="eraseMovieContainer" style="display: none">
|
||||||
|
<button is="emby-button" id="btnEraseMovieTimestamps" class="button-submit emby-button" type="button">Erase all timestamps for this movie</button>
|
||||||
|
|
||||||
|
<input type="checkbox" id="eraseMovieCacheCheckbox" style="margin-left: 10px" />
|
||||||
|
<label for="eraseMovieCacheCheckbox" style="margin-left: 5px">Erase cached fingerprint files</label>
|
||||||
|
<br />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button is="emby-button" class="button-submit emby-button" id="btnEraseIntroTimestamps">Erase all introduction timestamps (globally)</button>
|
||||||
<br />
|
<br />
|
||||||
|
|
||||||
<button id="btnEraseCreditTimestamps">Erase all end credits timestamps (globally)</button>
|
<button is="emby-button" class="button-submit emby-button" id="btnEraseCreditTimestamps">Erase all end credits timestamps (globally)</button>
|
||||||
|
<br />
|
||||||
<br />
|
<br />
|
||||||
|
|
||||||
<input type="checkbox" id="eraseModeCacheCheckbox" style="margin-left: 10px" />
|
<input type="checkbox" id="eraseModeCacheCheckbox" style="margin-left: 10px" />
|
||||||
<label for="eraseModeCacheCheckbox" style="margin-left: 5px">Erase cached fingerprint files</label>
|
<label for="eraseModeCacheCheckbox" style="margin-left: 5px">Erase cached fingerprint files</label>
|
||||||
|
<br />
|
||||||
|
<br />
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details id="support">
|
||||||
|
<summary>Support Bundle Info</summary>
|
||||||
|
|
||||||
|
<textarea id="supportBundle" rows="20" cols="75" readonly></textarea>
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
<details id="storage">
|
<details id="storage">
|
||||||
<br />
|
<br />
|
||||||
<summary>Storage Usage</summary>
|
<summary>Storage Usage</summary>
|
||||||
<div class="fieldDescription">See how much space each library uses.</div>
|
<div class="fieldDescription">See how much space each library uses.</div>
|
||||||
<textarea id="storage" rows="20" cols="75" readonly></textarea>
|
<textarea id="storageText" rows="20" cols="75" readonly></textarea>
|
||||||
</details>
|
</details>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
</form>
|
</form>
|
||||||
@ -616,6 +655,7 @@
|
|||||||
"MaximumIntroDuration",
|
"MaximumIntroDuration",
|
||||||
"MinimumCreditsDuration",
|
"MinimumCreditsDuration",
|
||||||
"MaximumCreditsDuration",
|
"MaximumCreditsDuration",
|
||||||
|
"MaximumMovieCreditsDuration",
|
||||||
"EdlAction",
|
"EdlAction",
|
||||||
"ProcessPriority",
|
"ProcessPriority",
|
||||||
"ProcessThreads",
|
"ProcessThreads",
|
||||||
@ -635,7 +675,7 @@
|
|||||||
"AutoSkipCreditsNotificationText",
|
"AutoSkipCreditsNotificationText",
|
||||||
];
|
];
|
||||||
|
|
||||||
var booleanConfigurationFields = ["AutoDetectIntros", "AutoDetectCredits", "AnalyzeSeasonZero", "SelectAllLibraries", "RegenerateEdlFiles", "CacheFingerprints", "AutoSkip", "AutoSkipCredits", "SkipFirstEpisode", "PersistSkipButton", "SkipButtonVisible"];
|
var booleanConfigurationFields = ["AutoDetectIntros", "AutoDetectCredits", "AnalyzeMovies", "AnalyzeSeasonZero", "SelectAllLibraries", "RegenerateEdlFiles", "CacheFingerprints", "AutoSkip", "AutoSkipCredits", "SkipFirstEpisode", "PersistSkipButton", "SkipButtonVisible"];
|
||||||
|
|
||||||
// visualizer elements
|
// visualizer elements
|
||||||
var ignorelistSection = document.querySelector("div#ignorelistSection");
|
var ignorelistSection = document.querySelector("div#ignorelistSection");
|
||||||
@ -645,20 +685,27 @@
|
|||||||
var saveIgnoreListSeriesButton = ignorelistSection.querySelector("button#saveIgnoreListSeries");
|
var saveIgnoreListSeriesButton = ignorelistSection.querySelector("button#saveIgnoreListSeries");
|
||||||
var canvas = document.querySelector("canvas#troubleshooter");
|
var canvas = document.querySelector("canvas#troubleshooter");
|
||||||
var selectShow = document.querySelector("select#troubleshooterShow");
|
var selectShow = document.querySelector("select#troubleshooterShow");
|
||||||
|
var seasonSelection = document.getElementById("seasonSelection");
|
||||||
var selectSeason = document.querySelector("select#troubleshooterSeason");
|
var selectSeason = document.querySelector("select#troubleshooterSeason");
|
||||||
|
var episodeSelection = document.getElementById("episodeSelection");
|
||||||
var selectEpisode1 = document.querySelector("select#troubleshooterEpisode1");
|
var selectEpisode1 = document.querySelector("select#troubleshooterEpisode1");
|
||||||
var selectEpisode2 = document.querySelector("select#troubleshooterEpisode2");
|
var selectEpisode2 = document.querySelector("select#troubleshooterEpisode2");
|
||||||
var txtOffset = document.querySelector("input#offset");
|
var txtOffset = document.querySelector("input#offset");
|
||||||
var txtSuggested = document.querySelector("span#suggestedShifts");
|
var txtSuggested = document.querySelector("span#suggestedShifts");
|
||||||
var btnSeasonEraseTimestamps = document.querySelector("button#btnEraseSeasonTimestamps");
|
var btnSeasonEraseTimestamps = document.querySelector("button#btnEraseSeasonTimestamps");
|
||||||
var eraseSeasonContainer = document.getElementById("eraseSeasonContainer");
|
var eraseSeasonContainer = document.getElementById("eraseSeasonContainer");
|
||||||
|
var btnMovieEraseTimestamps = document.querySelector("button#btnEraseMovieTimestamps");
|
||||||
|
var eraseMovieContainer = document.getElementById("eraseMovieContainer");
|
||||||
var timestampError = document.querySelector("textarea#timestampError");
|
var timestampError = document.querySelector("textarea#timestampError");
|
||||||
var timestampEditor = document.querySelector("#timestampEditor");
|
var timestampEditor = document.querySelector("#timestampEditor");
|
||||||
|
var rightEpisodeEditor = document.getElementById("rightEpisodeEditor");
|
||||||
var btnUpdateTimestamps = document.querySelector("button#btnUpdateTimestamps");
|
var btnUpdateTimestamps = document.querySelector("button#btnUpdateTimestamps");
|
||||||
var timeContainer = document.querySelector("span#timestampContainer");
|
var timeContainer = document.querySelector("span#timestampContainer");
|
||||||
|
var fingerprintVisualizer = document.getElementById("fingerprintVisualizer");
|
||||||
|
|
||||||
var windowHashInterval = 0;
|
var windowHashInterval = 0;
|
||||||
|
|
||||||
|
var analyzeMovies = document.getElementById("AnalyzeMovies");
|
||||||
var autoSkip = document.querySelector("input#AutoSkip");
|
var autoSkip = document.querySelector("input#AutoSkip");
|
||||||
var skipButtonVisible = document.getElementById("SkipButtonVisible");
|
var skipButtonVisible = document.getElementById("SkipButtonVisible");
|
||||||
var skipButtonVisibleLabel = document.getElementById("SkipButtonVisibleLabel");
|
var skipButtonVisibleLabel = document.getElementById("SkipButtonVisibleLabel");
|
||||||
@ -669,6 +716,7 @@
|
|||||||
var secondsOfIntroStartToPlay = document.querySelector("div#divSecondsOfIntroStartToPlay");
|
var secondsOfIntroStartToPlay = document.querySelector("div#divSecondsOfIntroStartToPlay");
|
||||||
var autoSkipClientList = document.getElementById("AutoSkipClientList");
|
var autoSkipClientList = document.getElementById("AutoSkipClientList");
|
||||||
var secondsOfCreditsStartToPlay = document.querySelector("div#divSecondsOfCreditsStartToPlay");
|
var secondsOfCreditsStartToPlay = document.querySelector("div#divSecondsOfCreditsStartToPlay");
|
||||||
|
var movieCreditsDuration = document.getElementById("movieCreditsDuration");
|
||||||
var autoSkipNotificationText = document.querySelector("div#divAutoSkipNotificationText");
|
var autoSkipNotificationText = document.querySelector("div#divAutoSkipNotificationText");
|
||||||
var autoSkipCredits = document.querySelector("input#AutoSkipCredits");
|
var autoSkipCredits = document.querySelector("input#AutoSkipCredits");
|
||||||
var autoSkipCreditsNotificationText = document.querySelector("div#divAutoSkipCreditsNotificationText");
|
var autoSkipCreditsNotificationText = document.querySelector("div#divAutoSkipCreditsNotificationText");
|
||||||
@ -692,15 +740,15 @@
|
|||||||
} else if (autoSkip.checked) {
|
} else if (autoSkip.checked) {
|
||||||
autoSkipClientList.style.display = "unset";
|
autoSkipClientList.style.display = "unset";
|
||||||
autoSkipClientList.style.width = "100%";
|
autoSkipClientList.style.width = "100%";
|
||||||
skipButtonVisibleLabel.textContent = "Show skip credit button";
|
skipButtonVisibleLabel.textContent = "Show Skip Credit Button";
|
||||||
} else if (autoSkipCredits.checked) {
|
} else if (autoSkipCredits.checked) {
|
||||||
autoSkipClientList.style.display = "unset";
|
autoSkipClientList.style.display = "unset";
|
||||||
autoSkipClientList.style.width = "100%";
|
autoSkipClientList.style.width = "100%";
|
||||||
skipButtonVisibleLabel.textContent = "Show skip intro button";
|
skipButtonVisibleLabel.textContent = "Show Skip Intro Button";
|
||||||
} else {
|
} else {
|
||||||
autoSkipClientList.style.display = "unset";
|
autoSkipClientList.style.display = "unset";
|
||||||
autoSkipClientList.style.width = "100%";
|
autoSkipClientList.style.width = "100%";
|
||||||
skipButtonVisibleLabel.textContent = "Show skip intro / credit button";
|
skipButtonVisibleLabel.textContent = "Show All Skip Buttons";
|
||||||
}
|
}
|
||||||
skipButtonVisibleChanged();
|
skipButtonVisibleChanged();
|
||||||
}
|
}
|
||||||
@ -780,7 +828,7 @@
|
|||||||
|
|
||||||
async function populateLibraries() {
|
async function populateLibraries() {
|
||||||
const response = await getJson("Library/VirtualFolders");
|
const response = await getJson("Library/VirtualFolders");
|
||||||
const tvLibraries = response.filter((item) => item.CollectionType === undefined || item.CollectionType === "tvshows");
|
const tvLibraries = response.filter((item) => item.CollectionType === undefined || item.CollectionType === "tvshows" || item.CollectionType === "movies");
|
||||||
const libraryNames = tvLibraries.map((lib) => lib.Name || "Unnamed Library");
|
const libraryNames = tvLibraries.map((lib) => lib.Name || "Unnamed Library");
|
||||||
generateCheckboxList(libraryNames, "libraryCheckboxes", "SelectedLibraries");
|
generateCheckboxList(libraryNames, "libraryCheckboxes", "SelectedLibraries");
|
||||||
}
|
}
|
||||||
@ -802,6 +850,16 @@
|
|||||||
|
|
||||||
persistSkip.addEventListener("change", persistSkipChanged);
|
persistSkip.addEventListener("change", persistSkipChanged);
|
||||||
|
|
||||||
|
async function analyzeMoviesChanged() {
|
||||||
|
if (analyzeMovies.checked) {
|
||||||
|
movieCreditsDuration.style.display = "unset";
|
||||||
|
} else {
|
||||||
|
movieCreditsDuration.style.display = "none";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
analyzeMovies.addEventListener("change", analyzeMoviesChanged);
|
||||||
|
|
||||||
// when the fingerprint visualizer opens, populate show names
|
// when the fingerprint visualizer opens, populate show names
|
||||||
async function visualizerToggled() {
|
async function visualizerToggled() {
|
||||||
if (!visualizer.open) {
|
if (!visualizer.open) {
|
||||||
@ -887,14 +945,17 @@
|
|||||||
const bundleText = await bundle.text();
|
const bundleText = await bundle.text();
|
||||||
|
|
||||||
// Display it to the user
|
// Display it to the user
|
||||||
const ta = document.querySelector("textarea#storage");
|
const ta = document.querySelector("textarea#storageText");
|
||||||
ta.value = bundleText;
|
ta.value = bundleText;
|
||||||
}
|
}
|
||||||
|
|
||||||
// show changed, populate seasons
|
// show changed, populate seasons
|
||||||
async function showChanged() {
|
async function showChanged() {
|
||||||
|
seasonSelection.style.display = "unset";
|
||||||
clearSelect(selectSeason);
|
clearSelect(selectSeason);
|
||||||
eraseSeasonContainer.style.display = "none";
|
eraseSeasonContainer.style.display = "none";
|
||||||
|
eraseMovieContainer.style.display = "none";
|
||||||
|
episodeSelection.style.display = "unset";
|
||||||
clearSelect(selectEpisode1);
|
clearSelect(selectEpisode1);
|
||||||
clearSelect(selectEpisode2);
|
clearSelect(selectEpisode2);
|
||||||
|
|
||||||
@ -907,6 +968,13 @@
|
|||||||
saveIgnoreListSeasonButton.style.display = "none";
|
saveIgnoreListSeasonButton.style.display = "none";
|
||||||
Dashboard.hideLoadingMsg();
|
Dashboard.hideLoadingMsg();
|
||||||
|
|
||||||
|
if (shows[selectShow.value].IsMovie) {
|
||||||
|
movieLoaded();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
saveIgnoreListSeriesButton.textContent = "Apply to series";
|
||||||
|
|
||||||
// add all seasons from this show to the season select
|
// add all seasons from this show to the season select
|
||||||
for (const season in shows[selectShow.value].Seasons) {
|
for (const season in shows[selectShow.value].Seasons) {
|
||||||
addItem(selectSeason, "Season " + shows[selectShow.value].Seasons[season], season);
|
addItem(selectSeason, "Season " + shows[selectShow.value].Seasons[season], season);
|
||||||
@ -957,20 +1025,23 @@
|
|||||||
Dashboard.showLoadingMsg();
|
Dashboard.showLoadingMsg();
|
||||||
|
|
||||||
timestampError.value = "";
|
timestampError.value = "";
|
||||||
|
fingerprintVisualizer.style.display = "unset";
|
||||||
canvas.style.display = "none";
|
canvas.style.display = "none";
|
||||||
|
|
||||||
lhs = await getJson("Intros/Episode/" + selectEpisode1.value + "/Chromaprint");
|
lhs = await getJson("Intros/Episode/" + selectEpisode1.value + "/Chromaprint");
|
||||||
if (lhs === undefined) {
|
if (lhs === undefined) {
|
||||||
timestampError.value += "Error: " + selectEpisode1.value + " fingerprints failed!\n";
|
timestampError.value += "Error: " + selectEpisode1.value + " fingerprints failed!\n";
|
||||||
} else if (lhs === null) {
|
} else if (lhs === null) {
|
||||||
timestampError.value += "Error: " + selectEpisode1.value + " fingerprints missing!\n";
|
timestampError.value += selectEpisode1.value + " fingerprints missing or incomplete.\n";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
rightEpisodeEditor.style.display = "unset";
|
||||||
|
|
||||||
rhs = await getJson("Intros/Episode/" + selectEpisode2.value + "/Chromaprint");
|
rhs = await getJson("Intros/Episode/" + selectEpisode2.value + "/Chromaprint");
|
||||||
if (rhs === undefined) {
|
if (rhs === undefined) {
|
||||||
timestampError.value += "Error: " + selectEpisode2.value + " fingerprints failed!";
|
timestampError.value += "Error: " + selectEpisode2.value + " fingerprints failed!";
|
||||||
} else if (rhs === null) {
|
} else if (rhs === null) {
|
||||||
timestampError.value += "Error: " + selectEpisode2.value + " fingerprints missing!\n";
|
timestampError.value += selectEpisode2.value + " fingerprints missing or incomplete.\n";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (timestampError.value == "") {
|
if (timestampError.value == "") {
|
||||||
@ -988,6 +1059,58 @@
|
|||||||
updateTimestampEditor();
|
updateTimestampEditor();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function movieLoaded() {
|
||||||
|
|
||||||
|
Dashboard.showLoadingMsg();
|
||||||
|
|
||||||
|
saveIgnoreListSeriesButton.textContent = "Apply to movie";
|
||||||
|
seasonSelection.style.display = "none";
|
||||||
|
episodeSelection.style.display = "none";
|
||||||
|
eraseMovieContainer.style.display = "unset";
|
||||||
|
|
||||||
|
timestampError.value = "";
|
||||||
|
fingerprintVisualizer.style.display = "none";
|
||||||
|
|
||||||
|
lhs = await getJson("Intros/Episode/" + selectShow.value + "/Chromaprint");
|
||||||
|
if (lhs === undefined) {
|
||||||
|
timestampError.value += "Error: " + selectShow.value + " fingerprints failed!\n";
|
||||||
|
} else if (lhs === null) {
|
||||||
|
timestampError.value += selectShow.value + " fingerprints missing or incomplete.\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
rightEpisodeEditor.style.display = "none";
|
||||||
|
|
||||||
|
if (timestampError.value == "") {
|
||||||
|
timestampErrorDiv.style.display = "none";
|
||||||
|
} else {
|
||||||
|
timestampErrorDiv.style.display = "unset";
|
||||||
|
}
|
||||||
|
|
||||||
|
Dashboard.hideLoadingMsg();
|
||||||
|
|
||||||
|
txtOffset.value = "0";
|
||||||
|
|
||||||
|
// Try to get the timestamps of each intro, falling back a default value of zero if no intro was found
|
||||||
|
const leftEpisodeJson = await getJson("Episode/" + selectShow.value + "/Timestamps");
|
||||||
|
|
||||||
|
// Update the editor for the movie
|
||||||
|
timestampEditor.style.display = "unset";
|
||||||
|
document.querySelector("#editLeftEpisodeTitle").textContent = selectShow.value;
|
||||||
|
document.querySelector("#editLeftIntroEpisodeStartEdit").value = leftEpisodeJson.Introduction.Start;
|
||||||
|
document.querySelector("#editLeftIntroEpisodeEndEdit").value = leftEpisodeJson.Introduction.End;
|
||||||
|
document.querySelector("#editLeftCreditEpisodeStartEdit").value = leftEpisodeJson.Credits.Start;
|
||||||
|
document.querySelector("#editLeftCreditEpisodeEndEdit").value = leftEpisodeJson.Credits.End;
|
||||||
|
|
||||||
|
// Update display inputs
|
||||||
|
const inputs = document.querySelectorAll('#timestampEditor input[type="number"]');
|
||||||
|
inputs.forEach((input) => {
|
||||||
|
const displayInput = document.getElementById(input.id.replace("Edit", "Display"));
|
||||||
|
displayInput.value = formatTime(parseFloat(input.value) || 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
setupTimeInputs();
|
||||||
|
}
|
||||||
|
|
||||||
function setupTimeInputs() {
|
function setupTimeInputs() {
|
||||||
const timestampEditor = document.getElementById("timestampEditor");
|
const timestampEditor = document.getElementById("timestampEditor");
|
||||||
timestampEditor.querySelectorAll(".inputContainer").forEach((container) => {
|
timestampEditor.querySelectorAll(".inputContainer").forEach((container) => {
|
||||||
@ -1215,8 +1338,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (config["SkipButtonWarning"]) {
|
if (config["SkipButtonWarning"]) {
|
||||||
document.getElementById("SkipButtonContainer").style.display = "none";
|
document.getElementById("warningMessage").style.display = "unset";
|
||||||
document.getElementById("PersistContainer").style.display = "none";
|
|
||||||
} else {
|
} else {
|
||||||
document.getElementById("warningMessage").style.display = "none";
|
document.getElementById("warningMessage").style.display = "none";
|
||||||
}
|
}
|
||||||
@ -1285,6 +1407,22 @@
|
|||||||
document.getElementById("eraseSeasonCacheCheckbox").checked = false;
|
document.getElementById("eraseSeasonCacheCheckbox").checked = false;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
btnMovieEraseTimestamps.addEventListener("click", () => {
|
||||||
|
Dashboard.confirm("Are you sure you want to erase all timestamps for this movie?", "Confirm timestamp erasure", (result) => {
|
||||||
|
if (!result) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const show = selectShow.value;
|
||||||
|
const eraseCacheChecked = document.getElementById("eraseMovieCacheCheckbox").checked;
|
||||||
|
|
||||||
|
const url = "Intros/Show/" + encodeURIComponent(show);
|
||||||
|
fetchWithAuth(url + "?eraseCache=" + eraseCacheChecked, "DELETE", null);
|
||||||
|
|
||||||
|
Dashboard.alert("Erased timestamps for " + show);
|
||||||
|
document.getElementById("eraseMovieCacheCheckbox").checked = false;
|
||||||
|
});
|
||||||
|
});
|
||||||
saveIgnoreListSeasonButton.addEventListener("click", () => {
|
saveIgnoreListSeasonButton.addEventListener("click", () => {
|
||||||
Dashboard.showLoadingMsg();
|
Dashboard.showLoadingMsg();
|
||||||
|
|
591
IntroSkipper/Configuration/inject.js
Normal file
591
IntroSkipper/Configuration/inject.js
Normal file
@ -0,0 +1,591 @@
|
|||||||
|
const introSkipper = {
|
||||||
|
originalFetch: window.fetch.bind(window),
|
||||||
|
d: (msg) => console.debug("[intro skipper] ", msg),
|
||||||
|
setup() {
|
||||||
|
const self = this;
|
||||||
|
this.initializeState();
|
||||||
|
this.initializeObserver();
|
||||||
|
this.currentOption =
|
||||||
|
localStorage.getItem("introskipperOption") || "Show Button";
|
||||||
|
window.fetch = this.fetchWrapper.bind(this);
|
||||||
|
document.addEventListener("viewshow", this.viewShow.bind(this));
|
||||||
|
this.videoPositionChanged = this.videoPositionChanged.bind(this);
|
||||||
|
this.handleEscapeKey = this.handleEscapeKey.bind(this);
|
||||||
|
this.d("Registered hooks");
|
||||||
|
},
|
||||||
|
initializeState() {
|
||||||
|
Object.assign(this, {
|
||||||
|
allowEnter: true,
|
||||||
|
skipSegments: {},
|
||||||
|
videoPlayer: null,
|
||||||
|
skipButton: null,
|
||||||
|
osdElement: null,
|
||||||
|
skipperData: null,
|
||||||
|
currentEpisodeId: null,
|
||||||
|
injectMetadata: false,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
initializeObserver() {
|
||||||
|
this.observer = new MutationObserver((mutations) => {
|
||||||
|
const actionSheet =
|
||||||
|
mutations[mutations.length - 1].target.querySelector(".actionSheet");
|
||||||
|
if (
|
||||||
|
actionSheet &&
|
||||||
|
!actionSheet.querySelector(`[data-id="${"introskipperMenu"}"]`)
|
||||||
|
)
|
||||||
|
this.injectIntroSkipperOptions(actionSheet);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
fetchWrapper(resource, options) {
|
||||||
|
const response = this.originalFetch(resource, options);
|
||||||
|
const url = new URL(resource);
|
||||||
|
if (url.pathname.includes("/PlaybackInfo")) {
|
||||||
|
this.processPlaybackInfo(url.pathname);
|
||||||
|
}
|
||||||
|
else if (this.injectMetadata && url.pathname.includes("/MetadataEditor")) {
|
||||||
|
this.processMetadata(url.pathname);
|
||||||
|
}
|
||||||
|
return response;
|
||||||
|
},
|
||||||
|
async processPlaybackInfo(url) {
|
||||||
|
const id = this.extractId(url);
|
||||||
|
if (id) {
|
||||||
|
try {
|
||||||
|
this.skipSegments = await this.secureFetch(
|
||||||
|
`Episode/${id}/IntroSkipperSegments`,
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
this.d(`Error fetching skip segments: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async processMetadata(url) {
|
||||||
|
const id = this.extractId(url);
|
||||||
|
if (id) {
|
||||||
|
try {
|
||||||
|
this.skipperData = await this.secureFetch(`Episode/${id}/Timestamps`);
|
||||||
|
if (this.skipperData) {
|
||||||
|
this.currentEpisodeId = id;
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
const metadataFormFields = document.querySelector(
|
||||||
|
".metadataFormFields",
|
||||||
|
);
|
||||||
|
metadataFormFields && this.injectSkipperFields(metadataFormFields);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Error processing", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
extractId(searchString) {
|
||||||
|
const startIndex = searchString.indexOf("Items/") + 6;
|
||||||
|
const endIndex = searchString.indexOf("/", startIndex);
|
||||||
|
return endIndex !== -1
|
||||||
|
? searchString.substring(startIndex, endIndex)
|
||||||
|
: searchString.substring(startIndex);
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Event handler that runs whenever the current view changes.
|
||||||
|
* Used to detect the start of video playback.
|
||||||
|
*/
|
||||||
|
viewShow() {
|
||||||
|
const location = window.location.hash;
|
||||||
|
this.d(`Location changed to ${location}`);
|
||||||
|
this.allowEnter = true;
|
||||||
|
this.injectMetadata = /#\/(tv|details|home|search)/.test(location);
|
||||||
|
if (location === "#/video") {
|
||||||
|
this.injectCss();
|
||||||
|
this.injectButton();
|
||||||
|
this.videoPlayer = document.querySelector("video");
|
||||||
|
if (this.videoPlayer) {
|
||||||
|
this.d("Hooking video timeupdate");
|
||||||
|
this.videoPlayer.addEventListener(
|
||||||
|
"timeupdate",
|
||||||
|
this.videoPositionChanged,
|
||||||
|
);
|
||||||
|
this.osdElement = document.querySelector("div.videoOsdBottom");
|
||||||
|
this.observer.observe(document.body, {
|
||||||
|
childList: true,
|
||||||
|
subtree: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.observer.disconnect();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Injects the CSS used by the skip intro button.
|
||||||
|
* Calling this function is a no-op if the CSS has already been injected.
|
||||||
|
*/
|
||||||
|
injectCss() {
|
||||||
|
if (document.querySelector("style#introSkipperCss")) {
|
||||||
|
this.d("CSS already added");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.d("Adding CSS");
|
||||||
|
const styleElement = document.createElement("style");
|
||||||
|
styleElement.id = "introSkipperCss";
|
||||||
|
styleElement.textContent = `
|
||||||
|
:root {
|
||||||
|
--rounding: 4px;
|
||||||
|
--accent: 0, 164, 220;
|
||||||
|
}
|
||||||
|
#skipIntro.upNextContainer {
|
||||||
|
width: unset;
|
||||||
|
margin: unset;
|
||||||
|
}
|
||||||
|
#skipIntro {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 7.5em;
|
||||||
|
right: 5em;
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
#skipIntro .emby-button {
|
||||||
|
color: #ffffff;
|
||||||
|
font-size: 110%;
|
||||||
|
background: rgba(0, 0, 0, 0.7);
|
||||||
|
border-radius: var(--rounding);
|
||||||
|
box-shadow: 0 0 4px rgba(0, 0, 0, 0.6);
|
||||||
|
transition: opacity 0.3s cubic-bezier(0.4,0,0.2,1),
|
||||||
|
transform 0.3s cubic-bezier(0.4,0,0.2,1),
|
||||||
|
background-color 0.2s ease-out,
|
||||||
|
box-shadow 0.2s ease-out;
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(50%);
|
||||||
|
}
|
||||||
|
#skipIntro.show .emby-button {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
#skipIntro .emby-button:hover {
|
||||||
|
background: rgb(var(--accent));
|
||||||
|
box-shadow: 0 0 8px rgba(var(--accent), 0.6);
|
||||||
|
filter: brightness(1.2);
|
||||||
|
}
|
||||||
|
#skipIntro .emby-button:focus {
|
||||||
|
background: rgb(var(--accent));
|
||||||
|
box-shadow: 0 0 8px rgba(var(--accent), 0.6);
|
||||||
|
}
|
||||||
|
#btnSkipSegmentText {
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
padding: 0 5px 0 5px;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
document.querySelector("head").appendChild(styleElement);
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Inject the skip intro button into the video player.
|
||||||
|
* Calling this function is a no-op if the CSS has already been injected.
|
||||||
|
*/
|
||||||
|
async injectButton() {
|
||||||
|
// Ensure the button we're about to inject into the page doesn't conflict with a pre-existing one
|
||||||
|
const preExistingButton = document.querySelector("div.skipIntro");
|
||||||
|
if (preExistingButton) {
|
||||||
|
preExistingButton.style.display = "none";
|
||||||
|
}
|
||||||
|
if (document.querySelector(".btnSkipIntro.injected")) {
|
||||||
|
this.d("Button already added");
|
||||||
|
this.skipButton = document.querySelector("#skipIntro");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const config = await this.secureFetch("Intros/UserInterfaceConfiguration");
|
||||||
|
if (!config.SkipButtonVisible) {
|
||||||
|
this.d("Not adding button: not visible");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.d("Adding button");
|
||||||
|
this.skipButton = document.createElement("div");
|
||||||
|
this.skipButton.id = "skipIntro";
|
||||||
|
this.skipButton.classList.add("hide", "upNextContainer");
|
||||||
|
this.skipButton.addEventListener("click", this.doSkip.bind(this));
|
||||||
|
this.skipButton.addEventListener("keydown", this.eventHandler.bind(this));
|
||||||
|
this.skipButton.innerHTML = `
|
||||||
|
<button is="emby-button" type="button" class="btnSkipIntro injected">
|
||||||
|
<span id="btnSkipSegmentText"></span>
|
||||||
|
<span class="material-icons skip_next"></span>
|
||||||
|
</button>
|
||||||
|
`;
|
||||||
|
this.skipButton.dataset.Introduction = config.SkipButtonIntroText;
|
||||||
|
this.skipButton.dataset.Credits = config.SkipButtonEndCreditsText;
|
||||||
|
const controls = document.querySelector("div#videoOsdPage");
|
||||||
|
controls.appendChild(this.skipButton);
|
||||||
|
},
|
||||||
|
/** Tests if the OSD controls are visible. */
|
||||||
|
osdVisible() {
|
||||||
|
return this.osdElement
|
||||||
|
? !this.osdElement.classList.contains("hide")
|
||||||
|
: false;
|
||||||
|
},
|
||||||
|
/** Get the currently playing skippable segment. */
|
||||||
|
getCurrentSegment(position) {
|
||||||
|
for (const key in this.skipSegments) {
|
||||||
|
const segment = this.skipSegments[key];
|
||||||
|
if (
|
||||||
|
(position > segment.ShowSkipPromptAt &&
|
||||||
|
position < segment.HideSkipPromptAt) ||
|
||||||
|
(this.osdVisible() &&
|
||||||
|
position > segment.IntroStart &&
|
||||||
|
position < segment.IntroEnd - 3)
|
||||||
|
) {
|
||||||
|
return { ...segment, SegmentType: key };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { SegmentType: "None" };
|
||||||
|
},
|
||||||
|
overrideBlur(button) {
|
||||||
|
if (!button.originalBlur) {
|
||||||
|
button.originalBlur = button.blur;
|
||||||
|
button.blur = function () {
|
||||||
|
if (!this.contains(document.activeElement)) {
|
||||||
|
this.originalBlur();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
/** Playback position changed, check if the skip button needs to be displayed. */
|
||||||
|
videoPositionChanged() {
|
||||||
|
if (!this.skipButton || !this.allowEnter) return;
|
||||||
|
const { SegmentType: segmentType } = this.getCurrentSegment(this.videoPlayer.currentTime);
|
||||||
|
if (
|
||||||
|
segmentType === "None" ||
|
||||||
|
this.currentOption === "Off"
|
||||||
|
) {
|
||||||
|
this.hideSkipButton();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
this.currentOption === "Automatically Skip" ||
|
||||||
|
(this.currentOption === "Button w/ auto PiP" &&
|
||||||
|
document.pictureInPictureElement)
|
||||||
|
) {
|
||||||
|
this.doSkip();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const button = this.skipButton.querySelector(".emby-button");
|
||||||
|
this.skipButton.querySelector("#btnSkipSegmentText").textContent =
|
||||||
|
this.skipButton.dataset[segmentType];
|
||||||
|
if (!this.skipButton.classList.contains("hide")) {
|
||||||
|
if (!this.osdVisible() && !button.contains(document.activeElement)) {
|
||||||
|
button.focus();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
this.skipButton.classList.remove("hide");
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
this.skipButton.classList.add("show");
|
||||||
|
this.overrideBlur(button);
|
||||||
|
button.focus();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
hideSkipButton() {
|
||||||
|
if (this.skipButton.classList.contains("show")) {
|
||||||
|
this.skipButton.classList.remove("show");
|
||||||
|
const button = this.skipButton.querySelector(".emby-button");
|
||||||
|
button.addEventListener(
|
||||||
|
"transitionend",
|
||||||
|
() => {
|
||||||
|
this.skipButton.classList.add("hide");
|
||||||
|
if (this.osdVisible()) {
|
||||||
|
this.osdElement.querySelector("button.btnPause").focus();
|
||||||
|
} else {
|
||||||
|
button.originalBlur();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ once: true },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
/** Seeks to the end of the intro. */
|
||||||
|
doSkip() {
|
||||||
|
if (!this.allowEnter) return;
|
||||||
|
const segment = this.getCurrentSegment(this.videoPlayer.currentTime);
|
||||||
|
if (segment.SegmentType === "None") {
|
||||||
|
console.warn("[intro skipper] doSkip() called without an active segment");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.d(`Skipping ${segment.SegmentType}`);
|
||||||
|
const seekedHandler = () => {
|
||||||
|
this.videoPlayer.removeEventListener("seeked", seekedHandler);
|
||||||
|
setTimeout(() => {
|
||||||
|
this.allowEnter = true;
|
||||||
|
}, 700);
|
||||||
|
};
|
||||||
|
this.videoPlayer.addEventListener("seeked", seekedHandler);
|
||||||
|
this.videoPlayer.currentTime = segment.IntroEnd;
|
||||||
|
this.hideSkipButton();
|
||||||
|
},
|
||||||
|
createButton(ref, id, innerHTML, clickHandler) {
|
||||||
|
const button = ref.cloneNode(true);
|
||||||
|
button.setAttribute("data-id", id);
|
||||||
|
button.innerHTML = innerHTML;
|
||||||
|
button.addEventListener("click", clickHandler);
|
||||||
|
return button;
|
||||||
|
},
|
||||||
|
closeSubmenu(fullscreen) {
|
||||||
|
document.querySelector(".dialogContainer").remove();
|
||||||
|
document.querySelector(".dialogBackdrop").remove();
|
||||||
|
document.dispatchEvent(new KeyboardEvent("keydown", { key: "Control" }));
|
||||||
|
if (!fullscreen) return;
|
||||||
|
document.removeEventListener("keydown", this.handleEscapeKey);
|
||||||
|
document.querySelector(".btnVideoOsdSettings").focus();
|
||||||
|
},
|
||||||
|
openSubmenu(ref, menu) {
|
||||||
|
const options = [
|
||||||
|
"Show Button",
|
||||||
|
"Button w/ auto PiP",
|
||||||
|
"Automatically Skip",
|
||||||
|
"Off",
|
||||||
|
];
|
||||||
|
const submenu = menu.cloneNode(true);
|
||||||
|
const scroller = submenu.querySelector(".actionSheetScroller");
|
||||||
|
scroller.innerHTML = "";
|
||||||
|
for (const option of options) {
|
||||||
|
if (option !== "Button w/ auto PiP" || document.pictureInPictureEnabled) {
|
||||||
|
const button = this.createButton(
|
||||||
|
ref,
|
||||||
|
`introskipper-${option.toLowerCase().replace(" ", "-")}`,
|
||||||
|
`<span class="actionsheetMenuItemIcon listItemIcon listItemIcon-transparent material-icons check" aria-hidden="true" style="visibility:${option === this.currentOption ? "visible" : "hidden"};"></span><div class="listItemBody actionsheetListItemBody"><div class="listItemBodyText actionSheetItemText">${option}</div></div>`,
|
||||||
|
() => this.selectOption(option),
|
||||||
|
);
|
||||||
|
scroller.appendChild(button);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const backdrop = document.createElement("div");
|
||||||
|
backdrop.className = "dialogBackdrop dialogBackdropOpened";
|
||||||
|
document.body.append(backdrop, submenu);
|
||||||
|
const actionSheet = submenu.querySelector(".actionSheet");
|
||||||
|
if (actionSheet.classList.contains("actionsheet-not-fullscreen")) {
|
||||||
|
this.adjustPosition(
|
||||||
|
actionSheet,
|
||||||
|
document.querySelector(".btnVideoOsdSettings"),
|
||||||
|
);
|
||||||
|
submenu.addEventListener("click", () => this.closeSubmenu(false));
|
||||||
|
} else {
|
||||||
|
submenu
|
||||||
|
.querySelector(".btnCloseActionSheet")
|
||||||
|
.addEventListener("click", () => this.closeSubmenu(true));
|
||||||
|
scroller.addEventListener("click", () => this.closeSubmenu(true));
|
||||||
|
document.addEventListener("keydown", this.handleEscapeKey);
|
||||||
|
setTimeout(() => scroller.firstElementChild.focus(), 240);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
selectOption(option) {
|
||||||
|
this.currentOption = option;
|
||||||
|
localStorage.setItem("introskipperOption", option);
|
||||||
|
this.d(`Introskipper option selected and saved: ${option}`);
|
||||||
|
},
|
||||||
|
isAutoSkipLocked(config) {
|
||||||
|
const isAutoSkip = config.AutoSkip && config.AutoSkipCredits;
|
||||||
|
const isAutoSkipClient = new Set(config.ClientList.split(",")).has(
|
||||||
|
ApiClient.appName(),
|
||||||
|
);
|
||||||
|
return isAutoSkip || (config.SkipButtonVisible && isAutoSkipClient);
|
||||||
|
},
|
||||||
|
async injectIntroSkipperOptions(actionSheet) {
|
||||||
|
if (!this.skipButton) return;
|
||||||
|
const config = await this.secureFetch("Intros/UserInterfaceConfiguration");
|
||||||
|
if (this.isAutoSkipLocked(config)) {
|
||||||
|
this.d("Auto skip enforced by server");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const statsButton = actionSheet.querySelector('[data-id="stats"]');
|
||||||
|
if (!statsButton) return;
|
||||||
|
const menuItem = this.createButton(
|
||||||
|
statsButton,
|
||||||
|
"introskipperMenu",
|
||||||
|
`<div class="listItemBody actionsheetListItemBody"><div class="listItemBodyText actionSheetItemText">Intro Skipper</div></div><div class="listItemAside actionSheetItemAsideText">${this.currentOption}</div>`,
|
||||||
|
() =>
|
||||||
|
this.openSubmenu(statsButton, actionSheet.closest(".dialogContainer")),
|
||||||
|
);
|
||||||
|
const originalWidth = actionSheet.offsetWidth;
|
||||||
|
statsButton.before(menuItem);
|
||||||
|
if (actionSheet.classList.contains("actionsheet-not-fullscreen"))
|
||||||
|
this.adjustPosition(actionSheet, menuItem, originalWidth);
|
||||||
|
},
|
||||||
|
adjustPosition(element, reference, originalWidth) {
|
||||||
|
if (originalWidth) {
|
||||||
|
const currentTop = Number.parseInt(element.style.top, 10) || 0;
|
||||||
|
element.style.top = `${currentTop - reference.offsetHeight}px`;
|
||||||
|
const newWidth = Math.max(reference.offsetWidth - originalWidth, 0);
|
||||||
|
const originalLeft = Number.parseInt(element.style.left, 10) || 0;
|
||||||
|
element.style.left = `${originalLeft - newWidth / 2}px`;
|
||||||
|
} else {
|
||||||
|
const rect = reference.getBoundingClientRect();
|
||||||
|
element.style.left = `${Math.min(rect.left - (element.offsetWidth - rect.width) / 2, window.innerWidth - element.offsetWidth - 10)}px`;
|
||||||
|
element.style.top = `${rect.top - element.offsetHeight + rect.height}px`;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
injectSkipperFields(metadataFormFields) {
|
||||||
|
const skipperFields = document.createElement("div");
|
||||||
|
skipperFields.className = "detailSection introskipperSection";
|
||||||
|
skipperFields.innerHTML = `
|
||||||
|
<h2>Intro Skipper</h2>
|
||||||
|
<div class="inlineForm">
|
||||||
|
<div class="inputContainer">
|
||||||
|
<label class="inputLabel inputLabelUnfocused" for="introStart">Intro Start</label>
|
||||||
|
<input type="text" id="introStartDisplay" class="emby-input custom-time-input" readonly>
|
||||||
|
<input type="number" id="introStartEdit" class="emby-input custom-time-input" style="display: none;" step="any" min="0">
|
||||||
|
</div>
|
||||||
|
<div class="inputContainer">
|
||||||
|
<label class="inputLabel inputLabelUnfocused" for="introEnd">Intro End</label>
|
||||||
|
<input type="text" id="introEndDisplay" class="emby-input custom-time-input" readonly>
|
||||||
|
<input type="number" id="introEndEdit" class="emby-input custom-time-input" style="display: none;" step="any" min="0">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="inlineForm">
|
||||||
|
<div class="inputContainer">
|
||||||
|
<label class="inputLabel inputLabelUnfocused" for="creditsStart">Credits Start</label>
|
||||||
|
<input type="text" id="creditsStartDisplay" class="emby-input custom-time-input" readonly>
|
||||||
|
<input type="number" id="creditsStartEdit" class="emby-input custom-time-input" style="display: none;" step="any" min="0">
|
||||||
|
</div>
|
||||||
|
<div class="inputContainer">
|
||||||
|
<label class="inputLabel inputLabelUnfocused" for="creditsEnd">Credits End</label>
|
||||||
|
<input type="text" id="creditsEndDisplay" class="emby-input custom-time-input" readonly>
|
||||||
|
<input type="number" id="creditsEndEdit" class="emby-input custom-time-input" style="display: none;" step="any" min="0">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
metadataFormFields
|
||||||
|
.querySelector("#metadataSettingsCollapsible")
|
||||||
|
.insertAdjacentElement("afterend", skipperFields);
|
||||||
|
this.attachSaveListener(metadataFormFields);
|
||||||
|
this.updateSkipperFields(skipperFields);
|
||||||
|
this.setTimeInputs(skipperFields);
|
||||||
|
},
|
||||||
|
updateSkipperFields(skipperFields) {
|
||||||
|
const { Introduction = {}, Credits = {} } = this.skipperData;
|
||||||
|
skipperFields.querySelector("#introStartEdit").value =
|
||||||
|
Introduction.Start || 0;
|
||||||
|
skipperFields.querySelector("#introEndEdit").value = Introduction.End || 0;
|
||||||
|
skipperFields.querySelector("#creditsStartEdit").value = Credits.Start || 0;
|
||||||
|
skipperFields.querySelector("#creditsEndEdit").value = Credits.End || 0;
|
||||||
|
},
|
||||||
|
attachSaveListener(metadataFormFields) {
|
||||||
|
const saveButton = metadataFormFields.querySelector(
|
||||||
|
".formDialogFooter .btnSave",
|
||||||
|
);
|
||||||
|
if (saveButton) {
|
||||||
|
saveButton.addEventListener("click", this.saveSkipperData.bind(this));
|
||||||
|
} else {
|
||||||
|
console.error("Save button not found");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
setTimeInputs(skipperFields) {
|
||||||
|
const inputContainers = skipperFields.querySelectorAll(".inputContainer");
|
||||||
|
for (const container of inputContainers) {
|
||||||
|
const displayInput = container.querySelector('[id$="Display"]');
|
||||||
|
const editInput = container.querySelector('[id$="Edit"]');
|
||||||
|
displayInput.addEventListener("pointerdown", (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
this.switchToEdit(displayInput, editInput);
|
||||||
|
});
|
||||||
|
editInput.addEventListener("blur", () =>
|
||||||
|
this.switchToDisplay(displayInput, editInput),
|
||||||
|
);
|
||||||
|
displayInput.value = this.formatTime(
|
||||||
|
Number.parseFloat(editInput.value) || 0,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
formatTime(totalSeconds) {
|
||||||
|
if (!totalSeconds) return "0 seconds";
|
||||||
|
const totalRoundedSeconds = Math.round(totalSeconds);
|
||||||
|
const hours = Math.floor(totalRoundedSeconds / 3600);
|
||||||
|
const minutes = Math.floor((totalRoundedSeconds % 3600) / 60);
|
||||||
|
const seconds = totalRoundedSeconds % 60;
|
||||||
|
const result = [];
|
||||||
|
if (hours) result.push(`${hours} hour${hours !== 1 ? "s" : ""}`);
|
||||||
|
if (minutes) result.push(`${minutes} minute${minutes !== 1 ? "s" : ""}`);
|
||||||
|
if (seconds || !result.length)
|
||||||
|
result.push(`${seconds} second${seconds !== 1 ? "s" : ""}`);
|
||||||
|
return result.join(" ");
|
||||||
|
},
|
||||||
|
switchToEdit(displayInput, editInput) {
|
||||||
|
displayInput.style.display = "none";
|
||||||
|
editInput.style.display = "";
|
||||||
|
editInput.focus();
|
||||||
|
},
|
||||||
|
switchToDisplay(displayInput, editInput) {
|
||||||
|
editInput.style.display = "none";
|
||||||
|
displayInput.style.display = "";
|
||||||
|
displayInput.value = this.formatTime(
|
||||||
|
Number.parseFloat(editInput.value) || 0,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
async saveSkipperData() {
|
||||||
|
const newTimestamps = {
|
||||||
|
Introduction: {
|
||||||
|
Start: Number.parseFloat(
|
||||||
|
document.getElementById("introStartEdit").value || 0,
|
||||||
|
),
|
||||||
|
End: Number.parseFloat(
|
||||||
|
document.getElementById("introEndEdit").value || 0,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
Credits: {
|
||||||
|
Start: Number.parseFloat(
|
||||||
|
document.getElementById("creditsStartEdit").value || 0,
|
||||||
|
),
|
||||||
|
End: Number.parseFloat(
|
||||||
|
document.getElementById("creditsEndEdit").value || 0,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const { Introduction = {}, Credits = {} } = this.skipperData;
|
||||||
|
if (
|
||||||
|
newTimestamps.Introduction.Start !== (Introduction.Start || 0) ||
|
||||||
|
newTimestamps.Introduction.End !== (Introduction.End || 0) ||
|
||||||
|
newTimestamps.Credits.Start !== (Credits.Start || 0) ||
|
||||||
|
newTimestamps.Credits.End !== (Credits.End || 0)
|
||||||
|
) {
|
||||||
|
const response = await this.secureFetch(
|
||||||
|
`Episode/${this.currentEpisodeId}/Timestamps`,
|
||||||
|
"POST",
|
||||||
|
JSON.stringify(newTimestamps),
|
||||||
|
);
|
||||||
|
this.d(
|
||||||
|
response.ok
|
||||||
|
? "Timestamps updated successfully"
|
||||||
|
: "Failed to update timestamps:",
|
||||||
|
response.status,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
this.d("Timestamps have not changed, skipping update");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
/** Make an authenticated fetch to the Jellyfin server and parse the response body as JSON. */
|
||||||
|
async secureFetch(url, method = "GET", body = null) {
|
||||||
|
const response = await fetch(`${ApiClient.serverAddress()}/${url}`, {
|
||||||
|
method,
|
||||||
|
headers: Object.assign(
|
||||||
|
{ Authorization: `MediaBrowser Token=${ApiClient.accessToken()}` },
|
||||||
|
method === "POST" ? { "Content-Type": "application/json" } : {},
|
||||||
|
),
|
||||||
|
body,
|
||||||
|
});
|
||||||
|
return response.ok
|
||||||
|
? method === "POST"
|
||||||
|
? response
|
||||||
|
: response.json()
|
||||||
|
: response.status === 404
|
||||||
|
? null
|
||||||
|
: console.error(`Error ${response.status} from ${url}`) || null;
|
||||||
|
},
|
||||||
|
/** Handle keydown events. */
|
||||||
|
eventHandler(e) {
|
||||||
|
if (e.key !== "Enter") return;
|
||||||
|
e.stopPropagation();
|
||||||
|
e.preventDefault();
|
||||||
|
this.doSkip();
|
||||||
|
},
|
||||||
|
handleEscapeKey(e) {
|
||||||
|
if (e.key === "Escape" || e.keyCode === 461 || e.keyCode === 10009) {
|
||||||
|
e.stopPropagation();
|
||||||
|
this.closeSubmenu(true);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
introSkipper.setup();
|
@ -1,14 +1,18 @@
|
|||||||
|
// Copyright (C) 2024 Intro-Skipper contributors <intro-skipper.org>
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-only.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Net.Mime;
|
using System.Net.Mime;
|
||||||
using ConfusedPolarBear.Plugin.IntroSkipper.Configuration;
|
using IntroSkipper.Configuration;
|
||||||
using ConfusedPolarBear.Plugin.IntroSkipper.Data;
|
using IntroSkipper.Data;
|
||||||
using MediaBrowser.Common.Api;
|
using MediaBrowser.Common.Api;
|
||||||
|
using MediaBrowser.Controller.Entities.Movies;
|
||||||
using MediaBrowser.Controller.Entities.TV;
|
using MediaBrowser.Controller.Entities.TV;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
namespace ConfusedPolarBear.Plugin.IntroSkipper.Controllers;
|
namespace IntroSkipper.Controllers;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Skip intro controller.
|
/// Skip intro controller.
|
||||||
@ -63,7 +67,7 @@ public class SkipIntroController : ControllerBase
|
|||||||
{
|
{
|
||||||
// only update existing episodes
|
// only update existing episodes
|
||||||
var rawItem = Plugin.Instance!.GetItem(id);
|
var rawItem = Plugin.Instance!.GetItem(id);
|
||||||
if (rawItem == null || rawItem is not Episode episode)
|
if (rawItem == null || rawItem is not Episode and not Movie)
|
||||||
{
|
{
|
||||||
return NotFound();
|
return NotFound();
|
||||||
}
|
}
|
||||||
@ -99,7 +103,7 @@ public class SkipIntroController : ControllerBase
|
|||||||
{
|
{
|
||||||
// only get return content for episodes
|
// only get return content for episodes
|
||||||
var rawItem = Plugin.Instance!.GetItem(id);
|
var rawItem = Plugin.Instance!.GetItem(id);
|
||||||
if (rawItem == null || rawItem is not Episode episode)
|
if (rawItem == null || rawItem is not Episode and not Movie)
|
||||||
{
|
{
|
||||||
return NotFound();
|
return NotFound();
|
||||||
}
|
}
|
||||||
@ -156,18 +160,21 @@ public class SkipIntroController : ControllerBase
|
|||||||
var segment = new Intro(timestamp);
|
var segment = new Intro(timestamp);
|
||||||
|
|
||||||
var config = Plugin.Instance!.Configuration;
|
var config = Plugin.Instance!.Configuration;
|
||||||
segment.IntroEnd -= config.RemainingSecondsOfIntro;
|
segment.IntroEnd = mode == AnalysisMode.Credits
|
||||||
|
? GetAdjustedIntroEnd(id, segment.IntroEnd, config)
|
||||||
|
: segment.IntroEnd - config.RemainingSecondsOfIntro;
|
||||||
|
|
||||||
if (config.PersistSkipButton)
|
if (config.PersistSkipButton)
|
||||||
{
|
{
|
||||||
segment.ShowSkipPromptAt = segment.IntroStart;
|
segment.ShowSkipPromptAt = segment.IntroStart;
|
||||||
segment.HideSkipPromptAt = segment.IntroEnd;
|
segment.HideSkipPromptAt = segment.IntroEnd - 3;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
segment.ShowSkipPromptAt = Math.Max(0, segment.IntroStart - config.ShowPromptAdjustment);
|
segment.ShowSkipPromptAt = Math.Max(0, segment.IntroStart - config.ShowPromptAdjustment);
|
||||||
segment.HideSkipPromptAt = Math.Min(
|
segment.HideSkipPromptAt = Math.Min(
|
||||||
segment.IntroStart + config.HidePromptAdjustment,
|
segment.IntroStart + config.HidePromptAdjustment,
|
||||||
segment.IntroEnd);
|
segment.IntroEnd - 3);
|
||||||
}
|
}
|
||||||
|
|
||||||
return segment;
|
return segment;
|
||||||
@ -178,6 +185,14 @@ public class SkipIntroController : ControllerBase
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static double GetAdjustedIntroEnd(Guid id, double segmentEnd, PluginConfiguration config)
|
||||||
|
{
|
||||||
|
var runTime = TimeSpan.FromTicks(Plugin.Instance!.GetItem(id)?.RunTimeTicks ?? 0).TotalSeconds;
|
||||||
|
return runTime > 0 && runTime < segmentEnd + 1
|
||||||
|
? runTime
|
||||||
|
: segmentEnd - config.RemainingSecondsOfIntro;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Erases all previously discovered introduction timestamps.
|
/// Erases all previously discovered introduction timestamps.
|
||||||
/// </summary>
|
/// </summary>
|
@ -1,10 +1,13 @@
|
|||||||
|
// Copyright (C) 2024 Intro-Skipper contributors <intro-skipper.org>
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-only.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Net.Mime;
|
using System.Net.Mime;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using ConfusedPolarBear.Plugin.IntroSkipper.Data;
|
using IntroSkipper.Data;
|
||||||
using ConfusedPolarBear.Plugin.IntroSkipper.Helper;
|
using IntroSkipper.Helper;
|
||||||
using MediaBrowser.Common;
|
using MediaBrowser.Common;
|
||||||
using MediaBrowser.Common.Api;
|
using MediaBrowser.Common.Api;
|
||||||
using MediaBrowser.Controller.Library;
|
using MediaBrowser.Controller.Library;
|
||||||
@ -12,7 +15,7 @@ using Microsoft.AspNetCore.Authorization;
|
|||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace ConfusedPolarBear.Plugin.IntroSkipper.Controllers;
|
namespace IntroSkipper.Controllers;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Troubleshooting controller.
|
/// Troubleshooting controller.
|
@ -1,15 +1,18 @@
|
|||||||
|
// Copyright (C) 2024 Intro-Skipper contributors <intro-skipper.org>
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-only.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Net.Mime;
|
using System.Net.Mime;
|
||||||
using ConfusedPolarBear.Plugin.IntroSkipper.Data;
|
using IntroSkipper.Data;
|
||||||
using MediaBrowser.Common.Api;
|
using MediaBrowser.Common.Api;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace ConfusedPolarBear.Plugin.IntroSkipper.Controllers;
|
namespace IntroSkipper.Controllers;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Audio fingerprint visualization controller. Allows browsing fingerprints on a per episode basis.
|
/// Audio fingerprint visualization controller. Allows browsing fingerprints on a per episode basis.
|
||||||
@ -47,7 +50,7 @@ public class VisualizationController(ILogger<VisualizationController> logger) :
|
|||||||
var seasonNumber = first.SeasonNumber;
|
var seasonNumber = first.SeasonNumber;
|
||||||
if (!showSeasons.TryGetValue(seriesId, out var showInfo))
|
if (!showSeasons.TryGetValue(seriesId, out var showInfo))
|
||||||
{
|
{
|
||||||
showInfo = new ShowInfos { SeriesName = first.SeriesName, ProductionYear = GetProductionYear(seriesId), LibraryName = GetLibraryName(seriesId), Seasons = [] };
|
showInfo = new ShowInfos { SeriesName = first.SeriesName, ProductionYear = GetProductionYear(seriesId), LibraryName = GetLibraryName(seriesId), IsMovie = first.IsMovie, Seasons = [] };
|
||||||
showSeasons[seriesId] = showInfo;
|
showSeasons[seriesId] = showInfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -65,6 +68,7 @@ public class VisualizationController(ILogger<VisualizationController> logger) :
|
|||||||
SeriesName = kvp.Value.SeriesName,
|
SeriesName = kvp.Value.SeriesName,
|
||||||
ProductionYear = kvp.Value.ProductionYear,
|
ProductionYear = kvp.Value.ProductionYear,
|
||||||
LibraryName = kvp.Value.LibraryName,
|
LibraryName = kvp.Value.LibraryName,
|
||||||
|
IsMovie = kvp.Value.IsMovie,
|
||||||
Seasons = kvp.Value.Seasons
|
Seasons = kvp.Value.Seasons
|
||||||
.OrderBy(s => s.Value)
|
.OrderBy(s => s.Value)
|
||||||
.ToDictionary(s => s.Key, s => s.Value)
|
.ToDictionary(s => s.Key, s => s.Value)
|
@ -1,4 +1,7 @@
|
|||||||
namespace ConfusedPolarBear.Plugin.IntroSkipper.Data;
|
// Copyright (C) 2024 Intro-Skipper contributors <intro-skipper.org>
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-only.
|
||||||
|
|
||||||
|
namespace IntroSkipper.Data;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Type of media file analysis to perform.
|
/// Type of media file analysis to perform.
|
@ -1,4 +1,7 @@
|
|||||||
namespace ConfusedPolarBear.Plugin.IntroSkipper.Data;
|
// Copyright (C) 2024 Intro-Skipper contributors <intro-skipper.org>
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-only.
|
||||||
|
|
||||||
|
namespace IntroSkipper.Data;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// A frame of video that partially (or entirely) consists of black pixels.
|
/// A frame of video that partially (or entirely) consists of black pixels.
|
@ -1,4 +1,7 @@
|
|||||||
namespace ConfusedPolarBear.Plugin.IntroSkipper.Data;
|
// Copyright (C) 2024 Intro-Skipper contributors <intro-skipper.org>
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-only.
|
||||||
|
|
||||||
|
namespace IntroSkipper.Data;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Taken from https://kodi.wiki/view/Edit_decision_list#MPlayer_EDL.
|
/// Taken from https://kodi.wiki/view/Edit_decision_list#MPlayer_EDL.
|
||||||
@ -11,32 +14,22 @@ public enum EdlAction
|
|||||||
None = -1,
|
None = -1,
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Completely remove the intro from playback as if it was never in the original video.
|
/// Completely remove the segment from playback as if it was never in the original video.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
Cut,
|
Cut = 0,
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Mute audio, continue playback.
|
/// Mute audio, continue playback.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
Mute,
|
Mute = 1,
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Inserts a new scene marker.
|
/// Inserts a new scene marker.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
SceneMarker,
|
SceneMarker = 2,
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Automatically skip the intro once during playback.
|
/// Automatically skip once during playback.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
CommercialBreak,
|
CommercialBreak = 3
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Show a skip button.
|
|
||||||
/// </summary>
|
|
||||||
Intro,
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Show a skip button.
|
|
||||||
/// </summary>
|
|
||||||
Credit,
|
|
||||||
}
|
}
|
@ -1,6 +1,9 @@
|
|||||||
|
// Copyright (C) 2024 Intro-Skipper contributors <intro-skipper.org>
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-only.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
|
||||||
namespace ConfusedPolarBear.Plugin.IntroSkipper.Data;
|
namespace IntroSkipper.Data;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Represents the state of an episode regarding analysis and blacklist status.
|
/// Represents the state of an episode regarding analysis and blacklist status.
|
@ -1,6 +1,9 @@
|
|||||||
|
// Copyright (C) 2024 Intro-Skipper contributors <intro-skipper.org>
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-only.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
|
||||||
namespace ConfusedPolarBear.Plugin.IntroSkipper.Data;
|
namespace IntroSkipper.Data;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Episode name and internal ID as returned by the visualization controller.
|
/// Episode name and internal ID as returned by the visualization controller.
|
@ -1,6 +1,9 @@
|
|||||||
|
// Copyright (C) 2024 Intro-Skipper contributors <intro-skipper.org>
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-only.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
|
||||||
namespace ConfusedPolarBear.Plugin.IntroSkipper.Data;
|
namespace IntroSkipper.Data;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Exception raised when an error is encountered analyzing audio.
|
/// Exception raised when an error is encountered analyzing audio.
|
@ -1,7 +1,10 @@
|
|||||||
|
// Copyright (C) 2024 Intro-Skipper contributors <intro-skipper.org>
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-only.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Runtime.Serialization;
|
using System.Runtime.Serialization;
|
||||||
|
|
||||||
namespace ConfusedPolarBear.Plugin.IntroSkipper.Data;
|
namespace IntroSkipper.Data;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Represents an item to ignore.
|
/// Represents an item to ignore.
|
@ -1,7 +1,10 @@
|
|||||||
|
// Copyright (C) 2024 Intro-Skipper contributors <intro-skipper.org>
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-only.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Runtime.Serialization;
|
using System.Runtime.Serialization;
|
||||||
|
|
||||||
namespace ConfusedPolarBear.Plugin.IntroSkipper.Data;
|
namespace IntroSkipper.Data;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Result of fingerprinting and analyzing two episodes in a season.
|
/// Result of fingerprinting and analyzing two episodes in a season.
|
@ -1,6 +1,9 @@
|
|||||||
|
// Copyright (C) 2024 Intro-Skipper contributors <intro-skipper.org>
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-only.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
|
||||||
namespace ConfusedPolarBear.Plugin.IntroSkipper.Data;
|
namespace IntroSkipper.Data;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Support bundle warning.
|
/// Support bundle warning.
|
@ -1,6 +1,9 @@
|
|||||||
|
// Copyright (C) 2024 Intro-Skipper contributors <intro-skipper.org>
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-only.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
|
||||||
namespace ConfusedPolarBear.Plugin.IntroSkipper.Data;
|
namespace IntroSkipper.Data;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Episode queued for analysis.
|
/// Episode queued for analysis.
|
||||||
@ -47,6 +50,11 @@ public class QueuedEpisode
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public bool IsAnime { get; set; }
|
public bool IsAnime { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets a value indicating whether an item is a movie.
|
||||||
|
/// </summary>
|
||||||
|
public bool IsMovie { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the timestamp (in seconds) to stop searching for an introduction at.
|
/// Gets or sets the timestamp (in seconds) to stop searching for an introduction at.
|
||||||
/// </summary>
|
/// </summary>
|
@ -1,9 +1,12 @@
|
|||||||
|
// Copyright (C) 2024 Intro-Skipper contributors <intro-skipper.org>
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-only.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.Runtime.Serialization;
|
using System.Runtime.Serialization;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
namespace ConfusedPolarBear.Plugin.IntroSkipper.Data;
|
namespace IntroSkipper.Data;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Result of fingerprinting and analyzing two episodes in a season.
|
/// Result of fingerprinting and analyzing two episodes in a season.
|
@ -1,7 +1,10 @@
|
|||||||
|
// Copyright (C) 2024 Intro-Skipper contributors <intro-skipper.org>
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-only.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
|
||||||
namespace ConfusedPolarBear.Plugin.IntroSkipper.Controllers
|
namespace IntroSkipper.Data
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Contains information about a show.
|
/// Contains information about a show.
|
||||||
@ -23,6 +26,11 @@ namespace ConfusedPolarBear.Plugin.IntroSkipper.Controllers
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public required string LibraryName { get; set; }
|
public required string LibraryName { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets a value indicating whether its a movie.
|
||||||
|
/// </summary>
|
||||||
|
public required bool IsMovie { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the Seasons of the show.
|
/// Gets the Seasons of the show.
|
||||||
/// </summary>
|
/// </summary>
|
@ -1,6 +1,9 @@
|
|||||||
|
// Copyright (C) 2024 Intro-Skipper contributors <intro-skipper.org>
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-only.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
|
||||||
namespace ConfusedPolarBear.Plugin.IntroSkipper.Data;
|
namespace IntroSkipper.Data;
|
||||||
|
|
||||||
#pragma warning disable CA1036 // Override methods on comparable types
|
#pragma warning disable CA1036 // Override methods on comparable types
|
||||||
|
|
@ -1,7 +1,10 @@
|
|||||||
|
// Copyright (C) 2024 Intro-Skipper contributors <intro-skipper.org>
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-only.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
|
||||||
namespace ConfusedPolarBear.Plugin.IntroSkipper.Data;
|
namespace IntroSkipper.Data;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Time range helpers.
|
/// Time range helpers.
|
@ -1,4 +1,7 @@
|
|||||||
namespace ConfusedPolarBear.Plugin.IntroSkipper.Data
|
// Copyright (C) 2024 Intro-Skipper contributors <intro-skipper.org>
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-only.
|
||||||
|
|
||||||
|
namespace IntroSkipper.Data
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Result of fingerprinting and analyzing two episodes in a season.
|
/// Result of fingerprinting and analyzing two episodes in a season.
|
@ -1,4 +1,7 @@
|
|||||||
namespace ConfusedPolarBear.Plugin.IntroSkipper.Data;
|
// Copyright (C) 2024 Intro-Skipper contributors <intro-skipper.org>
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-only.
|
||||||
|
|
||||||
|
namespace IntroSkipper.Data;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Warning manager.
|
/// Warning manager.
|
@ -1,3 +1,6 @@
|
|||||||
|
// Copyright (C) 2024 Intro-Skipper contributors <intro-skipper.org>
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-only.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
@ -6,10 +9,10 @@ using System.Globalization;
|
|||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
using ConfusedPolarBear.Plugin.IntroSkipper.Data;
|
using IntroSkipper.Data;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace ConfusedPolarBear.Plugin.IntroSkipper;
|
namespace IntroSkipper;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Wrapper for libchromaprint and the silencedetect filter.
|
/// Wrapper for libchromaprint and the silencedetect filter.
|
@ -1,4 +1,7 @@
|
|||||||
namespace ConfusedPolarBear.Plugin.IntroSkipper.Helper
|
// Copyright (C) 2024 Intro-Skipper contributors <intro-skipper.org>
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-only.
|
||||||
|
|
||||||
|
namespace IntroSkipper.Helper
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the commit used to build the plugin.
|
/// Gets the commit used to build the plugin.
|
@ -1,12 +1,15 @@
|
|||||||
|
// Copyright (C) 2024 Intro-Skipper contributors <intro-skipper.org>
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-only.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Runtime.Serialization;
|
using System.Runtime.Serialization;
|
||||||
using System.Xml;
|
using System.Xml;
|
||||||
using ConfusedPolarBear.Plugin.IntroSkipper.Data;
|
using IntroSkipper.Data;
|
||||||
|
|
||||||
namespace ConfusedPolarBear.Plugin.IntroSkipper
|
namespace IntroSkipper
|
||||||
{
|
{
|
||||||
internal sealed class XmlSerializationHelper
|
internal sealed class XmlSerializationHelper
|
||||||
{
|
{
|
@ -1,9 +1,9 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net8.0</TargetFramework>
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
<RootNamespace>ConfusedPolarBear.Plugin.IntroSkipper</RootNamespace>
|
<RootNamespace>IntroSkipper</RootNamespace>
|
||||||
<AssemblyVersion>1.0.0.5</AssemblyVersion>
|
<AssemblyVersion>1.10.9.2</AssemblyVersion>
|
||||||
<FileVersion>1.0.0.5</FileVersion>
|
<FileVersion>1.10.9.2</FileVersion>
|
||||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
@ -11,8 +11,8 @@
|
|||||||
<CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet>
|
<CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Jellyfin.Controller" Version="10.*-*" />
|
<PackageReference Include="Jellyfin.Controller" Version="10.9.*" />
|
||||||
<PackageReference Include="Jellyfin.Model" Version="10.*-*" />
|
<PackageReference Include="Jellyfin.Model" Version="10.9.*" />
|
||||||
<PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
|
<PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
|
||||||
<PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
|
<PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
|
||||||
<PackageReference Include="StyleCop.Analyzers.Unstable" Version="1.2.0.556" PrivateAssets="All" />
|
<PackageReference Include="StyleCop.Analyzers.Unstable" Version="1.2.0.556" PrivateAssets="All" />
|
@ -1,10 +1,13 @@
|
|||||||
|
// Copyright (C) 2024 Intro-Skipper contributors <intro-skipper.org>
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-only.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using ConfusedPolarBear.Plugin.IntroSkipper.Data;
|
using IntroSkipper.Data;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace ConfusedPolarBear.Plugin.IntroSkipper;
|
namespace IntroSkipper;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Update EDL files associated with a list of episodes.
|
/// Update EDL files associated with a list of episodes.
|
||||||
@ -97,14 +100,7 @@ public static class EdlManager
|
|||||||
edlContent += Environment.NewLine;
|
edlContent += Environment.NewLine;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (action == EdlAction.Intro)
|
edlContent += credit?.ToEdl(action);
|
||||||
{
|
|
||||||
edlContent += credit?.ToEdl(EdlAction.Credit);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
edlContent += credit?.ToEdl(action);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
File.WriteAllText(edlPath, edlContent);
|
File.WriteAllText(edlPath, edlContent);
|
@ -1,16 +1,20 @@
|
|||||||
|
// Copyright (C) 2024 Intro-Skipper contributors <intro-skipper.org>
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-only.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using ConfusedPolarBear.Plugin.IntroSkipper.Data;
|
using IntroSkipper.Data;
|
||||||
using Jellyfin.Data.Enums;
|
using Jellyfin.Data.Enums;
|
||||||
using Jellyfin.Extensions;
|
using Jellyfin.Extensions;
|
||||||
using MediaBrowser.Controller.Entities;
|
using MediaBrowser.Controller.Entities;
|
||||||
|
using MediaBrowser.Controller.Entities.Movies;
|
||||||
using MediaBrowser.Controller.Entities.TV;
|
using MediaBrowser.Controller.Entities.TV;
|
||||||
using MediaBrowser.Controller.Library;
|
using MediaBrowser.Controller.Library;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace ConfusedPolarBear.Plugin.IntroSkipper;
|
namespace IntroSkipper;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Manages enqueuing library items for analysis.
|
/// Manages enqueuing library items for analysis.
|
||||||
@ -28,6 +32,7 @@ public class QueueManager(ILogger<QueueManager> logger, ILibraryManager libraryM
|
|||||||
private double _analysisPercent;
|
private double _analysisPercent;
|
||||||
private List<string> _selectedLibraries = [];
|
private List<string> _selectedLibraries = [];
|
||||||
private bool _selectAllLibraries;
|
private bool _selectAllLibraries;
|
||||||
|
private bool _analyzeMovies;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets all media items on the server.
|
/// Gets all media items on the server.
|
||||||
@ -90,6 +95,8 @@ public class QueueManager(ILogger<QueueManager> logger, ILibraryManager libraryM
|
|||||||
|
|
||||||
_selectAllLibraries = config.SelectAllLibraries;
|
_selectAllLibraries = config.SelectAllLibraries;
|
||||||
|
|
||||||
|
_analyzeMovies = config.AnalyzeMovies;
|
||||||
|
|
||||||
if (!_selectAllLibraries)
|
if (!_selectAllLibraries)
|
||||||
{
|
{
|
||||||
// Get the list of library names which have been selected for analysis, ignoring whitespace and empty entries.
|
// Get the list of library names which have been selected for analysis, ignoring whitespace and empty entries.
|
||||||
@ -123,7 +130,7 @@ public class QueueManager(ILogger<QueueManager> logger, ILibraryManager libraryM
|
|||||||
// Order by series name, season, and then episode number so that status updates are logged in order
|
// Order by series name, season, and then episode number so that status updates are logged in order
|
||||||
ParentId = id,
|
ParentId = id,
|
||||||
OrderBy = [(ItemSortBy.SeriesSortName, SortOrder.Ascending), (ItemSortBy.ParentIndexNumber, SortOrder.Descending), (ItemSortBy.IndexNumber, SortOrder.Ascending),],
|
OrderBy = [(ItemSortBy.SeriesSortName, SortOrder.Ascending), (ItemSortBy.ParentIndexNumber, SortOrder.Descending), (ItemSortBy.IndexNumber, SortOrder.Ascending),],
|
||||||
IncludeItemTypes = [BaseItemKind.Episode],
|
IncludeItemTypes = [BaseItemKind.Episode, BaseItemKind.Movie],
|
||||||
Recursive = true,
|
Recursive = true,
|
||||||
IsVirtualItem = false
|
IsVirtualItem = false
|
||||||
};
|
};
|
||||||
@ -141,13 +148,18 @@ public class QueueManager(ILogger<QueueManager> logger, ILibraryManager libraryM
|
|||||||
|
|
||||||
foreach (var item in items)
|
foreach (var item in items)
|
||||||
{
|
{
|
||||||
if (item is not Episode episode)
|
if (item is Episode episode)
|
||||||
{
|
{
|
||||||
_logger.LogDebug("Item {Name} is not an episode", item.Name);
|
QueueEpisode(episode);
|
||||||
continue;
|
}
|
||||||
|
else if (_analyzeMovies && item is Movie movie)
|
||||||
|
{
|
||||||
|
QueueMovie(movie);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_logger.LogDebug("Item {Name} is not an episode or movie", item.Name);
|
||||||
}
|
}
|
||||||
|
|
||||||
QueueEpisode(episode);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_logger.LogDebug("Queued {Count} episodes", items.Count);
|
_logger.LogDebug("Queued {Count} episodes", items.Count);
|
||||||
@ -197,8 +209,11 @@ public class QueueManager(ILogger<QueueManager> logger, ILibraryManager libraryM
|
|||||||
duration >= 5 * 60 ? duration * _analysisPercent : duration,
|
duration >= 5 * 60 ? duration * _analysisPercent : duration,
|
||||||
60 * pluginInstance.Configuration.AnalysisLengthLimit);
|
60 * pluginInstance.Configuration.AnalysisLengthLimit);
|
||||||
|
|
||||||
|
var maxCreditsDuration = Math.Min(
|
||||||
|
duration >= 5 * 60 ? duration * _analysisPercent : duration,
|
||||||
|
60 * pluginInstance.Configuration.MaximumCreditsDuration);
|
||||||
|
|
||||||
// Queue the episode for analysis
|
// Queue the episode for analysis
|
||||||
var maxCreditsDuration = pluginInstance.Configuration.MaximumCreditsDuration;
|
|
||||||
seasonEpisodes.Add(new QueuedEpisode
|
seasonEpisodes.Add(new QueuedEpisode
|
||||||
{
|
{
|
||||||
SeriesName = episode.SeriesName,
|
SeriesName = episode.SeriesName,
|
||||||
@ -216,6 +231,34 @@ public class QueueManager(ILogger<QueueManager> logger, ILibraryManager libraryM
|
|||||||
pluginInstance.TotalQueued++;
|
pluginInstance.TotalQueued++;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void QueueMovie(Movie movie)
|
||||||
|
{
|
||||||
|
var pluginInstance = Plugin.Instance ?? throw new InvalidOperationException("Plugin instance was null");
|
||||||
|
if (string.IsNullOrEmpty(movie.Path))
|
||||||
|
{
|
||||||
|
_logger.LogWarning(
|
||||||
|
"Not queuing movie \"{Name}\" ({Id}) as no path was provided by Jellyfin",
|
||||||
|
movie.Name,
|
||||||
|
movie.Id);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Allocate a new list for each Movie
|
||||||
|
_queuedEpisodes.TryAdd(movie.Id, []);
|
||||||
|
var duration = TimeSpan.FromTicks(movie.RunTimeTicks ?? 0).TotalSeconds;
|
||||||
|
_queuedEpisodes[movie.Id].Add(new QueuedEpisode
|
||||||
|
{
|
||||||
|
SeriesName = movie.Name,
|
||||||
|
SeriesId = movie.Id,
|
||||||
|
EpisodeId = movie.Id,
|
||||||
|
Name = movie.Name,
|
||||||
|
Path = movie.Path,
|
||||||
|
Duration = Convert.ToInt32(duration),
|
||||||
|
IsMovie = true
|
||||||
|
});
|
||||||
|
pluginInstance.TotalQueued++;
|
||||||
|
}
|
||||||
|
|
||||||
private Guid GetSeasonId(Episode episode)
|
private Guid GetSeasonId(Episode episode)
|
||||||
{
|
{
|
||||||
if (episode.ParentIndexNumber == 0 && episode.AiredSeasonNumber != 0) // In-season special
|
if (episode.ParentIndexNumber == 0 && episode.AiredSeasonNumber != 0) // In-season special
|
@ -1,11 +1,16 @@
|
|||||||
|
// Copyright (C) 2024 Intro-Skipper contributors <intro-skipper.org>
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-only.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
using ConfusedPolarBear.Plugin.IntroSkipper.Configuration;
|
using System.Xml;
|
||||||
using ConfusedPolarBear.Plugin.IntroSkipper.Data;
|
using System.Xml.Serialization;
|
||||||
|
using IntroSkipper.Configuration;
|
||||||
|
using IntroSkipper.Data;
|
||||||
using MediaBrowser.Common;
|
using MediaBrowser.Common;
|
||||||
using MediaBrowser.Common.Configuration;
|
using MediaBrowser.Common.Configuration;
|
||||||
using MediaBrowser.Common.Plugins;
|
using MediaBrowser.Common.Plugins;
|
||||||
@ -16,9 +21,10 @@ using MediaBrowser.Controller.Persistence;
|
|||||||
using MediaBrowser.Model.Entities;
|
using MediaBrowser.Model.Entities;
|
||||||
using MediaBrowser.Model.Plugins;
|
using MediaBrowser.Model.Plugins;
|
||||||
using MediaBrowser.Model.Serialization;
|
using MediaBrowser.Model.Serialization;
|
||||||
|
using MediaBrowser.Model.Updates;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace ConfusedPolarBear.Plugin.IntroSkipper;
|
namespace IntroSkipper;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Intro skipper plugin. Uses audio analysis to find common sequences of audio shared between episodes.
|
/// Intro skipper plugin. Uses audio analysis to find common sequences of audio shared between episodes.
|
||||||
@ -85,6 +91,40 @@ public class Plugin : BasePlugin<PluginConfiguration>, IHasWebPages
|
|||||||
XmlSerializationHelper.MigrateXML(_introPath);
|
XmlSerializationHelper.MigrateXML(_introPath);
|
||||||
XmlSerializationHelper.MigrateXML(_creditsPath);
|
XmlSerializationHelper.MigrateXML(_creditsPath);
|
||||||
|
|
||||||
|
var oldConfigFile = Path.Join(applicationPaths.PluginConfigurationsPath, "ConfusedPolarBear.Plugin.IntroSkipper.xml");
|
||||||
|
|
||||||
|
if (File.Exists(oldConfigFile))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
XmlSerializer serializer = new XmlSerializer(typeof(PluginConfiguration));
|
||||||
|
using (FileStream fileStream = new FileStream(oldConfigFile, FileMode.Open))
|
||||||
|
{
|
||||||
|
var settings = new XmlReaderSettings
|
||||||
|
{
|
||||||
|
DtdProcessing = DtdProcessing.Prohibit, // Disable DTD processing
|
||||||
|
XmlResolver = null // Disable the XmlResolver
|
||||||
|
};
|
||||||
|
|
||||||
|
using (var reader = XmlReader.Create(fileStream, settings))
|
||||||
|
{
|
||||||
|
if (serializer.Deserialize(reader) is PluginConfiguration oldConfig)
|
||||||
|
{
|
||||||
|
Instance.UpdateConfiguration(oldConfig);
|
||||||
|
File.Delete(oldConfigFile);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
// Handle exceptions, such as file not found, deserialization errors, etc.
|
||||||
|
_logger.LogWarning("Something stupid happened: {Exception}", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MigrateRepoUrl(serverConfiguration);
|
||||||
|
|
||||||
// TODO: remove when https://github.com/jellyfin/jellyfin-meta/discussions/30 is complete
|
// TODO: remove when https://github.com/jellyfin/jellyfin-meta/discussions/30 is complete
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@ -405,6 +445,53 @@ public class Plugin : BasePlugin<PluginConfiguration>, IHasWebPages
|
|||||||
SaveTimestamps(AnalysisMode.Credits);
|
SaveTimestamps(AnalysisMode.Credits);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void MigrateRepoUrl(IServerConfigurationManager serverConfiguration)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
List<string> oldRepos =
|
||||||
|
[
|
||||||
|
"https://raw.githubusercontent.com/intro-skipper/intro-skipper/master/manifest.json",
|
||||||
|
"https://raw.githubusercontent.com/jumoog/intro-skipper/master/manifest.json",
|
||||||
|
"https://manifest.intro-skipper.workers.dev/manifest.json"
|
||||||
|
];
|
||||||
|
// Access the current server configuration
|
||||||
|
var config = serverConfiguration.Configuration;
|
||||||
|
|
||||||
|
// Get the list of current plugin repositories
|
||||||
|
var pluginRepositories = config.PluginRepositories.ToList();
|
||||||
|
|
||||||
|
// check if old plugins exits
|
||||||
|
if (pluginRepositories.Exists(repo => repo.Url != null && oldRepos.Contains(repo.Url)))
|
||||||
|
{
|
||||||
|
// remove all old plugins
|
||||||
|
pluginRepositories.RemoveAll(repo => repo.Url != null && oldRepos.Contains(repo.Url));
|
||||||
|
|
||||||
|
// Add repository only if it does not exit and the OverideManifestUrl Option is activated
|
||||||
|
if (!pluginRepositories.Exists(repo => repo.Url == "https://manifest.intro-skipper.org/manifest.json") && Instance!.Configuration.OverrideManifestUrl)
|
||||||
|
{
|
||||||
|
// Add the new repository to the list
|
||||||
|
pluginRepositories.Add(new RepositoryInfo
|
||||||
|
{
|
||||||
|
Name = "intro skipper (automatically migrated by plugin)",
|
||||||
|
Url = "https://manifest.intro-skipper.org/manifest.json",
|
||||||
|
Enabled = true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the configuration with the new repository list
|
||||||
|
config.PluginRepositories = [.. pluginRepositories];
|
||||||
|
|
||||||
|
// Save the updated configuration
|
||||||
|
serverConfiguration.SaveConfiguration();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error occurred while migrating repo URL");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Inject the skip button script into the web interface.
|
/// Inject the skip button script into the web interface.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -414,8 +501,7 @@ public class Plugin : BasePlugin<PluginConfiguration>, IHasWebPages
|
|||||||
string searchPattern = "dashboard-dashboard.*.chunk.js";
|
string searchPattern = "dashboard-dashboard.*.chunk.js";
|
||||||
string[] filePaths = Directory.GetFiles(webPath, searchPattern, SearchOption.TopDirectoryOnly);
|
string[] filePaths = Directory.GetFiles(webPath, searchPattern, SearchOption.TopDirectoryOnly);
|
||||||
string pattern = @"buildVersion""\)\.innerText=""(?<buildVersion>\d+\.\d+\.\d+)"",.*?webVersion""\)\.innerText=""(?<webVersion>\d+\.\d+\.\d+)";
|
string pattern = @"buildVersion""\)\.innerText=""(?<buildVersion>\d+\.\d+\.\d+)"",.*?webVersion""\)\.innerText=""(?<webVersion>\d+\.\d+\.\d+)";
|
||||||
string buildVersionString = "unknow";
|
string webVersionString = "unknown";
|
||||||
string webVersionString = "unknow";
|
|
||||||
// Create a Regex object
|
// Create a Regex object
|
||||||
Regex regex = new Regex(pattern);
|
Regex regex = new Regex(pattern);
|
||||||
|
|
||||||
@ -428,14 +514,13 @@ public class Plugin : BasePlugin<PluginConfiguration>, IHasWebPages
|
|||||||
// search for buildVersion and webVersion
|
// search for buildVersion and webVersion
|
||||||
if (match.Success)
|
if (match.Success)
|
||||||
{
|
{
|
||||||
buildVersionString = match.Groups["buildVersion"].Value;
|
|
||||||
webVersionString = match.Groups["webVersion"].Value;
|
webVersionString = match.Groups["webVersion"].Value;
|
||||||
_logger.LogInformation("Found jellyfin-web <{WebVersion}>", webVersionString);
|
_logger.LogInformation("Found jellyfin-web <{WebVersion}>", webVersionString);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (webVersionString != "unknow")
|
if (webVersionString != "unknown")
|
||||||
{
|
{
|
||||||
// append Revision
|
// append Revision
|
||||||
webVersionString += ".0";
|
webVersionString += ".0";
|
@ -1,8 +1,11 @@
|
|||||||
|
// Copyright (C) 2024 Intro-Skipper contributors <intro-skipper.org>
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-only.
|
||||||
|
|
||||||
using MediaBrowser.Controller;
|
using MediaBrowser.Controller;
|
||||||
using MediaBrowser.Controller.Plugins;
|
using MediaBrowser.Controller.Plugins;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
||||||
namespace ConfusedPolarBear.Plugin.IntroSkipper
|
namespace IntroSkipper
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Register Intro Skipper services.
|
/// Register Intro Skipper services.
|
@ -1,15 +1,18 @@
|
|||||||
|
// Copyright (C) 2024 Intro-Skipper contributors <intro-skipper.org>
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-only.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Collections.ObjectModel;
|
using System.Collections.ObjectModel;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using ConfusedPolarBear.Plugin.IntroSkipper.Analyzers;
|
using IntroSkipper.Analyzers;
|
||||||
using ConfusedPolarBear.Plugin.IntroSkipper.Data;
|
using IntroSkipper.Data;
|
||||||
using MediaBrowser.Controller.Library;
|
using MediaBrowser.Controller.Library;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace ConfusedPolarBear.Plugin.IntroSkipper.ScheduledTasks;
|
namespace IntroSkipper.ScheduledTasks;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Common code shared by all media item analyzer tasks.
|
/// Common code shared by all media item analyzer tasks.
|
||||||
@ -59,12 +62,11 @@ public class BaseItemAnalyzerTask
|
|||||||
CancellationToken cancellationToken,
|
CancellationToken cancellationToken,
|
||||||
IReadOnlyCollection<Guid>? seasonsToAnalyze = null)
|
IReadOnlyCollection<Guid>? seasonsToAnalyze = null)
|
||||||
{
|
{
|
||||||
var ffmpegValid = FFmpegWrapper.CheckFFmpegVersion();
|
|
||||||
// Assert that ffmpeg with chromaprint is installed
|
// Assert that ffmpeg with chromaprint is installed
|
||||||
if (Plugin.Instance!.Configuration.WithChromaprint && !ffmpegValid)
|
if (Plugin.Instance!.Configuration.WithChromaprint && !FFmpegWrapper.CheckFFmpegVersion())
|
||||||
{
|
{
|
||||||
throw new FingerprintException(
|
throw new FingerprintException(
|
||||||
"Analysis terminated! Chromaprint is not enabled in the current ffmpeg. If Jellyfin is running natively, install jellyfin-ffmpeg5. If Jellyfin is running in a container, upgrade to version 10.8.0 or newer.");
|
"Analysis terminated! Chromaprint is not enabled in the current ffmpeg. If Jellyfin is running natively, install jellyfin-ffmpeg6. If Jellyfin is running in a container, upgrade to version 10.9.0 or newer.");
|
||||||
}
|
}
|
||||||
|
|
||||||
var queueManager = new QueueManager(
|
var queueManager = new QueueManager(
|
||||||
@ -122,11 +124,8 @@ public class BaseItemAnalyzerTask
|
|||||||
|
|
||||||
Interlocked.Add(ref totalProcessed, episodes.Count * _analysisModes.Count); // Update total Processed directly
|
Interlocked.Add(ref totalProcessed, episodes.Count * _analysisModes.Count); // Update total Processed directly
|
||||||
progress.Report(totalProcessed * 100 / totalQueued);
|
progress.Report(totalProcessed * 100 / totalQueued);
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
else if (_analysisModes.Count != requiredModes.Count)
|
||||||
if (_analysisModes.Count != requiredModes.Count)
|
|
||||||
{
|
{
|
||||||
Interlocked.Add(ref totalProcessed, episodes.Count);
|
Interlocked.Add(ref totalProcessed, episodes.Count);
|
||||||
progress.Report(totalProcessed * 100 / totalQueued); // Partial analysis some modes have already been analyzed
|
progress.Report(totalProcessed * 100 / totalQueued); // Partial analysis some modes have already been analyzed
|
||||||
@ -188,7 +187,7 @@ public class BaseItemAnalyzerTask
|
|||||||
|
|
||||||
// Only analyze specials (season 0) if the user has opted in.
|
// Only analyze specials (season 0) if the user has opted in.
|
||||||
var first = items[0];
|
var first = items[0];
|
||||||
if (first.SeasonNumber == 0 && !Plugin.Instance!.Configuration.AnalyzeSeasonZero)
|
if (!first.IsMovie && first.SeasonNumber == 0 && !Plugin.Instance!.Configuration.AnalyzeSeasonZero)
|
||||||
{
|
{
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
@ -211,7 +210,7 @@ public class BaseItemAnalyzerTask
|
|||||||
new ChapterAnalyzer(_loggerFactory.CreateLogger<ChapterAnalyzer>())
|
new ChapterAnalyzer(_loggerFactory.CreateLogger<ChapterAnalyzer>())
|
||||||
};
|
};
|
||||||
|
|
||||||
if (first.IsAnime && Plugin.Instance!.Configuration.WithChromaprint)
|
if (first.IsAnime && Plugin.Instance!.Configuration.WithChromaprint && !first.IsMovie)
|
||||||
{
|
{
|
||||||
analyzers.Add(new ChromaprintAnalyzer(_loggerFactory.CreateLogger<ChromaprintAnalyzer>()));
|
analyzers.Add(new ChromaprintAnalyzer(_loggerFactory.CreateLogger<ChromaprintAnalyzer>()));
|
||||||
}
|
}
|
||||||
@ -221,7 +220,7 @@ public class BaseItemAnalyzerTask
|
|||||||
analyzers.Add(new BlackFrameAnalyzer(_loggerFactory.CreateLogger<BlackFrameAnalyzer>()));
|
analyzers.Add(new BlackFrameAnalyzer(_loggerFactory.CreateLogger<BlackFrameAnalyzer>()));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!first.IsAnime && Plugin.Instance!.Configuration.WithChromaprint)
|
if (!first.IsAnime && Plugin.Instance!.Configuration.WithChromaprint && !first.IsMovie)
|
||||||
{
|
{
|
||||||
analyzers.Add(new ChromaprintAnalyzer(_loggerFactory.CreateLogger<ChromaprintAnalyzer>()));
|
analyzers.Add(new ChromaprintAnalyzer(_loggerFactory.CreateLogger<ChromaprintAnalyzer>()));
|
||||||
}
|
}
|
@ -1,3 +1,6 @@
|
|||||||
|
// Copyright (C) 2024 Intro-Skipper contributors <intro-skipper.org>
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-only.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
@ -8,7 +11,7 @@ using MediaBrowser.Controller.Library;
|
|||||||
using MediaBrowser.Model.Tasks;
|
using MediaBrowser.Model.Tasks;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace ConfusedPolarBear.Plugin.IntroSkipper.ScheduledTasks;
|
namespace IntroSkipper.ScheduledTasks;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Analyze all television episodes for introduction sequences.
|
/// Analyze all television episodes for introduction sequences.
|
@ -1,14 +1,16 @@
|
|||||||
|
// Copyright (C) 2024 Intro-Skipper contributors <intro-skipper.org>
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-only.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Collections.ObjectModel;
|
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using ConfusedPolarBear.Plugin.IntroSkipper.Data;
|
using IntroSkipper.Data;
|
||||||
using MediaBrowser.Controller.Library;
|
using MediaBrowser.Controller.Library;
|
||||||
using MediaBrowser.Model.Tasks;
|
using MediaBrowser.Model.Tasks;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace ConfusedPolarBear.Plugin.IntroSkipper.ScheduledTasks;
|
namespace IntroSkipper.ScheduledTasks;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Analyze all television episodes for credits.
|
/// Analyze all television episodes for credits.
|
@ -1,14 +1,16 @@
|
|||||||
|
// Copyright (C) 2024 Intro-Skipper contributors <intro-skipper.org>
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-only.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Collections.ObjectModel;
|
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using ConfusedPolarBear.Plugin.IntroSkipper.Data;
|
using IntroSkipper.Data;
|
||||||
using MediaBrowser.Controller.Library;
|
using MediaBrowser.Controller.Library;
|
||||||
using MediaBrowser.Model.Tasks;
|
using MediaBrowser.Model.Tasks;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace ConfusedPolarBear.Plugin.IntroSkipper.ScheduledTasks;
|
namespace IntroSkipper.ScheduledTasks;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Analyze all television episodes for introduction sequences.
|
/// Analyze all television episodes for introduction sequences.
|
@ -1,14 +1,16 @@
|
|||||||
|
// Copyright (C) 2024 Intro-Skipper contributors <intro-skipper.org>
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-only.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Collections.ObjectModel;
|
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using ConfusedPolarBear.Plugin.IntroSkipper.Data;
|
using IntroSkipper.Data;
|
||||||
using MediaBrowser.Controller.Library;
|
using MediaBrowser.Controller.Library;
|
||||||
using MediaBrowser.Model.Tasks;
|
using MediaBrowser.Model.Tasks;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace ConfusedPolarBear.Plugin.IntroSkipper.ScheduledTasks;
|
namespace IntroSkipper.ScheduledTasks;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Analyze all television episodes for introduction sequences.
|
/// Analyze all television episodes for introduction sequences.
|
@ -1,7 +1,10 @@
|
|||||||
|
// Copyright (C) 2024 Intro-Skipper contributors <intro-skipper.org>
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-only.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
|
|
||||||
namespace ConfusedPolarBear.Plugin.IntroSkipper.ScheduledTasks;
|
namespace IntroSkipper.ScheduledTasks;
|
||||||
|
|
||||||
internal sealed class ScheduledTaskSemaphore : IDisposable
|
internal sealed class ScheduledTaskSemaphore : IDisposable
|
||||||
{
|
{
|
@ -1,10 +1,13 @@
|
|||||||
|
// Copyright (C) 2024 Intro-Skipper contributors <intro-skipper.org>
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-only.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using System.Timers;
|
using System.Timers;
|
||||||
using ConfusedPolarBear.Plugin.IntroSkipper.Configuration;
|
using IntroSkipper.Configuration;
|
||||||
using MediaBrowser.Common.Extensions;
|
using MediaBrowser.Common.Extensions;
|
||||||
using MediaBrowser.Controller.Library;
|
using MediaBrowser.Controller.Library;
|
||||||
using MediaBrowser.Controller.Session;
|
using MediaBrowser.Controller.Session;
|
||||||
@ -15,7 +18,7 @@ using Microsoft.Extensions.Hosting;
|
|||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Timer = System.Timers.Timer;
|
using Timer = System.Timers.Timer;
|
||||||
|
|
||||||
namespace ConfusedPolarBear.Plugin.IntroSkipper;
|
namespace IntroSkipper;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Automatically skip past introduction sequences.
|
/// Automatically skip past introduction sequences.
|
@ -1,10 +1,13 @@
|
|||||||
|
// Copyright (C) 2024 Intro-Skipper contributors <intro-skipper.org>
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-only.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using System.Timers;
|
using System.Timers;
|
||||||
using ConfusedPolarBear.Plugin.IntroSkipper.Configuration;
|
using IntroSkipper.Configuration;
|
||||||
using MediaBrowser.Common.Extensions;
|
using MediaBrowser.Common.Extensions;
|
||||||
using MediaBrowser.Controller.Library;
|
using MediaBrowser.Controller.Library;
|
||||||
using MediaBrowser.Controller.Session;
|
using MediaBrowser.Controller.Session;
|
||||||
@ -15,7 +18,7 @@ using Microsoft.Extensions.Hosting;
|
|||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Timer = System.Timers.Timer;
|
using Timer = System.Timers.Timer;
|
||||||
|
|
||||||
namespace ConfusedPolarBear.Plugin.IntroSkipper;
|
namespace IntroSkipper;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Automatically skip past credit sequences.
|
/// Automatically skip past credit sequences.
|
@ -1,10 +1,13 @@
|
|||||||
|
// Copyright (C) 2024 Intro-Skipper contributors <intro-skipper.org>
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-only.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using ConfusedPolarBear.Plugin.IntroSkipper.Configuration;
|
using IntroSkipper.Configuration;
|
||||||
using ConfusedPolarBear.Plugin.IntroSkipper.Data;
|
using IntroSkipper.Data;
|
||||||
using ConfusedPolarBear.Plugin.IntroSkipper.ScheduledTasks;
|
using IntroSkipper.ScheduledTasks;
|
||||||
using MediaBrowser.Controller.Entities.TV;
|
using MediaBrowser.Controller.Entities.TV;
|
||||||
using MediaBrowser.Controller.Library;
|
using MediaBrowser.Controller.Library;
|
||||||
using MediaBrowser.Model.Entities;
|
using MediaBrowser.Model.Entities;
|
||||||
@ -13,7 +16,7 @@ using MediaBrowser.Model.Tasks;
|
|||||||
using Microsoft.Extensions.Hosting;
|
using Microsoft.Extensions.Hosting;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace ConfusedPolarBear.Plugin.IntroSkipper;
|
namespace IntroSkipper;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Server entrypoint.
|
/// Server entrypoint.
|
30
README.md
30
README.md
@ -9,10 +9,14 @@
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
[![CodeQL](https://github.com/intro-skipper/intro-skipper/actions/workflows/codeql.yml/badge.svg)](https://github.com/intro-skipper/intro-skipper/actions/workflows/codeql.yml)
|
[![CodeQL](https://github.com/intro-skipper/intro-skipper/actions/workflows/codeql.yml/badge.svg)](https://github.com/intro-skipper/intro-skipper/actions/workflows/codeql.yml)
|
||||||
|
|
||||||
https://raw.githubusercontent.com/intro-skipper/intro-skipper/master/manifest.json
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
## Manifest URL (All Jellyfin Versions)
|
||||||
|
|
||||||
|
```
|
||||||
|
https://manifest.intro-skipper.org/manifest.json
|
||||||
|
```
|
||||||
|
|
||||||
## System requirements
|
## System requirements
|
||||||
|
|
||||||
* Jellyfin 10.9.11 (or newer)
|
* Jellyfin 10.9.11 (or newer)
|
||||||
@ -26,29 +30,15 @@
|
|||||||
|
|
||||||
* SyncPlay is not (yet) compatible with any method of skipping due to the nature of how the clients are synced.
|
* SyncPlay is not (yet) compatible with any method of skipping due to the nature of how the clients are synced.
|
||||||
|
|
||||||
## Jellyfin 10.8 (previous version)
|
## [Detection parameters](https://github.com/intro-skipper/intro-skipper/wiki#detection-parameters)
|
||||||
👉👉👉 [Jellyfin 10.8 Instructions](https://github.com/intro-skipper/intro-skipper/blob/10.8/README.md)
|
|
||||||
|
|
||||||
## Detection parameters
|
## [Detection types](https://github.com/intro-skipper/intro-skipper/wiki#detection-types)
|
||||||
|
|
||||||
Show introductions will be detected if they are:
|
|
||||||
|
|
||||||
* Located within the first 25% of an episode or the first 10 minutes, whichever is smaller
|
|
||||||
* Between 15 seconds and 2 minutes long
|
|
||||||
|
|
||||||
Ending credits will be detected if they are shorter than 4 minutes.
|
|
||||||
|
|
||||||
These parameters can be configured by opening the plugin settings
|
|
||||||
|
|
||||||
## [Installation](https://github.com/intro-skipper/intro-skipper/wiki/Installation)
|
## [Installation](https://github.com/intro-skipper/intro-skipper/wiki/Installation)
|
||||||
- #### [Install the plugin](https://github.com/intro-skipper/intro-skipper/wiki/Installation#step-1-install-the-plugin)
|
|
||||||
- #### [Verify the plugin](https://github.com/intro-skipper/intro-skipper/wiki/Installation#step-2-verify-the-plugin)
|
## [Jellyfin Skip Options](https://github.com/intro-skipper/intro-skipper/wiki/Jellyfin-Skip-Options)
|
||||||
- #### [Custom FFMPEG (MacOS)](https://github.com/intro-skipper/intro-skipper/wiki/Custom-FFMPEG-(MacOS))
|
|
||||||
|
|
||||||
## [Troubleshooting](https://github.com/intro-skipper/intro-skipper/wiki/Troubleshooting)
|
## [Troubleshooting](https://github.com/intro-skipper/intro-skipper/wiki/Troubleshooting)
|
||||||
- #### [Scheduled tasks fail instantly](https://github.com/intro-skipper/intro-skipper/wiki/Troubleshooting#scheduled-tasks-fail-instantly)
|
|
||||||
- #### [Skip button is not visible](https://github.com/intro-skipper/intro-skipper/wiki/Troubleshooting#skip-button-is-not-visible)
|
|
||||||
- #### [Autoskip is not working](https://github.com/intro-skipper/intro-skipper/wiki/Troubleshooting#autoskip-is-not-working)
|
|
||||||
|
|
||||||
## [API Documentation](https://github.com/intro-skipper/intro-skipper/blob/master/docs/api.md)
|
## [API Documentation](https://github.com/intro-skipper/intro-skipper/blob/master/docs/api.md)
|
||||||
|
|
||||||
|
1
VERSION.txt
Normal file
1
VERSION.txt
Normal file
@ -0,0 +1 @@
|
|||||||
|
10.9
|
@ -21,7 +21,7 @@ GOTO UserInput
|
|||||||
|
|
||||||
:FoundFile
|
:FoundFile
|
||||||
echo "%NewestFile%"
|
echo "%NewestFile%"
|
||||||
xcopy /y ConfusedPolarBear.Plugin.IntroSkipper.dll "%NewestFile%"
|
xcopy /y IntroSkipper.dll "%NewestFile%"
|
||||||
|
|
||||||
:UserInput
|
:UserInput
|
||||||
@pause
|
@pause
|
||||||
|
@ -6,8 +6,8 @@ if [ "$(uname)" == "Darwin" ]; then
|
|||||||
echo "Intro Skipper plugin not found!"
|
echo "Intro Skipper plugin not found!"
|
||||||
exit
|
exit
|
||||||
fi
|
fi
|
||||||
cp -f ConfusedPolarBear.Plugin.IntroSkipper*.dll \
|
cp -f IntroSkipper*.dll \
|
||||||
"$plugin/ConfusedPolarBear.Plugin.IntroSkipper.dll"
|
"$plugin/IntroSkipper.dll"
|
||||||
else
|
else
|
||||||
echo "Jellyfin plugin directory not found!"
|
echo "Jellyfin plugin directory not found!"
|
||||||
fi
|
fi
|
||||||
@ -19,8 +19,8 @@ elif [ "$(expr substr $(uname -s) 1 5)" == "Linux" ]; then
|
|||||||
echo "Intro Skipper plugin not found!"
|
echo "Intro Skipper plugin not found!"
|
||||||
exit
|
exit
|
||||||
fi
|
fi
|
||||||
cp -f ConfusedPolarBear.Plugin.IntroSkipper*.dll \
|
cp -f IntroSkipper*.dll \
|
||||||
"$plugin/ConfusedPolarBear.Plugin.IntroSkipper.dll"
|
"$plugin/IntroSkipper.dll"
|
||||||
else
|
else
|
||||||
echo "Jellyfin plugin directory not found!"
|
echo "Jellyfin plugin directory not found!"
|
||||||
fi
|
fi
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user