Compare commits
265 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b5cd185b57 | |||
| 945f1ed6dd | |||
| 9433aff28a | |||
| 754d4abd45 | |||
| 5f4afcc28e | |||
| 42c02b33eb | |||
| 13553ef1c6 | |||
| ac2e4dac9c | |||
| 0bbb3c36cd | |||
| db0d9b115c | |||
| bc343dd0c8 | |||
| 9ed4869ece | |||
| c2eebbe1a9 | |||
| e198b0fb92 | |||
| f6f6bbf978 | |||
| 75c2fce017 | |||
| 3c08e3c70c | |||
| d5c706b833 | |||
| ad4fb92577 | |||
| 8aec7f804d | |||
| 4c3de5146b | |||
| 1ce6587078 | |||
| 7942e89341 | |||
| 764dc8f326 | |||
| af8a02d289 | |||
| 9d745b0bc5 | |||
| a8e0896284 | |||
| 53d9a17c2a | |||
| 0c8bc243c6 | |||
| 03ea6b9e5c | |||
| 6c9a824546 | |||
| 5dc99e2fcf | |||
| 478537bf0a | |||
| 184c53ba00 | |||
| 70f063d33b | |||
| 60753cb828 | |||
| cba12adaa4 | |||
| d94fd1bac3 | |||
| dfdfac6be7 | |||
| 806efa9dac | |||
| 4ddb5cedac | |||
| cfa78f2a89 | |||
| 54cb513ae8 | |||
| bee5538d19 | |||
| 40194f9f26 | |||
| 67c6fd0422 | |||
| 7aa6af30fa | |||
| 7e496643f8 | |||
| 6e7dbd859c | |||
| 13c4144aab | |||
| 98f73185d1 | |||
| f7cbff910d | |||
| 2d5327d31b | |||
| a2e29dee3b | |||
| cf87c6e829 | |||
| d7f4f5cbcc | |||
| 6e8b9ca744 | |||
| b0ca3a49a5 | |||
| 4358072001 | |||
| bdd99441d1 | |||
| 38ed5dd9e0 | |||
| cfd94260f1 | |||
| a28b4ccd7c | |||
| ff55b9f517 | |||
| 93a2de5d46 | |||
| 5b62e4c001 | |||
| 50c0e1cef2 | |||
| ca0a9215fe | |||
| 6679867f74 | |||
| 6926a44e7c | |||
| d386f79dfd | |||
| 46dd4fcf5b | |||
| ab783238c3 | |||
| ff99517d12 | |||
| 532438d110 | |||
| 8e4194f56b | |||
| bff699fc3f | |||
| f3a9050311 | |||
| 3cb340c61a | |||
| 4317fdbf63 | |||
| a9c6611dcf | |||
| e28a3ea37b | |||
| 86dd1769ec | |||
| 4711a6a742 | |||
| a783b3c739 | |||
| 3e95ca26aa | |||
| a58139eca0 | |||
| 5ab5224328 | |||
| d36d677179 | |||
| 784592b362 | |||
| 2f90d6a03e | |||
| af375987ee | |||
| ee683d5737 | |||
| 583e94b35a | |||
| 80a1b6050d | |||
| a4942e78b6 | |||
| 48070f311e | |||
| 96a72ab909 | |||
| 9c6aa56db0 | |||
| 413f45046d | |||
| 5ad6b2a381 | |||
| 795047a50e | |||
| 323a208de2 | |||
| 55d320c456 | |||
| d5f2f0ae8e | |||
| c3bdfc26ea | |||
| c6f7f79385 | |||
| 53ede13f54 | |||
| eeda7dd91e | |||
| 867fbbae5b | |||
| 76ac750de2 | |||
| ddbbb4ba25 | |||
| 3fca2d1e09 | |||
| 4f2f6c0429 | |||
| ebb2cdfcc6 | |||
| c058a35abe | |||
| 11eb456c56 | |||
| 4db1ce755a | |||
| a424721d93 | |||
| 16598bb86e | |||
| 98472b5aa6 | |||
| ab96b227a1 | |||
| 5ff96113c8 | |||
| 6697f2b4cc | |||
| a45ef87f84 | |||
| 157facdceb | |||
| be8481c795 | |||
| bd0a8371a5 | |||
| a75c474647 | |||
| 1dd7019676 | |||
| 355576c283 | |||
| 9091cdbe5b | |||
| a162ebb25d | |||
| 36eec27d9f | |||
| 2fee654efe | |||
| ce8aea629b | |||
| a8f821625b | |||
| 6cb2d89b95 | |||
| 080f3856b7 | |||
| b436ae13a1 | |||
| dce122840c | |||
| 9129bbda71 | |||
| 698237a77a | |||
| 0bb2cb1108 | |||
| 286d43c840 | |||
| 622079c887 | |||
| c9cc4f150e | |||
| 52342b3dc3 | |||
| 9fff31c451 | |||
| 233595baac | |||
| a968ae838c | |||
| 44d00af16f | |||
| 8edb8c9202 | |||
| 625b6b6958 | |||
| 6e76761994 | |||
| b07ffb2a83 | |||
| b3ef252edb | |||
| 4ffdbaf7f8 | |||
| fc221e4407 | |||
| b65f6332cb | |||
| 1a3fa6e823 | |||
| 962794c62c | |||
| 9a40ba93e8 | |||
| fd4a9a0130 | |||
| cc28016984 | |||
| 9179ea8602 | |||
| 6675f7f762 | |||
| 8b49afad25 | |||
| e355c304a2 | |||
| 2bdee20beb | |||
| a085a5a4bf | |||
| 3f0267ced0 | |||
| 70123f597a | |||
| b4d6ea2646 | |||
| e0ef8b1e86 | |||
| 362582dddf | |||
| 1c49427761 | |||
| 49effb3d3f | |||
| 66e6a49cc7 | |||
| 3dca19c3e5 | |||
| 55ab16a01a | |||
| 08c8df0292 | |||
| 1a8f3b1aa7 | |||
| 59bfaa077e | |||
| 9635a49c06 | |||
| 00eeff552c | |||
| 4294e7d138 | |||
| b2c7ba6509 | |||
| 81895dadab | |||
| 59710b2aba | |||
| f34db507b0 | |||
| a2432beaa2 | |||
| c908ea5b9c | |||
| e61beea66b | |||
| 61b3849406 | |||
| cb9f79b3fa | |||
| cf7cdb1ab3 | |||
| f202240fe6 | |||
| b9d0aeeb05 | |||
| 46ac3cfdfa | |||
| a00060ca9c | |||
| c3f5248c80 | |||
| 9921dd2308 | |||
| 1fcbbba49f | |||
| 4557ab6efd | |||
| 9febfef12a | |||
| 56e8c29a94 | |||
| 7ea9d20e86 | |||
| bff3b2b59a | |||
| c4b1d1d954 | |||
| 8842a5fdf9 | |||
| bc5fd7fa20 | |||
| 13fd9de010 | |||
| 6803016a17 | |||
| 00047b6753 | |||
| 107aa0ed6c | |||
| c93d705216 | |||
| 8cbdeee818 | |||
| f70527d218 | |||
| 9f2c7e4510 | |||
| a5904a5310 | |||
| a7c6d575ed | |||
| 5b14b3ac87 | |||
| 8b6eae7a6c | |||
| 3c33025cfa | |||
| da56d00689 | |||
| aab2de6189 | |||
| 9b0c4b5d34 | |||
| d6cf76f948 | |||
| 03c77fcbc5 | |||
| 1f33b908be | |||
| ebb74cf7f5 | |||
| 59550760eb | |||
| 3b9d235aee | |||
| 857ca0f8e0 | |||
| 80f1091af3 | |||
| d1ddd4d32d | |||
| 924f85260e | |||
| ca56fcac34 | |||
| 37c7056204 | |||
| 8d89c82660 | |||
| 06efee0051 | |||
| 83b92f5dfe | |||
| 8e77326c20 | |||
| 37a49ddabd | |||
| 6e24a61623 | |||
| ddd61fa39c | |||
| 3ba7e2ec52 | |||
| 40d5c90c4c | |||
| 634b3f7dc7 | |||
| 057020f0ba | |||
| 02846a7d00 | |||
| c9fee17716 | |||
| 195acd2f1b | |||
| 14032d3ed3 | |||
| 604ed24f7c | |||
| eefdd2a635 | |||
| 30715d5c26 | |||
| c12340583e | |||
| a9b335f797 | |||
| 6d5c0b86b2 | |||
| 347b2131db | |||
| 5a7b43f6e9 | |||
| 6f6cb3dab6 | |||
| 80b236d643 |
@@ -0,0 +1,152 @@
|
||||
name: TubeArchivistMetadata
|
||||
|
||||
permissions:
|
||||
code: read
|
||||
releases: write
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
tags:
|
||||
- v*
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
name: Build
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup .NET
|
||||
uses: actions/setup-dotnet@v4
|
||||
with:
|
||||
dotnet-version: 9.0.x
|
||||
|
||||
- name: Restore
|
||||
run: dotnet restore TubeArchivistMetadata.sln
|
||||
|
||||
- name: Build
|
||||
run: dotnet build TubeArchivistMetadata.sln -c Release --no-restore
|
||||
|
||||
- name: Package plugin
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
plugin_version="$(awk -F'\"' '/^version:/ { print $2 }' build.yaml)"
|
||||
artifact="artifacts/tubearchivistmetadata_${plugin_version}.zip"
|
||||
checksum="$(./scripts/package-release.sh)"
|
||||
|
||||
echo "PLUGIN_VERSION=$plugin_version" >> "$GITHUB_ENV"
|
||||
echo "PLUGIN_ARTIFACT=$artifact" >> "$GITHUB_ENV"
|
||||
echo "PLUGIN_CHECKSUM=$checksum" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Validate release metadata
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
tag_version="${GITHUB_REF_NAME#v}"
|
||||
if [ "$PLUGIN_VERSION" != "$tag_version" ] && [ "$PLUGIN_VERSION" != "${tag_version}.0" ]; then
|
||||
echo "Tag $GITHUB_REF_NAME does not match build.yaml version $PLUGIN_VERSION" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
grep -F "\"version\": \"$PLUGIN_VERSION\"" manifest.json >/dev/null
|
||||
grep -F "\"checksum\": \"$PLUGIN_CHECKSUM\"" manifest.json >/dev/null
|
||||
grep -F "/releases/download/$GITHUB_REF_NAME/$(basename "$PLUGIN_ARTIFACT")" manifest.json >/dev/null
|
||||
|
||||
release:
|
||||
runs-on: ubuntu-latest
|
||||
name: Release
|
||||
needs: build
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup .NET
|
||||
uses: actions/setup-dotnet@v4
|
||||
with:
|
||||
dotnet-version: 9.0.x
|
||||
|
||||
- name: Setup Go for tea
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: 1.26.x
|
||||
|
||||
- name: Install tea
|
||||
run: go install code.gitea.io/tea@v0.14.0
|
||||
|
||||
- name: Restore
|
||||
run: dotnet restore TubeArchivistMetadata.sln
|
||||
|
||||
- name: Build
|
||||
run: dotnet build TubeArchivistMetadata.sln -c Release --no-restore
|
||||
|
||||
- name: Package plugin
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
plugin_version="$(awk -F'\"' '/^version:/ { print $2 }' build.yaml)"
|
||||
artifact="artifacts/tubearchivistmetadata_${plugin_version}.zip"
|
||||
checksum="$(./scripts/package-release.sh)"
|
||||
|
||||
echo "PLUGIN_VERSION=$plugin_version" >> "$GITHUB_ENV"
|
||||
echo "PLUGIN_ARTIFACT=$artifact" >> "$GITHUB_ENV"
|
||||
echo "PLUGIN_CHECKSUM=$checksum" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Validate release metadata
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
tag_version="${GITHUB_REF_NAME#v}"
|
||||
if [ "$PLUGIN_VERSION" != "$tag_version" ] && [ "$PLUGIN_VERSION" != "${tag_version}.0" ]; then
|
||||
echo "Tag $GITHUB_REF_NAME does not match build.yaml version $PLUGIN_VERSION" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
grep -F "\"version\": \"$PLUGIN_VERSION\"" manifest.json >/dev/null
|
||||
grep -F "\"checksum\": \"$PLUGIN_CHECKSUM\"" manifest.json >/dev/null
|
||||
grep -F "/releases/download/$GITHUB_REF_NAME/$(basename "$PLUGIN_ARTIFACT")" manifest.json >/dev/null
|
||||
|
||||
- name: Generate release notes
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
changelog="$(awk '/changelog:/ {flag=1; next} /^[^ ]/ {flag=0} flag' build.yaml | awk '{$1=$1};1')"
|
||||
{
|
||||
echo "Changelog:"
|
||||
echo "* $changelog"
|
||||
} > release-changelog.md
|
||||
|
||||
- name: Create Gitea release
|
||||
env:
|
||||
GITEA_SERVER_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
if [ -z "${GITEA_SERVER_TOKEN:-}" ]; then
|
||||
echo "Missing built-in GITEA_TOKEN" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
tea logins add \
|
||||
--name gitea \
|
||||
--url "$GITHUB_SERVER_URL" \
|
||||
--token "$GITEA_SERVER_TOKEN" \
|
||||
--no-version-check
|
||||
|
||||
tea releases create "$GITHUB_REF_NAME" \
|
||||
--repo "$GITHUB_REPOSITORY" \
|
||||
--target "$GITHUB_SHA" \
|
||||
--title "TubeArchivist Metadata $PLUGIN_VERSION" \
|
||||
--note-file release-changelog.md \
|
||||
--asset "$PLUGIN_ARTIFACT" \
|
||||
--login gitea
|
||||
@@ -0,0 +1,59 @@
|
||||
name: Bug Report
|
||||
description: File a bug report
|
||||
title: "[Bug]: "
|
||||
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Thanks for taking the time to help improve this project! Please read the [how to open an issue](https://github.com/tubearchivist/tubearchivist/blob/master/CONTRIBUTING.md#how-to-open-an-issue) guide carefully before continuing.
|
||||
|
||||
- type: checkboxes
|
||||
id: latest
|
||||
attributes:
|
||||
label: "I've read the documentation"
|
||||
options:
|
||||
- label: I'm running the latest version of tubearchivist-jf-plugin.
|
||||
required: true
|
||||
- label: I'm running the latest version of TubeArchivist.
|
||||
required: true
|
||||
- label: I have read the [how to open an issue](https://github.com/tubearchivist/tubearchivist/blob/master/CONTRIBUTING.md#how-to-open-an-issue) guide, particularly the [bug report](https://github.com/tubearchivist/tubearchivist/blob/master/CONTRIBUTING.md#bug-report) section.
|
||||
required: true
|
||||
- label: I have searched for both closed and open already existing issues about the problem I am reporting.
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: os
|
||||
attributes:
|
||||
label: Operating System
|
||||
description: On what operating system are you running Tube Archivist on?
|
||||
placeholder: eg. Linux, Windows, ...
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: what-happened
|
||||
attributes:
|
||||
label: Your Bug Report
|
||||
value: "## Describe the bug\n\n## Steps To Reproduce\n\n## Expected behavior"
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: logs
|
||||
attributes:
|
||||
label: Relevant Jellyfin log output
|
||||
description: Please copy and paste any relevant Jellyfin logs. This will be automatically formatted into code, so no need for backticks.
|
||||
render: shell
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Anything else?
|
||||
description: |
|
||||
Links? References? Anything that will give us more context about the issue you are encountering!
|
||||
|
||||
Tip: You can attach images or log files by clicking this area to highlight it and then dragging files in.
|
||||
validations:
|
||||
required: false
|
||||
@@ -0,0 +1,34 @@
|
||||
name: Feature Request
|
||||
description: I have an idea for a great addition to this project
|
||||
title: "[Feature Request]: "
|
||||
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Thanks for taking the time to help improve this project! This project is *very* selective with accepting new feature requests. Please read the [how to open an issue](https://github.com/tubearchivist/tubearchivist/blob/master/CONTRIBUTING.md#how-to-open-an-issue) guide carefully before continuing.
|
||||
|
||||
- type: checkboxes
|
||||
id: already
|
||||
attributes:
|
||||
label: "I've read the documentation"
|
||||
options:
|
||||
- label: I have read the [how to open an issue](https://github.com/tubearchivist/tubearchivist/blob/master/CONTRIBUTING.md#how-to-open-an-issue) guide, particularly the [feature request](https://github.com/tubearchivist/tubearchivist/blob/master/CONTRIBUTING.md#feature-request) section.
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: description
|
||||
attributes:
|
||||
label: Your Feature Request
|
||||
value: "## Is your feature request related to a problem? Please describe.\n\n## Describe the solution you'd like\n\n## Additional context"
|
||||
placeholder: Tell us what you see!
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: checkboxes
|
||||
id: help
|
||||
attributes:
|
||||
label: Your help is needed!
|
||||
description: This project is ambitious as it is, please contribute.
|
||||
options:
|
||||
- label: Yes I will work on this in the next few days or weeks.
|
||||
@@ -3,3 +3,4 @@ obj/
|
||||
.vs/
|
||||
.idea/
|
||||
artifacts
|
||||
nuget.config
|
||||
|
||||
Vendored
+6
-5
@@ -1,15 +1,16 @@
|
||||
{
|
||||
// jellyfinDir : The directory of the cloned jellyfin server project
|
||||
// This needs to be built once before it can be used
|
||||
"jellyfinDir" : "${workspaceFolder}/../jellyfin/Jellyfin.Server",
|
||||
"jellyfinDir": "${workspaceFolder}/../jellyfin/Jellyfin.Server",
|
||||
// jellyfinWebDir : The directory of the cloned jellyfin-web project
|
||||
// This needs to be built once before it can be used
|
||||
"jellyfinWebDir" : "${workspaceFolder}/../jellyfin-web",
|
||||
"jellyfinWebDir": "${workspaceFolder}/../jellyfin-web",
|
||||
// jellyfinDataDir : the root data directory for a running jellyfin instance
|
||||
// This is where jellyfin stores its configs, plugins, metadata etc
|
||||
// This is platform specific by default, but on Windows defaults to
|
||||
// ${env:LOCALAPPDATA}/jellyfin
|
||||
"jellyfinDataDir" : "${env:LOCALAPPDATA}/jellyfin",
|
||||
"jellyfinDataDir": "${env:LOCALAPPDATA}/jellyfin",
|
||||
// The name of the plugin
|
||||
"pluginName" : "Jellyfin.Plugin.Template",
|
||||
}
|
||||
"pluginName": "Jellyfin.Plugin.Template",
|
||||
"git.ignoreLimitWarning": true,
|
||||
}
|
||||
@@ -1,7 +1,8 @@
|
||||
<Project>
|
||||
<PropertyGroup>
|
||||
<Version>1.2.1.0</Version>
|
||||
<AssemblyVersion>1.2.1.0</AssemblyVersion>
|
||||
<FileVersion>1.2.1.0</FileVersion>
|
||||
<Version>1.4.5.0</Version>
|
||||
<AssemblyVersion>1.4.5.0</AssemblyVersion>
|
||||
<FileVersion>1.4.5.0</FileVersion>
|
||||
<IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
namespace Jellyfin.Plugin.TubeArchivistMetadata.Configuration;
|
||||
|
||||
/// <summary>
|
||||
/// The NumberingScheme.
|
||||
/// </summary>
|
||||
public enum NumberingScheme
|
||||
{
|
||||
/// <summary>
|
||||
/// Default (no numbering).
|
||||
/// </summary>
|
||||
Default,
|
||||
|
||||
/// <summary>
|
||||
/// YYYYMMDD (e.g. 20250804 for August 4th, 2025).
|
||||
/// </summary>
|
||||
YYYYMMDD,
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Data;
|
||||
using System.Linq;
|
||||
using Jellyfin.Plugin.TubeArchivistMetadata.Utilities;
|
||||
using MediaBrowser.Model.Plugins;
|
||||
using Microsoft.Extensions.Logging;
|
||||
@@ -13,6 +15,8 @@ namespace Jellyfin.Plugin.TubeArchivistMetadata.Configuration
|
||||
{
|
||||
private ILogger _logger;
|
||||
private string _tubeArchivistUrl;
|
||||
private string _tubeArchivistApiKey;
|
||||
private HashSet<string> _jfUsernamesTo;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="PluginConfiguration"/> class.
|
||||
@@ -30,8 +34,19 @@ namespace Jellyfin.Plugin.TubeArchivistMetadata.Configuration
|
||||
|
||||
CollectionTitle = string.Empty;
|
||||
_tubeArchivistUrl = string.Empty;
|
||||
TubeArchivistApiKey = string.Empty;
|
||||
_tubeArchivistApiKey = string.Empty;
|
||||
MaxDescriptionLength = 500;
|
||||
JFTAProgressSync = false;
|
||||
JFUsernameFrom = string.Empty;
|
||||
TAJFProgressSync = false;
|
||||
JFTAPlaylistsSync = false;
|
||||
JFTAPlaylistsDelete = false;
|
||||
TAJFPlaylistsSync = false;
|
||||
TAJFPlaylistsDelete = false;
|
||||
_jfUsernamesTo = new HashSet<string>();
|
||||
TAJFProgressTaskInterval = 60;
|
||||
JFTAPlaylistsSyncTaskInterval = 60;
|
||||
TAJFPlaylistsSyncTaskInterval = 60;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -60,17 +75,129 @@ namespace Jellyfin.Plugin.TubeArchivistMetadata.Configuration
|
||||
_logger.LogInformation("{Message}", "Given TubeArchivist URL contains no schema. Adding http://...");
|
||||
_tubeArchivistUrl = Utils.SanitizeUrl("http://" + value);
|
||||
}
|
||||
|
||||
Plugin.Instance?.LogTAApiConnectionStatus();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets TubeArchivist API key.
|
||||
/// </summary>
|
||||
public string TubeArchivistApiKey { get; set; }
|
||||
public string TubeArchivistApiKey
|
||||
{
|
||||
get
|
||||
{
|
||||
return _tubeArchivistApiKey;
|
||||
}
|
||||
|
||||
set
|
||||
{
|
||||
_tubeArchivistApiKey = value;
|
||||
Plugin.Instance?.LogTAApiConnectionStatus();
|
||||
Plugin.Instance?.UpdateAuthorizationHeader(value);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets maximum series and episodes overviews length.
|
||||
/// </summary>
|
||||
public int MaxDescriptionLength { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether to enable TA->JF playback progress synchronization.
|
||||
/// </summary>
|
||||
public bool TAJFProgressSync { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether to enable JF->TA playback progress synchronization.
|
||||
/// </summary>
|
||||
public bool JFTAProgressSync { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether to enable JF->TA playlists synchronization.
|
||||
/// </summary>
|
||||
public bool JFTAPlaylistsSync { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether to delete playlists from TA when not found on JF.
|
||||
/// </summary>
|
||||
public bool JFTAPlaylistsDelete { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether to enable TA->JF playlists synchronization.
|
||||
/// </summary>
|
||||
public bool TAJFPlaylistsSync { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether to delete playlists from JF when not found on TA.
|
||||
/// </summary>
|
||||
public bool TAJFPlaylistsDelete { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the playback progress owner Jellyfin username to synchronize data to TubeArchivist.
|
||||
/// </summary>
|
||||
public string JFUsernameFrom { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the playback progress owners Jellyfin usernames to synchronize data from TubeArchivist.
|
||||
/// </summary>
|
||||
public string JFUsernamesTo
|
||||
{
|
||||
get
|
||||
{
|
||||
_logger.LogInformation("JFUsernamesTo configured: {Message}", string.Join(", ", _jfUsernamesTo));
|
||||
return string.Join(", ", _jfUsernamesTo);
|
||||
}
|
||||
|
||||
set
|
||||
{
|
||||
// Clear existing usernames
|
||||
_jfUsernamesTo.Clear();
|
||||
|
||||
// Split by comma, then trim each part to remove leading/trailing spaces
|
||||
foreach (var username in value.Split(','))
|
||||
{
|
||||
var trimmedUsername = username.Trim();
|
||||
if (!string.IsNullOrEmpty(trimmedUsername))
|
||||
{
|
||||
_jfUsernamesTo.Add(trimmedUsername);
|
||||
}
|
||||
}
|
||||
|
||||
_logger.LogInformation("Set JFUsernamesTo to: {Message}", string.Join(", ", _jfUsernamesTo));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the interval in seconds at which the TubeArchivist to Jellyfin playback progress synchronization task should run.
|
||||
/// It requires Jellyfin server restart to take effect.
|
||||
/// </summary>
|
||||
public int TAJFProgressTaskInterval { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the interval in seconds at which the Jellyfin to TubeArchivist playlists synchronization task should run.
|
||||
/// It requires Jellyfin server restart to take effect.
|
||||
/// </summary>
|
||||
public int JFTAPlaylistsSyncTaskInterval { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the interval in seconds at which the TubeArchivist to Jellyfin playlists synchronization task should run.
|
||||
/// It requires Jellyfin server restart to take effect.
|
||||
/// </summary>
|
||||
public int TAJFPlaylistsSyncTaskInterval { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the preferred numbering scheme for episodes (index number) in Jellyfin.
|
||||
/// </summary>
|
||||
public NumberingScheme EpisodeNumberingScheme { get; set; } = NumberingScheme.Default;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the playback progress owners Jellyfin usernames to synchronize data from TubeArchivist.
|
||||
/// </summary>
|
||||
/// <returns>An array of usernames.</returns>
|
||||
public HashSet<string> GetJFUsernamesToArray()
|
||||
{
|
||||
return _jfUsernamesTo;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Template</title>
|
||||
<title>TubeArchivistMetadata</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
@@ -12,25 +12,138 @@
|
||||
<div data-role="content">
|
||||
<div class="content-primary">
|
||||
<form id="TemplateConfigForm">
|
||||
<div>
|
||||
<h1>TubeArchivistMetadata</h1>
|
||||
</div>
|
||||
<div class="inputContainer">
|
||||
<label class="inputLabel inputLabelUnfocused" for="CollectionTitle">Collection Title</label>
|
||||
<input id="CollectionTitle" name="CollectionTitle" type="text" is="emby-input" placeholder="eg. YouTube" />
|
||||
<input id="CollectionTitle" name="CollectionTitle" type="text" is="emby-input"
|
||||
placeholder="eg. YouTube" />
|
||||
<div class="fieldDescription">TubeArchivist collection display name</div>
|
||||
</div>
|
||||
<div class="inputContainer">
|
||||
<label class="inputLabel inputLabelUnfocused" for="TubeArchivistUrl">TubeArchivist URL</label>
|
||||
<input id="TubeArchivistUrl" name="TubeArchivistUrl" type="text" is="emby-input" placeholder="http://..." />
|
||||
<input id="TubeArchivistUrl" name="TubeArchivistUrl" type="text" is="emby-input"
|
||||
placeholder="http://..." />
|
||||
<div class="fieldDescription">TubeArchivist URL</div>
|
||||
</div>
|
||||
<div class="inputContainer">
|
||||
<label class="inputLabel inputLabelUnfocused" for="TubeArchivistApiKey">TubeArchivist API key</label>
|
||||
<label class="inputLabel inputLabelUnfocused" for="TubeArchivistApiKey">TubeArchivist API
|
||||
key</label>
|
||||
<input id="TubeArchivistApiKey" name="TubeArchivistApiKey" type="text" is="emby-input" />
|
||||
<div class="fieldDescription">TubeArchivist API key</div>
|
||||
</div>
|
||||
<div class="inputContainer">
|
||||
<label class="inputLabel inputLabelUnfocused" for="MaxDescriptionLength">Maximum overviews length</label>
|
||||
<input id="MaxDescriptionLength" name="MaxDescriptionLength" type="number" is="emby-input" min="0" />
|
||||
<div class="fieldDescription">This is the maximum length of the descriptions showed in series and episodes</div>
|
||||
<label class="inputLabel inputLabelUnfocused" for="MaxDescriptionLength">Maximum overviews
|
||||
length</label>
|
||||
<input id="MaxDescriptionLength" name="MaxDescriptionLength" type="number" is="emby-input"
|
||||
min="0" />
|
||||
<div class="fieldDescription">This is the maximum length of the descriptions showed in series
|
||||
and episodes</div>
|
||||
</div>
|
||||
<div class="selectContainer">
|
||||
<label class="selectLabel" for="EpisodeNumberingScheme">Episode numbering scheme</label>
|
||||
<select is="emby-select" id="EpisodeNumberingScheme" name="EpisodeNumberingScheme" label="Description markup" class="emby-select-withcolor emby-select">
|
||||
<option value="Default">Default (no specified numbering)</option>
|
||||
<option value="YYYYMMDD">YYYYMMDD (e.g. 20250804 for August 4th, 2025)</option>
|
||||
</select>
|
||||
<div class="selectArrowContainer">
|
||||
<span class="selectArrow material-icons keyboard_arrow_down" aria-hidden="true"></span>
|
||||
</div>
|
||||
<div class="fieldDescription">This is the numbering scheme for episodes in Jellyfin (IndexNumber).
|
||||
Default is the same as no numbering which allows Jellyfin to use the fallback numbering scheme.
|
||||
</div>
|
||||
<div>
|
||||
<h2>Synchronization</h2>
|
||||
</div>
|
||||
<div>
|
||||
<h3>Jellyfin to TubeArchivist</h3>
|
||||
</div>
|
||||
<div class="inputContainer">
|
||||
<label class="inputLabel inputLabelUnfocused" for="JFUsernameFrom">Jellyfin username to sync
|
||||
from</label>
|
||||
<input id="JFUsernameFrom" name="JFUsernameFrom" type="text" is="emby-input"
|
||||
placeholder="Username" />
|
||||
<div class="fieldDescription">This is the Jellyfin username to synchronize data to
|
||||
TubeArchivist</div>
|
||||
</div>
|
||||
<div class="checkboxContainer checkboxContainer-withDescription">
|
||||
<label class="emby-checkbox-label" for="JFTAProgressSync">
|
||||
<input id="JFTAProgressSync" name="JFTAProgressSync" type="checkbox" is="emby-checkbox" />
|
||||
<span>Synchronize Jellyfin playback progress to TubeArchivist</span>
|
||||
</label>
|
||||
</div>
|
||||
<div style="display: flex; gap: 2em;">
|
||||
<div class="checkboxContainer checkboxContainer-withDescription">
|
||||
<label class="emby-checkbox-label" for="JFTAPlaylistsSync">
|
||||
<input id="JFTAPlaylistsSync" name="JFTAPlaylistsSync" type="checkbox" is="emby-checkbox" />
|
||||
<span>Synchronize Jellyfin playlists to TubeArchivist</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="checkboxContainer checkboxContainer-withDescription">
|
||||
<label class="emby-checkbox-label" for="JFTAPlaylistsDelete">
|
||||
<input id="JFTAPlaylistsDelete" name="JFTAPlaylistsDelete" type="checkbox" is="emby-checkbox" />
|
||||
<span>Delete TubeArchivist playlists when no more present on Jellyfin</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h3>TubeArchivist to Jellyfin</h3>
|
||||
</div>
|
||||
<div class="inputContainer">
|
||||
<label class="inputLabel inputLabelUnfocused" for="JFUsernamesTo">Jellyfin usernames to sync
|
||||
playback status to</label>
|
||||
<input id="JFUsernamesTo" name="JFUsernamesTo" type="text" is="emby-input"
|
||||
placeholder="Username1, username2, ..." />
|
||||
<div class="fieldDescription">These are the (comma separated) Jellyfin usernames to synchronize
|
||||
data from TubeArchivist</div>
|
||||
</div>
|
||||
<div class="checkboxContainer checkboxContainer-withDescription">
|
||||
<label class="emby-checkbox-label" for="TAJFProgressSync">
|
||||
<input id="TAJFProgressSync" name="TAJFProgressSync" type="checkbox" is="emby-checkbox" />
|
||||
<span>Synchronize TubeArchivist playback progress to Jellyfin</span>
|
||||
</label>
|
||||
</div>
|
||||
<div style="display: flex; gap: 2em;">
|
||||
<div class="checkboxContainer checkboxContainer-withDescription">
|
||||
<label class="emby-checkbox-label" for="TAJFPlaylistsSync">
|
||||
<input id="TAJFPlaylistsSync" name="TAJFPlaylistsSync" type="checkbox" is="emby-checkbox" />
|
||||
<span>Synchronize TubeArchivist playlists to Jellyfin</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="checkboxContainer checkboxContainer-withDescription">
|
||||
<label class="emby-checkbox-label" for="TAJFPlaylistsDelete">
|
||||
<input id="TAJFPlaylistsDelete" name="TAJFPlaylistsDelete" type="checkbox" is="emby-checkbox" />
|
||||
<span>Delete Jellyfin playlists when no more present on TubeArchivist</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h2>Tasks intervals</h2>
|
||||
</div>
|
||||
<div class="inputContainer">
|
||||
<label class="inputLabel inputLabelUnfocused" for="TAJFProgressTaskInterval">TubeArchivist to Jellyfin
|
||||
playback progress synchronization interval</label>
|
||||
<input id="TAJFProgressTaskInterval" name="TAJFProgressTaskInterval" type="number" is="emby-input" min="60" />
|
||||
<div class="fieldDescription">This is the TubeArchivist to Jellyfin playback progress
|
||||
synchronization interval in seconds (Jellyfin restart required).</div>
|
||||
</div>
|
||||
<div class="inputContainer">
|
||||
<label class="inputLabel inputLabelUnfocused" for="JFTAPlaylistsSyncTaskInterval">Jellyfin to TubeArchivist
|
||||
playlists synchronization interval</label>
|
||||
<input id="JFTAPlaylistsSyncTaskInterval" name="JFTAPlaylistsSyncTaskInterval" type="number" is="emby-input" min="60" />
|
||||
<div class="fieldDescription">This is the Jellyfin to TubeArchivist playlists
|
||||
synchronization interval in seconds (Jellyfin restart required).</div>
|
||||
</div>
|
||||
<div class="inputContainer">
|
||||
<label class="inputLabel inputLabelUnfocused" for="TAJFPlaylistsSyncTaskInterval">TubeArchivist to Jellyfin
|
||||
playlists synchronization interval</label>
|
||||
<input id="TAJFPlaylistsSyncTaskInterval" name="TAJFPlaylistsSyncTaskInterval" type="number" is="emby-input"
|
||||
min="60" />
|
||||
<div class="fieldDescription">This is the TubeArchivist to Jellyfin playlists
|
||||
synchronization interval in seconds (Jellyfin restart required).</div>
|
||||
</div>
|
||||
<div>
|
||||
<button is="emby-button" type="submit" class="raised button-submit block emby-button">
|
||||
@@ -53,6 +166,16 @@
|
||||
document.querySelector('#TubeArchivistUrl').value = config.TubeArchivistUrl;
|
||||
document.querySelector('#TubeArchivistApiKey').value = config.TubeArchivistApiKey;
|
||||
document.querySelector('#MaxDescriptionLength').value = config.MaxDescriptionLength;
|
||||
document.querySelector('#JFTAProgressSync').checked = config.JFTAProgressSync;
|
||||
document.querySelector('#JFUsernamesTo').value = config.JFUsernamesTo;
|
||||
document.querySelector('#TAJFProgressSync').checked = config.TAJFProgressSync;
|
||||
document.querySelector('#JFTAPlaylistsSync').checked = config.JFTAPlaylistsSync;
|
||||
document.querySelector('#TAJFPlaylistsSync').checked = config.TAJFPlaylistsSync;
|
||||
document.querySelector('#JFUsernameFrom').value = config.JFUsernameFrom;
|
||||
document.querySelector('#EpisodeNumberingScheme').value = config.EpisodeNumberingScheme;
|
||||
document.querySelector('#TAJFProgressTaskInterval').value = config.TAJFProgressTaskInterval;
|
||||
document.querySelector('#JFTAPlaylistsSyncTaskInterval').value = config.JFTAPlaylistsSyncTaskInterval;
|
||||
document.querySelector('#TAJFPlaylistsSyncTaskInterval').value = config.TAJFPlaylistsSyncTaskInterval;
|
||||
Dashboard.hideLoadingMsg();
|
||||
});
|
||||
});
|
||||
@@ -65,6 +188,16 @@
|
||||
config.TubeArchivistUrl = document.querySelector('#TubeArchivistUrl').value;
|
||||
config.TubeArchivistApiKey = document.querySelector('#TubeArchivistApiKey').value;
|
||||
config.MaxDescriptionLength = document.querySelector('#MaxDescriptionLength').value;
|
||||
config.JFTAProgressSync = document.querySelector('#JFTAProgressSync').checked;
|
||||
config.JFUsernamesTo = document.querySelector('#JFUsernamesTo').value;
|
||||
config.TAJFProgressSync = document.querySelector('#TAJFProgressSync').checked;
|
||||
config.JFTAPlaylistsSync = document.querySelector('#JFTAPlaylistsSync').checked;
|
||||
config.TAJFPlaylistsSync = document.querySelector('#TAJFPlaylistsSync').checked;
|
||||
config.JFUsernameFrom = document.querySelector('#JFUsernameFrom').value;
|
||||
config.EpisodeNumberingScheme = document.querySelector('#EpisodeNumberingScheme').value;
|
||||
config.TAJFProgressTaskInterval = document.querySelector('#TAJFProgressTaskInterval').value;
|
||||
config.JFTAPlaylistsSyncTaskInterval = document.querySelector('#JFTAPlaylistsSyncTaskInterval').value;
|
||||
config.TAJFPlaylistsSyncTaskInterval = document.querySelector('#TAJFPlaylistsSyncTaskInterval').value;
|
||||
ApiClient.updatePluginConfiguration(TAMetadataConfig.pluginUniqueId, config).then(function (result) {
|
||||
Dashboard.processPluginConfigurationUpdateResult(result);
|
||||
});
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<RootNamespace>Jellyfin.Plugin.TubeArchivistMetadata</RootNamespace>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
@@ -11,8 +11,8 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Jellyfin.Controller" Version="10.8.13" />
|
||||
<PackageReference Include="Jellyfin.Model" Version="10.8.13" />
|
||||
<PackageReference Include="Jellyfin.Controller" Version="10.11.0" />
|
||||
<PackageReference Include="Jellyfin.Model" Version="10.11.0" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
@@ -1,12 +1,24 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Data.Enums;
|
||||
using Jellyfin.Plugin.TubeArchivistMetadata.Configuration;
|
||||
using Jellyfin.Plugin.TubeArchivistMetadata.Tasks;
|
||||
using Jellyfin.Plugin.TubeArchivistMetadata.TubeArchivist;
|
||||
using Jellyfin.Plugin.TubeArchivistMetadata.Utilities;
|
||||
using MediaBrowser.Common.Configuration;
|
||||
using MediaBrowser.Common.Plugins;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Entities.TV;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Playlists;
|
||||
using MediaBrowser.Controller.Session;
|
||||
using MediaBrowser.Model.Plugins;
|
||||
using MediaBrowser.Model.Serialization;
|
||||
using MediaBrowser.Model.Tasks;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Jellyfin.Plugin.TubeArchivistMetadata
|
||||
@@ -16,13 +28,29 @@ namespace Jellyfin.Plugin.TubeArchivistMetadata
|
||||
/// </summary>
|
||||
public class Plugin : BasePlugin<PluginConfiguration>, IHasWebPages
|
||||
{
|
||||
private readonly IUserManager _userManager;
|
||||
private readonly IUserDataManager _userDataManager;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Plugin"/> class.
|
||||
/// </summary>
|
||||
/// <param name="applicationPaths">Instance of the <see cref="IApplicationPaths"/> interface.</param>
|
||||
/// <param name="xmlSerializer">Instance of the <see cref="IXmlSerializer"/> interface.</param>
|
||||
/// <param name="logger">Instance of the <see cref="ILogger"/> interface.</param>
|
||||
public Plugin(IApplicationPaths applicationPaths, IXmlSerializer xmlSerializer, ILogger<Plugin> logger)
|
||||
/// <param name="sessionManager">Instance of the <see cref="ISessionManager"/> interface.</param>
|
||||
/// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
|
||||
/// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
|
||||
/// <param name="userDataManager">Instance of the <see cref="IUserDataManager"/> interface.</param>
|
||||
/// <param name="playlistManager">Instance of the <see cref="IPlaylistManager"/> interface.</param>
|
||||
public Plugin(
|
||||
IApplicationPaths applicationPaths,
|
||||
IXmlSerializer xmlSerializer,
|
||||
ILogger<Plugin> logger,
|
||||
ISessionManager sessionManager,
|
||||
ILibraryManager libraryManager,
|
||||
IUserManager userManager,
|
||||
IUserDataManager userDataManager,
|
||||
IPlaylistManager playlistManager)
|
||||
: base(applicationPaths, xmlSerializer)
|
||||
{
|
||||
Instance = this;
|
||||
@@ -31,9 +59,18 @@ namespace Jellyfin.Plugin.TubeArchivistMetadata
|
||||
handler.AllowAutoRedirect = false;
|
||||
handler.CheckCertificateRevocationList = true;
|
||||
HttpClient = new HttpClient(handler);
|
||||
HttpClient.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Token", Instance?.Configuration.TubeArchivistApiKey);
|
||||
UpdateAuthorizationHeader(Configuration.TubeArchivistApiKey);
|
||||
|
||||
SessionManager = sessionManager;
|
||||
sessionManager.PlaybackProgress += OnPlaybackProgress;
|
||||
LibraryManager = libraryManager;
|
||||
_userManager = userManager;
|
||||
_userDataManager = userDataManager;
|
||||
userDataManager.UserDataSaved += OnWatchedStatusChange;
|
||||
|
||||
logger.LogInformation("{Message}", "Collection display name: " + Instance?.Configuration.CollectionTitle);
|
||||
logger.LogInformation("{Message}", "TubeArchivist API URL: " + Instance?.Configuration.TubeArchivistUrl);
|
||||
logger.LogInformation("{Message}", "Pinging TubeArchivist API...");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -57,6 +94,16 @@ namespace Jellyfin.Plugin.TubeArchivistMetadata
|
||||
/// </summary>
|
||||
public HttpClient HttpClient { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the SessionManager used globally by the plugin.
|
||||
/// </summary>
|
||||
public ISessionManager SessionManager { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the LibraryManager used globally by the plugin.
|
||||
/// </summary>
|
||||
public ILibraryManager LibraryManager { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public IEnumerable<PluginPageInfo> GetPages() => new[]
|
||||
{
|
||||
@@ -66,5 +113,159 @@ namespace Jellyfin.Plugin.TubeArchivistMetadata
|
||||
EmbeddedResourcePath = string.Format(CultureInfo.InvariantCulture, "{0}.Configuration.configPage.html", GetType().Namespace)
|
||||
}
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Updates the HTTP client's Authorization header with the current API key.
|
||||
/// </summary>
|
||||
/// <param name="apiKey">TubeArchivist API key.</param>
|
||||
public void UpdateAuthorizationHeader(string apiKey)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(apiKey))
|
||||
{
|
||||
HttpClient.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Token", apiKey);
|
||||
Logger.LogInformation("{Message}", "Updated Authorization header with API key");
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.LogWarning("{Message}", "No TubeArchivist API key configured");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Logs the TubeArchivist API connection status.
|
||||
/// </summary>
|
||||
public void LogTAApiConnectionStatus()
|
||||
{
|
||||
Logger.LogInformation("{Message}", "Ping...");
|
||||
|
||||
var api = TubeArchivistApi.GetInstance();
|
||||
Task.Run(() => api.Ping()).ContinueWith(
|
||||
task =>
|
||||
{
|
||||
if (task.Result != null)
|
||||
{
|
||||
Logger.LogInformation("{Message}", "TubeArchivist API said: " + task.Result.Response + "!");
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.LogCritical("{Message}", "TubeArchivist API was unreachable!");
|
||||
}
|
||||
},
|
||||
TaskScheduler.Default);
|
||||
}
|
||||
|
||||
private async void OnPlaybackProgress(object? sender, PlaybackProgressEventArgs eventArgs)
|
||||
{
|
||||
if (eventArgs == null || eventArgs.Item.Id == Guid.Empty)
|
||||
{
|
||||
Logger.LogDebug("Skipping progress synchronization: PlaybackProgress event triggered with null or empty Guid.");
|
||||
return;
|
||||
}
|
||||
|
||||
var topParent = eventArgs.Item.GetTopParent();
|
||||
if (
|
||||
Instance!.Configuration.JFTAProgressSync &&
|
||||
eventArgs.Users.Any(u => Instance!.Configuration.JFUsernameFrom.Equals(u.Username, StringComparison.Ordinal)) &&
|
||||
eventArgs.PlaybackPositionTicks.HasValue &&
|
||||
string.Equals(topParent?.Name, Instance?.Configuration.CollectionTitle, StringComparison.OrdinalIgnoreCase)
|
||||
)
|
||||
{
|
||||
long progress = (long)eventArgs.PlaybackPositionTicks / TimeSpan.TicksPerSecond;
|
||||
var videoId = Utils.GetVideoNameFromPath(eventArgs.Item.Path);
|
||||
try
|
||||
{
|
||||
var statusCode = await TubeArchivistApi.GetInstance().SetProgress(videoId, progress).ConfigureAwait(true);
|
||||
if (statusCode != System.Net.HttpStatusCode.OK)
|
||||
{
|
||||
Logger.LogCritical("{Message}", $"POST /video/{videoId}/progress returned {statusCode} for video {eventArgs.Item.Name} with progress {progress} seconds");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogCritical("An exception occurred while calling POST /video/{VideoId}/progress for for video {VideoName} with progress {Progress} seconds: {ExceptionMessage}", videoId, eventArgs.Item.Name, progress, ex.Message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async void OnWatchedStatusChange(object? sender, UserDataSaveEventArgs eventArgs)
|
||||
{
|
||||
if (eventArgs == null || eventArgs.Item.Id == Guid.Empty)
|
||||
{
|
||||
Logger.LogDebug("Skipping watched status synchronization: WatchedStatusChange event triggered with null or empty Guid.");
|
||||
return;
|
||||
}
|
||||
|
||||
var topParent = eventArgs.Item.GetTopParent();
|
||||
var user = _userManager.GetUserById(eventArgs.UserId);
|
||||
if (
|
||||
Configuration.JFTAProgressSync
|
||||
&& user == null
|
||||
&& string.Equals(topParent?.Name, Instance?.Configuration.CollectionTitle, StringComparison.OrdinalIgnoreCase)
|
||||
)
|
||||
{
|
||||
Logger.LogError("OnWatchedStatusChange callback called without user id for item {ItemName}", eventArgs.Item.Name);
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
Configuration.JFTAProgressSync
|
||||
&& user != null
|
||||
&& string.Equals(Configuration.JFUsernameFrom, user!.Username, StringComparison.Ordinal)
|
||||
&& string.Equals(topParent?.Name, Instance?.Configuration.CollectionTitle, StringComparison.OrdinalIgnoreCase)
|
||||
)
|
||||
{
|
||||
var userItemData = _userDataManager.GetUserData(user, eventArgs.Item);
|
||||
var isPlayed = eventArgs.Item.IsPlayed(user, userItemData);
|
||||
Logger.LogDebug("User {UserId} changed watched status to {Status} for the item {ItemName}", eventArgs.UserId, isPlayed, eventArgs.Item.Name);
|
||||
string itemYTId;
|
||||
try
|
||||
{
|
||||
if (eventArgs.Item is Series)
|
||||
{
|
||||
itemYTId = Utils.GetChannelNameFromPath(eventArgs.Item.Path);
|
||||
}
|
||||
else if (eventArgs.Item is Episode)
|
||||
{
|
||||
itemYTId = Utils.GetVideoNameFromPath(eventArgs.Item.Path);
|
||||
}
|
||||
else
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError(ex, "Error while processing item path: {ItemPath}", eventArgs.Item.Path ?? "null");
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var statusCode = await TubeArchivistApi.GetInstance().SetWatchedStatus(itemYTId, isPlayed).ConfigureAwait(true);
|
||||
if (statusCode != System.Net.HttpStatusCode.OK)
|
||||
{
|
||||
Logger.LogCritical("POST /watched returned {StatusCode} for item {ItemName} ({VideoYTId}) with watched status {IsPlayed}", statusCode, eventArgs.Item.Name, itemYTId, isPlayed);
|
||||
}
|
||||
|
||||
if (eventArgs.Item is Episode)
|
||||
{
|
||||
var progress = _userDataManager.GetUserData(user, eventArgs.Item)?.PlaybackPositionTicks / TimeSpan.TicksPerSecond;
|
||||
if (progress != null)
|
||||
{
|
||||
var videoId = Utils.GetVideoNameFromPath(eventArgs.Item.Path);
|
||||
statusCode = await TubeArchivistApi.GetInstance().SetProgress(videoId, progress.Value).ConfigureAwait(true);
|
||||
if (statusCode != System.Net.HttpStatusCode.OK)
|
||||
{
|
||||
Logger.LogCritical("{Message}", $"POST /video/{videoId}/progress returned {statusCode} for video {eventArgs.Item.Name} with progress {progress} seconds");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogCritical("An exception occurred while calling POST /watched for item {ItemName} ({VideoYTId}) with watched status {IsPlayed}: {ExceptionMessage}", eventArgs.Item.Name, itemYTId, isPlayed, ex.Message);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Data;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
@@ -46,7 +47,7 @@ namespace Jellyfin.Plugin.TubeArchivistMetadata.Providers
|
||||
public string Name => "TubeArchivist";
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool Supports(BaseItem item) => item is Episode;
|
||||
public bool Supports(BaseItem item) => item is Episode && Utils.IsTubeArchivistItem(item);
|
||||
|
||||
/// <inheritdoc />
|
||||
public IEnumerable<ImageType> GetSupportedImages(BaseItem item)
|
||||
@@ -59,12 +60,12 @@ namespace Jellyfin.Plugin.TubeArchivistMetadata.Providers
|
||||
{
|
||||
var list = new List<RemoteImageInfo>();
|
||||
var taApi = TubeArchivistApi.GetInstance();
|
||||
var videoTAId = item.Path.Split("/").Last().Split(".").First();
|
||||
var videoTAId = Utils.GetVideoNameFromPath(item.Path);
|
||||
var video = await taApi.GetVideo(videoTAId).ConfigureAwait(true);
|
||||
_logger.LogInformation("{Message}", string.Format(CultureInfo.CurrentCulture, "Getting images for video: {0} ({1})", video?.Title, videoTAId));
|
||||
_logger.LogInformation("{Message}", "Thumb URI: " + video?.VidThumbUrl);
|
||||
_logger.LogDebug("{Message}", string.Format(CultureInfo.CurrentCulture, "Getting images for video: {0} ({1})", video?.Title, videoTAId));
|
||||
_logger.LogDebug("{Message}", "Thumb URI: " + video?.VidThumbUrl);
|
||||
|
||||
if (video != null)
|
||||
if (video != null && Utils.HasImageUrl(video.VidThumbUrl))
|
||||
{
|
||||
list.Add(new RemoteImageInfo
|
||||
{
|
||||
@@ -86,6 +87,11 @@ namespace Jellyfin.Plugin.TubeArchivistMetadata.Providers
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!Utils.HasImageUrl(url))
|
||||
{
|
||||
throw new HttpRequestException("TubeArchivist returned an empty image URL.");
|
||||
}
|
||||
|
||||
return await Plugin.Instance.HttpClient.GetAsync(new Uri(Utils.SanitizeUrl(Plugin.Instance.Configuration.TubeArchivistUrl + url).TrimEnd('/')), cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Data;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
@@ -49,11 +50,16 @@ namespace Jellyfin.Plugin.TubeArchivistMetadata.Providers
|
||||
public async Task<MetadataResult<Episode>> GetMetadata(EpisodeInfo info, CancellationToken cancellationToken)
|
||||
{
|
||||
var result = new MetadataResult<Episode>();
|
||||
if (string.IsNullOrWhiteSpace(info.Path))
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
var taApi = TubeArchivistApi.GetInstance();
|
||||
var videoTAId = info.Path.Split("/").Last().Split(".").First();
|
||||
var videoTAId = Utils.GetVideoNameFromPath(info.Path);
|
||||
var video = await taApi.GetVideo(videoTAId).ConfigureAwait(true);
|
||||
_logger.LogInformation("{Message}", string.Format(CultureInfo.CurrentCulture, "Getting metadata for video: {0} ({1})", video?.Title, videoTAId));
|
||||
_logger.LogInformation("{Message}", "Received metadata: \n" + JsonConvert.SerializeObject(video));
|
||||
_logger.LogDebug("{Message}", string.Format(CultureInfo.CurrentCulture, "Getting metadata for video: {0} ({1})", video?.Title, videoTAId));
|
||||
_logger.LogDebug("{Message}", "Received metadata: \n" + JsonConvert.SerializeObject(video));
|
||||
|
||||
if (video != null)
|
||||
{
|
||||
@@ -62,10 +68,11 @@ namespace Jellyfin.Plugin.TubeArchivistMetadata.Providers
|
||||
{
|
||||
Name = video.Channel.Name,
|
||||
ImageUrl = video.Channel.ThumbUrl,
|
||||
Type = PersonType.Actor,
|
||||
Type = Data.Enums.PersonKind.Actor,
|
||||
});
|
||||
result.HasMetadata = true;
|
||||
result.Item = video.ToEpisode();
|
||||
result.Item.Path = info.Path;
|
||||
result.Provider = Name;
|
||||
result.People = peopleInfo;
|
||||
}
|
||||
@@ -77,9 +84,13 @@ namespace Jellyfin.Plugin.TubeArchivistMetadata.Providers
|
||||
public async Task<IEnumerable<RemoteSearchResult>> GetSearchResults(EpisodeInfo searchInfo, CancellationToken cancellationToken)
|
||||
{
|
||||
var results = new List<RemoteSearchResult>();
|
||||
if (string.IsNullOrWhiteSpace(searchInfo.Path))
|
||||
{
|
||||
return results;
|
||||
}
|
||||
|
||||
var taApi = TubeArchivistApi.GetInstance();
|
||||
var videoTAId = searchInfo.Path.Split("/").Last().Split(".").First();
|
||||
var videoTAId = Utils.GetVideoNameFromPath(searchInfo.Path);
|
||||
var video = await taApi.GetVideo(videoTAId).ConfigureAwait(true);
|
||||
if (video != null)
|
||||
{
|
||||
|
||||
@@ -45,12 +45,12 @@ namespace Jellyfin.Plugin.TubeArchivistMetadata.Providers
|
||||
public string Name => "TubeArchivist";
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool Supports(BaseItem item) => item is Series;
|
||||
public bool Supports(BaseItem item) => item is Series && Utils.IsTubeArchivistItem(item);
|
||||
|
||||
/// <inheritdoc />
|
||||
public IEnumerable<ImageType> GetSupportedImages(BaseItem item)
|
||||
{
|
||||
return new[] { ImageType.Primary };
|
||||
return new[] { ImageType.Primary, ImageType.Art, ImageType.Banner, ImageType.Backdrop };
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -58,14 +58,12 @@ namespace Jellyfin.Plugin.TubeArchivistMetadata.Providers
|
||||
{
|
||||
var list = new List<RemoteImageInfo>();
|
||||
var taApi = TubeArchivistApi.GetInstance();
|
||||
var channelTAId = item.Path.Split("/").Last();
|
||||
var channelTAId = Utils.GetChannelNameFromPath(item.Path);
|
||||
var channel = await taApi.GetChannel(channelTAId).ConfigureAwait(true);
|
||||
_logger.LogInformation("{Message}", string.Format(CultureInfo.CurrentCulture, "Getting images for channel: {0} ({1})", channel?.Name, channelTAId));
|
||||
_logger.LogInformation("{Message}", "Thumb URI: " + channel?.ThumbUrl);
|
||||
_logger.LogInformation("{Message}", "TVArt URI: " + channel?.TvartUrl);
|
||||
_logger.LogInformation("{Message}", "Banner URI: " + channel?.BannerUrl);
|
||||
_logger.LogDebug("{Message}", string.Format(CultureInfo.CurrentCulture, "Getting images for channel: {0} ({1})", channel?.Name, channelTAId));
|
||||
_logger.LogDebug("{Message}", "Thumb URI: " + channel?.ThumbUrl);
|
||||
|
||||
if (channel != null)
|
||||
if (channel != null && Utils.HasImageUrl(channel.ThumbUrl))
|
||||
{
|
||||
list.Add(new RemoteImageInfo
|
||||
{
|
||||
@@ -73,12 +71,27 @@ namespace Jellyfin.Plugin.TubeArchivistMetadata.Providers
|
||||
Type = ImageType.Primary,
|
||||
Url = channel.ThumbUrl
|
||||
});
|
||||
}
|
||||
|
||||
if (channel != null && Utils.HasImageUrl(channel.TvartUrl))
|
||||
{
|
||||
list.Add(new RemoteImageInfo
|
||||
{
|
||||
ProviderName = Name,
|
||||
Type = ImageType.Art,
|
||||
Url = channel.TvartUrl
|
||||
});
|
||||
|
||||
list.Add(new RemoteImageInfo
|
||||
{
|
||||
ProviderName = Name,
|
||||
Type = ImageType.Backdrop,
|
||||
Url = channel.TvartUrl
|
||||
});
|
||||
}
|
||||
|
||||
if (channel != null && Utils.HasImageUrl(channel.BannerUrl))
|
||||
{
|
||||
list.Add(new RemoteImageInfo
|
||||
{
|
||||
ProviderName = Name,
|
||||
@@ -99,6 +112,11 @@ namespace Jellyfin.Plugin.TubeArchivistMetadata.Providers
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!Utils.HasImageUrl(url))
|
||||
{
|
||||
throw new HttpRequestException("TubeArchivist returned an empty image URL.");
|
||||
}
|
||||
|
||||
return await Plugin.Instance.HttpClient.GetAsync(new Uri(Utils.SanitizeUrl(Plugin.Instance.Configuration.TubeArchivistUrl + url).TrimEnd('/')), cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,11 +49,16 @@ namespace Jellyfin.Plugin.TubeArchivistMetadata.Providers
|
||||
public async Task<MetadataResult<Series>> GetMetadata(SeriesInfo info, CancellationToken cancellationToken)
|
||||
{
|
||||
var result = new MetadataResult<Series>();
|
||||
if (string.IsNullOrWhiteSpace(info.Path))
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
var taApi = TubeArchivistApi.GetInstance();
|
||||
var channelTAId = info.Path.Split("/").Last();
|
||||
var channelTAId = Utils.GetChannelNameFromPath(info.Path);
|
||||
var channel = await taApi.GetChannel(channelTAId).ConfigureAwait(true);
|
||||
_logger.LogInformation("{Message}", string.Format(CultureInfo.CurrentCulture, "Getting metadata for channel: {0} ({1})", channel?.Name, channelTAId));
|
||||
_logger.LogInformation("{Message}", "Received metadata: \n" + JsonConvert.SerializeObject(channel));
|
||||
_logger.LogDebug("{Message}", string.Format(CultureInfo.CurrentCulture, "Getting metadata for channel: {0} ({1})", channel?.Name, channelTAId));
|
||||
_logger.LogDebug("{Message}", "Received metadata: \n" + JsonConvert.SerializeObject(channel));
|
||||
|
||||
if (channel != null)
|
||||
{
|
||||
@@ -62,7 +67,7 @@ namespace Jellyfin.Plugin.TubeArchivistMetadata.Providers
|
||||
{
|
||||
Name = channel.Name,
|
||||
ImageUrl = channel.ThumbUrl,
|
||||
Type = PersonType.Actor,
|
||||
Type = Data.Enums.PersonKind.Actor,
|
||||
});
|
||||
result.HasMetadata = true;
|
||||
result.Item = channel.ToSeries();
|
||||
@@ -77,9 +82,13 @@ namespace Jellyfin.Plugin.TubeArchivistMetadata.Providers
|
||||
public async Task<IEnumerable<RemoteSearchResult>> GetSearchResults(SeriesInfo searchInfo, CancellationToken cancellationToken)
|
||||
{
|
||||
var results = new List<RemoteSearchResult>();
|
||||
if (string.IsNullOrWhiteSpace(searchInfo.Path))
|
||||
{
|
||||
return results;
|
||||
}
|
||||
|
||||
var taApi = TubeArchivistApi.GetInstance();
|
||||
var channelTAId = searchInfo.Path.Split("/").Last();
|
||||
var channelTAId = Utils.GetChannelNameFromPath(searchInfo.Path);
|
||||
var channel = await taApi.GetChannel(channelTAId).ConfigureAwait(true);
|
||||
if (channel != null)
|
||||
{
|
||||
|
||||
@@ -0,0 +1,437 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using J2N.Collections.Generic.Extensions;
|
||||
using Jellyfin.Data.Enums;
|
||||
using Jellyfin.Database.Implementations.Entities;
|
||||
using Jellyfin.Extensions;
|
||||
using Jellyfin.Plugin.TubeArchivistMetadata.TubeArchivist;
|
||||
using Jellyfin.Plugin.TubeArchivistMetadata.Utilities;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Entities.TV;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Playlists;
|
||||
using MediaBrowser.Model.Dto;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.Playlists;
|
||||
using MediaBrowser.Model.Tasks;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Jellyfin.Plugin.TubeArchivistMetadata.Tasks
|
||||
{
|
||||
/// <summary>
|
||||
/// Task to sync TubeArchivist playlists to Jellyfin.
|
||||
/// </summary>
|
||||
public class JFToTubeArchivistPlaylistsSyncTask : IScheduledTask
|
||||
{
|
||||
private readonly ILogger<Plugin> _logger;
|
||||
private readonly ILibraryManager _libraryManager;
|
||||
private readonly IUserManager _userManager;
|
||||
private readonly IPlaylistManager _playlistManager;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="JFToTubeArchivistPlaylistsSyncTask"/> class.
|
||||
/// </summary>
|
||||
/// <param name="logger">Logger.</param>
|
||||
/// <param name="libraryManager">Library manager.</param>
|
||||
/// <param name="userManager">User manager.</param>
|
||||
/// <param name="playlistManager">Playlists manager.</param>
|
||||
public JFToTubeArchivistPlaylistsSyncTask(ILogger<Plugin> logger, ILibraryManager libraryManager, IUserManager userManager, IPlaylistManager playlistManager)
|
||||
{
|
||||
_logger = logger;
|
||||
_libraryManager = libraryManager;
|
||||
_userManager = userManager;
|
||||
_playlistManager = playlistManager;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string Name => "JFToTubeArchivistPlaylistsSyncTask";
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string Description => "This tasks syncs Jellyfin playlists to TubeArchivist";
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string Category => "TubeArchivistMetadata";
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string Key => "JFToTubeArchivistPlaylistsSyncTask";
|
||||
|
||||
private Dictionary<Guid, List<BaseItem>> GetAllVideos(IEnumerable<MediaBrowser.Controller.Playlists.Playlist> playlists, User user)
|
||||
{
|
||||
var items = new Dictionary<Guid, List<BaseItem>>();
|
||||
foreach (var playlist in playlists)
|
||||
{
|
||||
items[playlist.Id] = new List<BaseItem>(playlist.GetChildren(user, true, new InternalItemsQuery()));
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
private int CountTotalVideos(Dictionary<Guid, List<BaseItem>> playlistsItems)
|
||||
{
|
||||
var totalVideosCount = 0;
|
||||
foreach (var playlistId in playlistsItems.Keys)
|
||||
{
|
||||
totalVideosCount += playlistsItems[playlistId].Count;
|
||||
}
|
||||
|
||||
return totalVideosCount;
|
||||
}
|
||||
|
||||
private string GetPlaylistUpdatedName(string playlistName, string newId)
|
||||
{
|
||||
var index = playlistName.LastIndexOf(" (", StringComparison.CurrentCulture);
|
||||
if (index < 0)
|
||||
{
|
||||
return $"{playlistName} ({newId})";
|
||||
}
|
||||
else
|
||||
{
|
||||
var authorAndName = playlistName.Substring(0, index);
|
||||
return $"{authorAndName} ({newId})";
|
||||
}
|
||||
}
|
||||
|
||||
private void MoveElementToPosition(Collection<PlaylistEntry> playlistEntries, int oldPosition, int newPosition)
|
||||
{
|
||||
var oldItem = playlistEntries[oldPosition];
|
||||
playlistEntries.RemoveAt(oldPosition);
|
||||
playlistEntries.Insert(newPosition, oldItem);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task ExecuteAsync(IProgress<double> progress, CancellationToken cancellationToken)
|
||||
{
|
||||
progress.Report(0);
|
||||
if (Plugin.Instance!.Configuration.JFTAPlaylistsSync)
|
||||
{
|
||||
var start = DateTime.Now;
|
||||
_logger.LogInformation("Starting Jellyfin->TubeArchivist playlists synchronization.");
|
||||
var taApi = TubeArchivistApi.GetInstance();
|
||||
var taPlaylists = await taApi.GetPlaylists().ConfigureAwait(true);
|
||||
|
||||
var jfUsername = Plugin.Instance!.Configuration.JFUsernameFrom;
|
||||
|
||||
var user = _userManager.GetUserByName(jfUsername);
|
||||
if (user == null)
|
||||
{
|
||||
_logger.LogInformation("{Message}", $"Jellyfin user with username {jfUsername} not found");
|
||||
return;
|
||||
}
|
||||
|
||||
var userPlaylists = _playlistManager.GetPlaylists(user.Id).ToList();
|
||||
var jfItemsToAnalyze = GetAllVideos(userPlaylists, user);
|
||||
var totalVideosCount = CountTotalVideos(jfItemsToAnalyze);
|
||||
var processedVideosCount = 0;
|
||||
|
||||
_logger.LogInformation("Found a total of {PlaylistsCount} playlists to analyze with a total of {VideosCount} videos", userPlaylists.Count, totalVideosCount);
|
||||
if (taPlaylists != null && Plugin.Instance!.Configuration.JFTAPlaylistsDelete)
|
||||
{
|
||||
var taPlaylistsToDelete = taPlaylists.Where(tp => !userPlaylists.Select(up => Utils.GetTAPlaylistIdFromName(up.Name)).Contains(tp.Id));
|
||||
foreach (var taPlaylistToDelete in taPlaylistsToDelete)
|
||||
{
|
||||
_logger.LogInformation("Deleting TubeArchivist playlist {PlaylistName} ({PlaylistId})", taPlaylistToDelete.Name, taPlaylistToDelete.Id);
|
||||
await taApi.DeletePlaylist(taPlaylistToDelete.Id).ConfigureAwait(true);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var jfPlaylist in userPlaylists)
|
||||
{
|
||||
_logger.LogInformation("Analyzing playlist {PlaylistName}...", jfPlaylist.Name);
|
||||
var taPlaylistId = Utils.GetTAPlaylistIdFromName(jfPlaylist.Name);
|
||||
if (taPlaylistId == null)
|
||||
{
|
||||
_logger.LogDebug("The playlist {PlaylistName} was not a TA playlist: could not find TA playlist id at the end", jfPlaylist.Name);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Try to find a TA playlist with matching id
|
||||
// N.B.: only videos with TubeArchivist provider can be synced to TA, if there is a non matching video log a warn
|
||||
var taPlaylist = taPlaylists?.Where(tp => tp.Id == taPlaylistId).FirstOrDefault();
|
||||
|
||||
if (taPlaylist != null)
|
||||
{
|
||||
// If it exists and is custom add new videos and update videos order
|
||||
if (taPlaylist.Type == PlaylistType.Custom)
|
||||
{
|
||||
var jfItems = jfItemsToAnalyze[jfPlaylist.Id];
|
||||
_logger.LogInformation("Found {PlaylistVideosCount} videos in playlist {PlaylistName}", jfItems.Count, jfPlaylist.Name);
|
||||
|
||||
var itemsToProcess = new List<PlaylistItemAction>();
|
||||
|
||||
// Add videos to move/add to the TA playlist
|
||||
for (var i = 0; i < jfItems.Count; i++)
|
||||
{
|
||||
if (!jfItems[i].ProviderIds.ContainsKey(Constants.ProviderName))
|
||||
{
|
||||
_logger.LogError("Could not sync {JFItem} video from playlist {JFPlaylist} because it doesn't belong to TubeArchivist", jfItems[i].Name, jfPlaylist.Name);
|
||||
continue;
|
||||
}
|
||||
|
||||
var position = taPlaylist.Entries.FindIndex(e => e.YoutubeId == jfItems[i].ProviderIds[Constants.ProviderName]);
|
||||
PlaylistItemAction action;
|
||||
if (position < 0)
|
||||
{
|
||||
_logger.LogDebug("Video {VideoName} from playlist {PlaylistName} not found in TA playlist", jfItems[i].Name, jfPlaylist.Name);
|
||||
var positionsToMove = i - position;
|
||||
action = new PlaylistItemAction(i, jfItems[i].ProviderIds[Constants.ProviderName], CustomPlaylistAction.Create, positionsToMove, jfItems[i].Name);
|
||||
taPlaylist.Entries.Insert(i, new PlaylistEntry(jfItems[i].ProviderIds[Constants.ProviderName], jfItems[i].Name, jfItems[i].Studios[0], i, true));
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogDebug("Found video {VideoName} at position {JFPosition} in playlist {PlaylistName} at position {TAPosition} in TA playlist", jfItems[i].Name, i, jfPlaylist.Name, position);
|
||||
if (i == 0)
|
||||
{
|
||||
action = new PlaylistItemAction(i, jfItems[i].ProviderIds[Constants.ProviderName], CustomPlaylistAction.Top, jfItems[i].Name);
|
||||
}
|
||||
else if (i == jfItems.Count)
|
||||
{
|
||||
action = new PlaylistItemAction(i, jfItems[i].ProviderIds[Constants.ProviderName], CustomPlaylistAction.Bottom, jfItems[i].Name);
|
||||
}
|
||||
else
|
||||
{
|
||||
var positionsToMove = i - position;
|
||||
action = new PlaylistItemAction(i, jfItems[i].ProviderIds[Constants.ProviderName], positionsToMove > 0 ? CustomPlaylistAction.Up : CustomPlaylistAction.Down, Math.Abs(positionsToMove), jfItems[i].Name);
|
||||
}
|
||||
|
||||
var temp = taPlaylist.Entries[position];
|
||||
taPlaylist.Entries.RemoveAt(position);
|
||||
taPlaylist.Entries.Insert(i, temp);
|
||||
}
|
||||
|
||||
itemsToProcess.Add(action);
|
||||
}
|
||||
|
||||
// Add videos to delete from the TA playlist
|
||||
var itemsToDelete = taPlaylist.Entries
|
||||
.Where(e => !itemsToProcess
|
||||
.Select(i => i.YoutubeId)
|
||||
.Contains(e.YoutubeId))
|
||||
.ToList();
|
||||
foreach (var item in itemsToDelete)
|
||||
{
|
||||
_logger.LogDebug("Video {VideoName} not found in JF playlist, marking for deletion", item.Title);
|
||||
var ret = taPlaylist.Entries.Remove(item);
|
||||
}
|
||||
|
||||
itemsToProcess.AddRange(itemsToDelete.Select(e => new PlaylistItemAction(e.YoutubeId, e.Title)));
|
||||
|
||||
foreach (var item in itemsToProcess)
|
||||
{
|
||||
_logger.LogDebug("Analyzing video {VideoName} from playlist {PlaylistName} at position {Position}", item.Name, jfPlaylist.Name, item.Index);
|
||||
switch (item.Action)
|
||||
{
|
||||
case CustomPlaylistAction.Create:
|
||||
var success = await AddVideoToTAPlaylist(taApi, jfPlaylist, taPlaylistId, item).ConfigureAwait(true);
|
||||
if (!success)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case CustomPlaylistAction.Top:
|
||||
case CustomPlaylistAction.Bottom:
|
||||
_logger.LogDebug("Moving the video {VideoName} to the {Side} of the playlist {PlaylistName}", item.Name, item.Action, jfPlaylist.Name);
|
||||
await MoveOrDeleteVideo(taApi, jfPlaylist, taPlaylistId, item).ConfigureAwait(true);
|
||||
break;
|
||||
|
||||
case CustomPlaylistAction.Remove:
|
||||
_logger.LogDebug("Removing the video {VideoName} in playlist {PlaylistName}", item.Name, jfPlaylist.Name);
|
||||
await MoveOrDeleteVideo(taApi, jfPlaylist, taPlaylistId, item).ConfigureAwait(true);
|
||||
break;
|
||||
|
||||
default:
|
||||
_logger.LogDebug("Moving the video {VideoName} {Direction} in playlist {PlaylistName}", item.Name, item.Action, jfPlaylist.Name);
|
||||
await MoveOrDeleteVideo(taApi, jfPlaylist, taPlaylistId, item).ConfigureAwait(true);
|
||||
break;
|
||||
}
|
||||
|
||||
processedVideosCount++;
|
||||
progress.Report(processedVideosCount * 100 / totalVideosCount);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogWarning("Could not sync modifications on a YouTube downloaded TA playlist: {TAPlaylist}", $"{taPlaylist.Name} - {taPlaylist.Channel} ({taPlaylist.Id})");
|
||||
continue;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// If it doesn't, create a new custom playlist and add videos
|
||||
_logger.LogInformation("Playlist {PlaylistName} was not found on TubeArchivist. Creating a new playlist...", jfPlaylist.Name);
|
||||
|
||||
var playlistName = Utils.GetTAPlaylistNameFromName(jfPlaylist.Name);
|
||||
if (string.IsNullOrEmpty(playlistName))
|
||||
{
|
||||
playlistName = jfPlaylist.Name;
|
||||
}
|
||||
|
||||
_logger.LogDebug("Creating a new TubeArchivist playlist with name: {PlaylistName}", playlistName);
|
||||
|
||||
var creationRequest = new CustomPlaylistCreation(playlistName);
|
||||
var createdTAPlaylist = await taApi.CreateCustomPlaylist(creationRequest).ConfigureAwait(true);
|
||||
if (createdTAPlaylist == null)
|
||||
{
|
||||
_logger.LogError("Failed to create the playlist {JFPlaylist}", playlistName);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Update the JF playlist name with the new TA playlist id
|
||||
var newPlaylistName = GetPlaylistUpdatedName(jfPlaylist.Name, createdTAPlaylist.Id);
|
||||
var updateRequest = new PlaylistUpdateRequest
|
||||
{
|
||||
Id = jfPlaylist.Id,
|
||||
Name = newPlaylistName,
|
||||
UserId = user.Id
|
||||
};
|
||||
await _playlistManager.UpdatePlaylist(updateRequest).ConfigureAwait(true);
|
||||
_logger.LogDebug("Updated the playlist name from {NewPlaylistName} to {NewPlaylistName}", playlistName, newPlaylistName);
|
||||
|
||||
foreach (var jfItem in jfPlaylist.GetItemList(new InternalItemsQuery()))
|
||||
{
|
||||
_logger.LogDebug("Adding the video {VideoName} to the playlist {PlaylistName}", jfItem.Name, jfPlaylist.Name);
|
||||
var action = new CustomPlaylistEntryAction(CustomPlaylistAction.Create, jfItem.ProviderIds[Constants.ProviderName]);
|
||||
var response = await taApi.CustomPlaylistEntryAction(taPlaylistId, action).ConfigureAwait(true);
|
||||
if (response != System.Net.HttpStatusCode.OK)
|
||||
{
|
||||
_logger.LogError("Failed to add the video {JFItem} to playlist {JFPlaylist}", $"{jfItem.Name} ({jfItem.ProviderIds[Constants.ProviderName]})", jfPlaylist.Name);
|
||||
continue;
|
||||
}
|
||||
|
||||
processedVideosCount++;
|
||||
progress.Report(processedVideosCount * 100 / totalVideosCount);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_logger.LogInformation("Time elapsed: {Time}", DateTime.Now - start);
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogInformation("Jellyfin->TubeArchivist playlists synchronization is currently disabled.");
|
||||
}
|
||||
|
||||
progress.Report(100);
|
||||
}
|
||||
|
||||
private async Task MoveOrDeleteVideo(TubeArchivistApi taApi, MediaBrowser.Controller.Playlists.Playlist jfPlaylist, string taPlaylistId, PlaylistItemAction item)
|
||||
{
|
||||
var response = HttpStatusCode.OK;
|
||||
for (var i = 0; i < item.PositionsToMove; i++)
|
||||
{
|
||||
var action = new CustomPlaylistEntryAction(item.Action, item.YoutubeId);
|
||||
response = await taApi.CustomPlaylistEntryAction(taPlaylistId, action).ConfigureAwait(true);
|
||||
if (response != System.Net.HttpStatusCode.OK)
|
||||
{
|
||||
_logger.LogError("Failed to execute the operation {Operation} for the video {JFItem} in playlist {PlaylistName}", item.Action, $"{item.Name} ({item.YoutubeId})", jfPlaylist.Name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<bool> AddVideoToTAPlaylist(TubeArchivistApi taApi, MediaBrowser.Controller.Playlists.Playlist jfPlaylist, string taPlaylistId, PlaylistItemAction item)
|
||||
{
|
||||
// TA playlist doesn't contain the JF video
|
||||
_logger.LogDebug("Adding the video {VideoName} to the playlist {PlaylistName}", item.Name, jfPlaylist.Name);
|
||||
var action = new CustomPlaylistEntryAction(CustomPlaylistAction.Create, item.YoutubeId);
|
||||
var response = await taApi.CustomPlaylistEntryAction(taPlaylistId, action).ConfigureAwait(true);
|
||||
if (response != System.Net.HttpStatusCode.OK)
|
||||
{
|
||||
_logger.LogError("Failed to add the video {JFItem} to playlist {JFPlaylist}", $"{item.Name} ({item.YoutubeId})", jfPlaylist.Name);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Since now we have the video added at the bottom of the TA playlist (previouse entries count + 1),
|
||||
// we need to move the video to the correct position
|
||||
var positionsToMove = item.PositionsToMove;
|
||||
action = new CustomPlaylistEntryAction(CustomPlaylistAction.Up, item.YoutubeId);
|
||||
if (item.PositionsToMove < 0)
|
||||
{
|
||||
positionsToMove = (int)Math.Abs((decimal)positionsToMove!);
|
||||
action = new CustomPlaylistEntryAction(CustomPlaylistAction.Up, item.YoutubeId);
|
||||
}
|
||||
|
||||
_logger.LogDebug("Moving the video {VideoName} of the playlist {PlaylistName} {Positions} up", item.Name, jfPlaylist.Name, positionsToMove);
|
||||
for (var i = 0; i < positionsToMove; i++)
|
||||
{
|
||||
response = await taApi.CustomPlaylistEntryAction(taPlaylistId, action).ConfigureAwait(true);
|
||||
if (response != System.Net.HttpStatusCode.OK)
|
||||
{
|
||||
_logger.LogError("Failed to move {Direction} the video {JFItem} in playlist {JFPlaylist}", item.Action, $"{item.Name} ({item.YoutubeId})", jfPlaylist.Name);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IEnumerable<TaskTriggerInfo> GetDefaultTriggers()
|
||||
{
|
||||
return
|
||||
[
|
||||
new TaskTriggerInfo
|
||||
{
|
||||
Type = TaskTriggerInfoType.IntervalTrigger,
|
||||
IntervalTicks = TimeSpan.FromSeconds(Plugin.Instance!.Configuration.JFTAPlaylistsSyncTaskInterval).Ticks
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
private sealed class PlaylistItemAction
|
||||
{
|
||||
public PlaylistItemAction(
|
||||
int index,
|
||||
string youtubeId,
|
||||
CustomPlaylistAction action,
|
||||
int positionsToMove,
|
||||
string name)
|
||||
{
|
||||
Index = index;
|
||||
YoutubeId = youtubeId;
|
||||
Action = action;
|
||||
PositionsToMove = positionsToMove;
|
||||
Name = name;
|
||||
}
|
||||
|
||||
public PlaylistItemAction(
|
||||
int index,
|
||||
string youtubeId,
|
||||
CustomPlaylistAction action,
|
||||
string name)
|
||||
{
|
||||
Index = index;
|
||||
YoutubeId = youtubeId;
|
||||
Action = action;
|
||||
Name = name;
|
||||
PositionsToMove = 1;
|
||||
}
|
||||
|
||||
public PlaylistItemAction(
|
||||
string youtubeId,
|
||||
string name)
|
||||
{
|
||||
YoutubeId = youtubeId;
|
||||
Action = CustomPlaylistAction.Remove;
|
||||
Name = name;
|
||||
PositionsToMove = 1;
|
||||
}
|
||||
|
||||
public int? Index { get; set; }
|
||||
|
||||
public string YoutubeId { get; set; }
|
||||
|
||||
public string Name { get; set; }
|
||||
|
||||
public CustomPlaylistAction Action { get; set; }
|
||||
|
||||
public int? PositionsToMove { get; set; }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,249 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Data.Enums;
|
||||
using Jellyfin.Plugin.TubeArchivistMetadata.TubeArchivist;
|
||||
using Jellyfin.Plugin.TubeArchivistMetadata.Utilities;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Entities.TV;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Model.Dto;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.Tasks;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Jellyfin.Plugin.TubeArchivistMetadata.Tasks
|
||||
{
|
||||
/// <summary>
|
||||
/// Task to sync Jellyfin playback progresses to TubeArchivist.
|
||||
/// </summary>
|
||||
public class JFToTubeArchivistProgressSyncTask : IScheduledTask
|
||||
{
|
||||
private readonly ILogger<Plugin> _logger;
|
||||
private readonly ILibraryManager _libraryManager;
|
||||
private readonly IUserManager _userManager;
|
||||
private readonly IUserDataManager _userDataManager;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="JFToTubeArchivistProgressSyncTask"/> class.
|
||||
/// </summary>
|
||||
/// <param name="logger">Logger.</param>
|
||||
/// <param name="libraryManager">Library manager.</param>
|
||||
/// <param name="userManager">User manager.</param>
|
||||
/// <param name="userDataManager">User data manager.</param>
|
||||
public JFToTubeArchivistProgressSyncTask(ILogger<Plugin> logger, ILibraryManager libraryManager, IUserManager userManager, IUserDataManager userDataManager)
|
||||
{
|
||||
_logger = logger;
|
||||
_libraryManager = libraryManager;
|
||||
_userManager = userManager;
|
||||
_userDataManager = userDataManager;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string Name => "JFToTubeArchivistProgressSyncTask";
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string Description => "This tasks syncs Jellyfin playback progresses to TubeArchivist";
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string Category => "TubeArchivistMetadata";
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string Key => "JFToTubeArchivistProgressSyncTask";
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task ExecuteAsync(IProgress<double> progress, CancellationToken cancellationToken)
|
||||
{
|
||||
progress.Report(0);
|
||||
if (Plugin.Instance!.Configuration.JFTAProgressSync)
|
||||
{
|
||||
var start = DateTime.Now;
|
||||
_logger.LogInformation("Starting Jellyfin->TubeArchivist playback progresses synchronization.");
|
||||
var taApi = TubeArchivistApi.GetInstance();
|
||||
var videosCount = 0;
|
||||
var jfUsername = Plugin.Instance!.Configuration.JFUsernameFrom;
|
||||
var user = _userManager.GetUserByName(jfUsername);
|
||||
if (user == null)
|
||||
{
|
||||
_logger.LogInformation("{Message}", $"Jellyfin user with username {jfUsername} not found");
|
||||
return;
|
||||
}
|
||||
|
||||
var items = _libraryManager.GetItemList(new InternalItemsQuery
|
||||
{
|
||||
Name = Plugin.Instance?.Configuration.CollectionTitle,
|
||||
IncludeItemTypes = new[] { BaseItemKind.CollectionFolder }
|
||||
});
|
||||
|
||||
var collectionItem = items.Count > 0 ? items[0] : null;
|
||||
|
||||
if (collectionItem == null)
|
||||
{
|
||||
var message = $"Collection '{Plugin.Instance?.Configuration.CollectionTitle}' not found.";
|
||||
_logger.LogCritical("{Message}", message);
|
||||
}
|
||||
else
|
||||
{
|
||||
var collection = (CollectionFolder)collectionItem;
|
||||
var channels = collection.GetChildren(user, false, new InternalItemsQuery
|
||||
{
|
||||
IncludeItemTypes = new[] { BaseItemKind.Series }
|
||||
});
|
||||
_logger.LogDebug("Analyzing collection {Id} with name {Name}", collectionItem.Id, collectionItem.Name);
|
||||
_logger.LogDebug("Found {Message} channels", channels.Count);
|
||||
|
||||
foreach (Series channel in channels)
|
||||
{
|
||||
var channelYTId = Utils.GetChannelNameFromPath(channel.Path);
|
||||
var years = channel.GetChildren(user, false, new InternalItemsQuery
|
||||
{
|
||||
IncludeItemTypes = new[] { BaseItemKind.Season }
|
||||
});
|
||||
_logger.LogDebug("Found {Years} years in channel {ChannelName}", years.Count, channel.Name);
|
||||
|
||||
foreach (Season year in years)
|
||||
{
|
||||
var videos = year.GetChildren(user, false, new InternalItemsQuery
|
||||
{
|
||||
IncludeItemTypes = new[] { BaseItemKind.Episode }
|
||||
});
|
||||
_logger.LogDebug("Found {Videos} videos in year {YearName} of the channel {ChannelName}", videos.Count, year.Name, channel.Name);
|
||||
videosCount += videos.Count;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_logger.LogDebug("Found a total of {VideosCount} videos", videosCount);
|
||||
|
||||
var processedVideosCount = 0;
|
||||
if (collectionItem == null)
|
||||
{
|
||||
var message = $"Collection '{Plugin.Instance?.Configuration.CollectionTitle}' not found.";
|
||||
_logger.LogCritical("{Message}", message);
|
||||
}
|
||||
else
|
||||
{
|
||||
var collection = (CollectionFolder)collectionItem;
|
||||
var channels = collection.GetChildren(user, false, new InternalItemsQuery
|
||||
{
|
||||
IncludeItemTypes = new[] { BaseItemKind.Series }
|
||||
});
|
||||
|
||||
foreach (Series channel in channels)
|
||||
{
|
||||
var channelYTId = Utils.GetChannelNameFromPath(channel.Path);
|
||||
var isChannelWatched = false;
|
||||
var isChannelCheckedForWatched = false;
|
||||
var years = channel.GetChildren(user, false, new InternalItemsQuery
|
||||
{
|
||||
IncludeItemTypes = new[] { BaseItemKind.Season }
|
||||
});
|
||||
|
||||
foreach (Season year in years)
|
||||
{
|
||||
var videos = year.GetChildren(user, false, new InternalItemsQuery
|
||||
{
|
||||
IncludeItemTypes = new[] { BaseItemKind.Episode }
|
||||
});
|
||||
videosCount += videos.Count;
|
||||
|
||||
foreach (Episode video in videos)
|
||||
{
|
||||
var videoYTId = Utils.GetVideoNameFromPath(video.Path);
|
||||
_logger.LogDebug("Current video extracted YouTube id: {VideoYtId}", videoYTId);
|
||||
HttpStatusCode statusCode;
|
||||
var channelItemData = _userDataManager.GetUserData(user, channel);
|
||||
|
||||
if (!isChannelCheckedForWatched && channel.IsPlayed(user, channelItemData))
|
||||
{
|
||||
var isChannelPlayed = channel.IsPlayed(user, channelItemData);
|
||||
statusCode = await taApi.SetWatchedStatus(channelYTId, isChannelPlayed).ConfigureAwait(true);
|
||||
if (statusCode != System.Net.HttpStatusCode.OK)
|
||||
{
|
||||
_logger.LogCritical("{Message}", $"POST /watched returned {statusCode} for channel {channel.Name} ({channelYTId}) with wacthed status {isChannelPlayed}");
|
||||
}
|
||||
else
|
||||
{
|
||||
isChannelWatched = true;
|
||||
}
|
||||
|
||||
isChannelCheckedForWatched = true;
|
||||
}
|
||||
|
||||
var videoItemData = _userDataManager.GetUserData(user, video);
|
||||
if (!isChannelWatched)
|
||||
{
|
||||
var isVideoPlayed = video.IsPlayed(user, videoItemData);
|
||||
var taVideo = await taApi.GetVideo(videoYTId).ConfigureAwait(true);
|
||||
if (taVideo != null)
|
||||
{
|
||||
var isTAVideoPlayed = taVideo?.Player.IsWatched ?? false;
|
||||
if (isTAVideoPlayed != isVideoPlayed)
|
||||
{
|
||||
statusCode = await taApi.SetWatchedStatus(videoYTId, isVideoPlayed).ConfigureAwait(true);
|
||||
if (statusCode != System.Net.HttpStatusCode.OK)
|
||||
{
|
||||
_logger.LogCritical("{Message}", $"POST /watched returned {statusCode} for video {video.Name} ({videoYTId}) with wacthed status {isVideoPlayed}");
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogInformation("Video {VideoId} watch status marked as {Status} in TubeArchivist", videoYTId, isVideoPlayed);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_logger.LogDebug("{Message}", isVideoPlayed);
|
||||
if (!isVideoPlayed)
|
||||
{
|
||||
var playbackProgress = _userDataManager.GetUserData(user, video)?.PlaybackPositionTicks / TimeSpan.TicksPerSecond;
|
||||
if (playbackProgress != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
statusCode = await taApi.SetProgress(videoYTId, playbackProgress.Value).ConfigureAwait(true);
|
||||
if (statusCode != System.Net.HttpStatusCode.OK)
|
||||
{
|
||||
_logger.LogCritical("{Message}", $"POST /video/{videoYTId}/progress returned {statusCode} for video {video.Name} with progress {progress} seconds");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogCritical("An exception occurred while calling POST /video/{VideoId}/progress for for video {VideoName} with progress {Progress} seconds: {ExceptionMessage}", videoYTId, videoYTId, playbackProgress.Value, ex.Message);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
processedVideosCount++;
|
||||
progress.Report(processedVideosCount * 100 / videosCount);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_logger.LogInformation("Time elapsed: {Time}", DateTime.Now - start);
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogInformation("Jellyfin->TubeArchivist playback synchronization is currently disabled.");
|
||||
}
|
||||
|
||||
progress.Report(100);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IEnumerable<TaskTriggerInfo> GetDefaultTriggers()
|
||||
{
|
||||
return
|
||||
[
|
||||
new TaskTriggerInfo
|
||||
{
|
||||
Type = TaskTriggerInfoType.StartupTrigger,
|
||||
},
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,209 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Data.Enums;
|
||||
using Jellyfin.Plugin.TubeArchivistMetadata.TubeArchivist;
|
||||
using Jellyfin.Plugin.TubeArchivistMetadata.Utilities;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Entities.TV;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Playlists;
|
||||
using MediaBrowser.Model.Dto;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.Playlists;
|
||||
using MediaBrowser.Model.Tasks;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Jellyfin.Plugin.TubeArchivistMetadata.Tasks
|
||||
{
|
||||
/// <summary>
|
||||
/// Task to sync TubeArchivist playlists to Jellyfin.
|
||||
/// </summary>
|
||||
public class TAToJellyfinPlaylistsSyncTask : IScheduledTask
|
||||
{
|
||||
private readonly ILogger<Plugin> _logger;
|
||||
private readonly ILibraryManager _libraryManager;
|
||||
private readonly IUserManager _userManager;
|
||||
private readonly IPlaylistManager _playlistManager;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="TAToJellyfinPlaylistsSyncTask"/> class.
|
||||
/// </summary>
|
||||
/// <param name="logger">Logger.</param>
|
||||
/// <param name="libraryManager">Library manager.</param>
|
||||
/// <param name="userManager">User manager.</param>
|
||||
/// <param name="playlistManager">Playlists manager.</param>
|
||||
public TAToJellyfinPlaylistsSyncTask(ILogger<Plugin> logger, ILibraryManager libraryManager, IUserManager userManager, IPlaylistManager playlistManager)
|
||||
{
|
||||
_logger = logger;
|
||||
_libraryManager = libraryManager;
|
||||
_userManager = userManager;
|
||||
_playlistManager = playlistManager;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string Name => "TAToJellyfinPlaylistsSyncTask";
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string Description => "This tasks syncs TubeArchivist playlists to Jellyfin";
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string Category => "TubeArchivistMetadata";
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string Key => "TAToJellyfinPlaylistsSyncTask";
|
||||
|
||||
private int CountTotalVideos(ISet<TubeArchivist.Playlist> taPlaylists)
|
||||
{
|
||||
var totalEntries = 0;
|
||||
foreach (var taPlaylist in taPlaylists)
|
||||
{
|
||||
totalEntries += taPlaylist.Entries.Count;
|
||||
}
|
||||
|
||||
return totalEntries;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task ExecuteAsync(IProgress<double> progress, CancellationToken cancellationToken)
|
||||
{
|
||||
progress.Report(0);
|
||||
if (Plugin.Instance!.Configuration.TAJFPlaylistsSync)
|
||||
{
|
||||
var start = DateTime.Now;
|
||||
_logger.LogInformation("Starting TubeArchivist->Jellyfin playlists synchronization.");
|
||||
var taApi = TubeArchivistApi.GetInstance();
|
||||
_logger.LogInformation("Getting TubeArchivist playlists");
|
||||
var taPlaylists = await taApi.GetPlaylists().ConfigureAwait(true);
|
||||
_logger.LogInformation("Received playlists:\n{Playlists}", taPlaylists);
|
||||
if (taPlaylists != null)
|
||||
{
|
||||
var totalVideosCount = CountTotalVideos(taPlaylists);
|
||||
var processedVideosCount = 0;
|
||||
foreach (var jfUsername in Plugin.Instance!.Configuration.GetJFUsernamesToArray())
|
||||
{
|
||||
var user = _userManager.GetUserByName(jfUsername);
|
||||
if (user == null)
|
||||
{
|
||||
_logger.LogInformation("{Message}", $"Jellyfin user with username {jfUsername} not found");
|
||||
continue;
|
||||
}
|
||||
|
||||
var userPlaylists = _playlistManager.GetPlaylists(user.Id).ToList();
|
||||
|
||||
if (Plugin.Instance!.Configuration.TAJFPlaylistsDelete)
|
||||
{
|
||||
var jfPlaylistsToDelete = userPlaylists.Where(up => !taPlaylists.Select(tp => tp.Id).Contains(Utils.GetTAPlaylistIdFromName(up.Name)));
|
||||
foreach (var jfPlaylistToDelete in jfPlaylistsToDelete)
|
||||
{
|
||||
_logger.LogInformation("Deleting Jellyfin playlist {PlaylistName}", jfPlaylistToDelete.Name);
|
||||
_libraryManager.DeleteItem(jfPlaylistToDelete, new DeleteOptions());
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var taPlaylist in taPlaylists)
|
||||
{
|
||||
var playlistName = taPlaylist.Type == PlaylistType.Regular ? $"{taPlaylist.Name} - {taPlaylist.Channel} ({taPlaylist.Id})" : $"{taPlaylist.Name} ({taPlaylist.Id})";
|
||||
var userPlaylist = userPlaylists.Where(up => up.Name == playlistName).FirstOrDefault();
|
||||
|
||||
var jfEntryIds = new List<Guid>();
|
||||
var currentPlaylistVideos = 0;
|
||||
foreach (var taEntry in taPlaylist.Entries)
|
||||
{
|
||||
currentPlaylistVideos++;
|
||||
var taEntryStr = $"{taEntry.Uploader} - {taEntry.Title} ({taEntry.YoutubeId}) in playlist {playlistName}";
|
||||
if (!taEntry.IsDownloaded)
|
||||
{
|
||||
_logger.LogInformation("The entry {TAEntry} was skipped because has not been downloaded by TubeArchivist", taEntryStr);
|
||||
continue;
|
||||
}
|
||||
|
||||
var entries = _libraryManager.GetItemList(new InternalItemsQuery
|
||||
{
|
||||
HasAnyProviderId = new Dictionary<string, string>()
|
||||
{
|
||||
{
|
||||
Constants.ProviderName, taEntry.YoutubeId
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
var jfEntry = entries.Count > 0 ? entries[0] : null;
|
||||
|
||||
if (jfEntry != null)
|
||||
{
|
||||
jfEntryIds.Add(jfEntry.Id);
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogWarning("The relative video for {TAEntry} was not found on Jellyfin", taEntryStr);
|
||||
}
|
||||
}
|
||||
|
||||
if (userPlaylist != null)
|
||||
{
|
||||
var updateRequest = new PlaylistUpdateRequest
|
||||
{
|
||||
Id = userPlaylist.Id,
|
||||
UserId = user.Id,
|
||||
Name = playlistName,
|
||||
Ids = jfEntryIds
|
||||
};
|
||||
|
||||
var result = _playlistManager.UpdatePlaylist(updateRequest);
|
||||
_logger.LogInformation("Updated playlist {PlaylistName} with id {Id}", playlistName, result.Id);
|
||||
}
|
||||
else
|
||||
{
|
||||
var creationRequest = new PlaylistCreationRequest
|
||||
{
|
||||
Name = playlistName,
|
||||
ItemIdList = jfEntryIds,
|
||||
MediaType = MediaType.Video,
|
||||
UserId = user.Id
|
||||
};
|
||||
|
||||
var result = await _playlistManager.CreatePlaylist(creationRequest).ConfigureAwait(true);
|
||||
_logger.LogInformation("Created playlist {PlaylistName} with id {Id}", playlistName, result.Id);
|
||||
}
|
||||
|
||||
processedVideosCount += currentPlaylistVideos;
|
||||
|
||||
if (totalVideosCount == 0)
|
||||
{
|
||||
progress.Report(100);
|
||||
}
|
||||
else
|
||||
{
|
||||
progress.Report(processedVideosCount * 100 / totalVideosCount);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_logger.LogInformation("Time elapsed: {Time}", DateTime.Now - start);
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogInformation("TubeArchivist->Jellyfin playlists synchronization is currently disabled.");
|
||||
}
|
||||
|
||||
progress.Report(100);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IEnumerable<TaskTriggerInfo> GetDefaultTriggers()
|
||||
{
|
||||
return
|
||||
[
|
||||
new TaskTriggerInfo
|
||||
{
|
||||
Type = TaskTriggerInfoType.IntervalTrigger,
|
||||
IntervalTicks = TimeSpan.FromSeconds(Plugin.Instance!.Configuration.TAJFPlaylistsSyncTaskInterval).Ticks
|
||||
},
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,227 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Data.Enums;
|
||||
using Jellyfin.Plugin.TubeArchivistMetadata.TubeArchivist;
|
||||
using Jellyfin.Plugin.TubeArchivistMetadata.Utilities;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Entities.TV;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Model.Dto;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.Tasks;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Jellyfin.Plugin.TubeArchivistMetadata.Tasks
|
||||
{
|
||||
/// <summary>
|
||||
/// Task to sync TubeArchivist playback progresses to Jellyfin.
|
||||
/// </summary>
|
||||
public class TAToJellyfinProgressSyncTask : IScheduledTask
|
||||
{
|
||||
private readonly ILogger<Plugin> _logger;
|
||||
private readonly ILibraryManager _libraryManager;
|
||||
private readonly IUserManager _userManager;
|
||||
private readonly IUserDataManager _userDataManager;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="TAToJellyfinProgressSyncTask"/> class.
|
||||
/// </summary>
|
||||
/// <param name="logger">Logger.</param>
|
||||
/// <param name="libraryManager">Library manager.</param>
|
||||
/// <param name="userManager">User manager.</param>
|
||||
/// <param name="userDataManager">User data manager.</param>
|
||||
public TAToJellyfinProgressSyncTask(ILogger<Plugin> logger, ILibraryManager libraryManager, IUserManager userManager, IUserDataManager userDataManager)
|
||||
{
|
||||
_logger = logger;
|
||||
_libraryManager = libraryManager;
|
||||
_userManager = userManager;
|
||||
_userDataManager = userDataManager;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string Name => "TAToJellyfinProgressSyncTask";
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string Description => "This tasks syncs TubeArchivist playback progresses to Jellyfin";
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string Category => "TubeArchivistMetadata";
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string Key => "TAToJellyfinProgressSyncTask";
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task ExecuteAsync(IProgress<double> progress, CancellationToken cancellationToken)
|
||||
{
|
||||
progress.Report(0);
|
||||
if (Plugin.Instance!.Configuration.TAJFProgressSync)
|
||||
{
|
||||
var start = DateTime.Now;
|
||||
_logger.LogInformation("Starting TubeArchivist->Jellyfin playback progresses synchronization.");
|
||||
var taApi = TubeArchivistApi.GetInstance();
|
||||
var videosCount = 0;
|
||||
foreach (var jfUsername in Plugin.Instance!.Configuration.GetJFUsernamesToArray())
|
||||
{
|
||||
var user = _userManager.GetUserByName(jfUsername);
|
||||
if (user == null)
|
||||
{
|
||||
_logger.LogInformation("{Message}", $"Jellyfin user with username {jfUsername} not found");
|
||||
continue;
|
||||
}
|
||||
|
||||
var items = _libraryManager.GetItemList(new InternalItemsQuery
|
||||
{
|
||||
Name = Plugin.Instance?.Configuration.CollectionTitle,
|
||||
IncludeItemTypes = new[] { BaseItemKind.CollectionFolder }
|
||||
});
|
||||
|
||||
var collectionItem = items.Count > 0 ? items[0] : null;
|
||||
|
||||
if (collectionItem == null)
|
||||
{
|
||||
var message = $"Collection '{Plugin.Instance?.Configuration.CollectionTitle}' not found.";
|
||||
_logger.LogCritical("{Message}", message);
|
||||
}
|
||||
else
|
||||
{
|
||||
var collection = (CollectionFolder)collectionItem;
|
||||
var channels = collection.GetChildren(user, false, new InternalItemsQuery
|
||||
{
|
||||
IncludeItemTypes = new[] { BaseItemKind.Series }
|
||||
});
|
||||
_logger.LogDebug("Analyzing collection {Id} with name {Name}", collectionItem.Id, collectionItem.Name);
|
||||
_logger.LogDebug("Found {Message} channels", channels.Count);
|
||||
|
||||
foreach (Series channel in channels)
|
||||
{
|
||||
var years = channel.GetChildren(user, false, new InternalItemsQuery
|
||||
{
|
||||
IncludeItemTypes = new[] { BaseItemKind.Season }
|
||||
});
|
||||
_logger.LogDebug("Found {Years} years in channel {ChannelName}", years.Count, channel.Name);
|
||||
|
||||
foreach (Season year in years)
|
||||
{
|
||||
var videos = year.GetChildren(user, false, new InternalItemsQuery
|
||||
{
|
||||
IncludeItemTypes = new[] { BaseItemKind.Episode }
|
||||
});
|
||||
_logger.LogDebug("Found {Videos} videos in year {YearName} of the channel {ChannelName}", videos.Count, year.Name, channel.Name);
|
||||
|
||||
videosCount += videos.Count;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_logger.LogDebug("Found a total of {VideosCount} videos", videosCount);
|
||||
|
||||
var processedVideosCount = 0;
|
||||
foreach (var jfUsername in Plugin.Instance!.Configuration.GetJFUsernamesToArray())
|
||||
{
|
||||
var user = _userManager.GetUserByName(jfUsername);
|
||||
if (user == null)
|
||||
{
|
||||
_logger.LogDebug("{Message}", $"Jellyfin user with username {jfUsername} not found");
|
||||
continue;
|
||||
}
|
||||
|
||||
var items = _libraryManager.GetItemList(new InternalItemsQuery
|
||||
{
|
||||
Name = Plugin.Instance?.Configuration.CollectionTitle,
|
||||
IncludeItemTypes = new[] { BaseItemKind.CollectionFolder }
|
||||
});
|
||||
|
||||
var collectionItem = items.Count > 0 ? items[0] : null;
|
||||
|
||||
if (collectionItem == null)
|
||||
{
|
||||
var message = $"Collection '{Plugin.Instance?.Configuration.CollectionTitle}' not found.";
|
||||
_logger.LogCritical("{Message}", message);
|
||||
}
|
||||
else
|
||||
{
|
||||
var collection = (CollectionFolder)collectionItem;
|
||||
var channels = collection.GetChildren(user, false, new InternalItemsQuery
|
||||
{
|
||||
IncludeItemTypes = new[] { BaseItemKind.Series }
|
||||
});
|
||||
|
||||
foreach (Series channel in channels)
|
||||
{
|
||||
var years = channel.GetChildren(user, false, new InternalItemsQuery
|
||||
{
|
||||
IncludeItemTypes = new[] { BaseItemKind.Season }
|
||||
});
|
||||
|
||||
foreach (Season year in years)
|
||||
{
|
||||
var videos = year.GetChildren(user, false, new InternalItemsQuery
|
||||
{
|
||||
IncludeItemTypes = new[] { BaseItemKind.Episode }
|
||||
});
|
||||
|
||||
foreach (Episode video in videos)
|
||||
{
|
||||
var playbackProgress = await taApi.GetProgress(Utils.GetVideoNameFromPath(video.Path)).ConfigureAwait(true);
|
||||
if (playbackProgress != null)
|
||||
{
|
||||
var userItemData = _userDataManager.GetUserData(user, video);
|
||||
var userUpdateData = new UpdateUserItemDataDto
|
||||
{
|
||||
PlaybackPositionTicks = playbackProgress.Position * TimeSpan.TicksPerSecond
|
||||
};
|
||||
|
||||
var taVideoInfo = await taApi.GetVideo(Utils.GetVideoNameFromPath(video.Path)).ConfigureAwait(true);
|
||||
if (taVideoInfo != null)
|
||||
{
|
||||
if (taVideoInfo.Player.IsWatched)
|
||||
{
|
||||
userUpdateData.Played = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
userUpdateData.Played = false;
|
||||
}
|
||||
}
|
||||
|
||||
_userDataManager.SaveUserData(user, video, userUpdateData, UserDataSaveReason.UpdateUserData);
|
||||
_logger.LogInformation("{Message}", $"Playback progress for video {video.Name} set to {userItemData?.PlaybackPositionTicks / TimeSpan.TicksPerSecond} seconds for user {jfUsername}.");
|
||||
_logger.LogInformation("{Message}", $"Watched status for video {video.Name} set to {userItemData?.Played} seconds for user {jfUsername}.");
|
||||
|
||||
processedVideosCount++;
|
||||
progress.Report(processedVideosCount * 100 / videosCount);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_logger.LogInformation("Time elapsed: {Time}", DateTime.Now - start);
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogInformation("TubeArchivist->Jellyfin playback synchronization is currently disabled.");
|
||||
}
|
||||
|
||||
progress.Report(100);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IEnumerable<TaskTriggerInfo> GetDefaultTriggers()
|
||||
{
|
||||
return
|
||||
[
|
||||
new TaskTriggerInfo
|
||||
{
|
||||
Type = TaskTriggerInfoType.IntervalTrigger,
|
||||
IntervalTicks = TimeSpan.FromSeconds(Plugin.Instance!.Configuration.TAJFProgressTaskInterval).Ticks
|
||||
},
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace Jellyfin.Plugin.TubeArchivistMetadata.TubeArchivist
|
||||
{
|
||||
/// <summary>
|
||||
/// A class representing TubeArchivist API video watched status.
|
||||
/// </summary>
|
||||
public class Watched
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Watched"/> class.
|
||||
/// </summary>
|
||||
/// <param name="id">Id of the video/channel/playlist.</param>
|
||||
/// <param name="isWatched">Watched status.</param>
|
||||
public Watched(string id, bool isWatched)
|
||||
{
|
||||
Id = id;
|
||||
IsWatched = isWatched;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the id of the video/channel/playlist.
|
||||
/// </summary>
|
||||
[JsonProperty(PropertyName = "id")]
|
||||
public string Id { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the item has been watched or not.
|
||||
/// </summary>
|
||||
[JsonProperty(PropertyName = "is_watched")]
|
||||
public bool IsWatched { get; }
|
||||
}
|
||||
}
|
||||
+9
-5
@@ -96,7 +96,7 @@ namespace Jellyfin.Plugin.TubeArchivistMetadata.TubeArchivist
|
||||
{
|
||||
Name = Name,
|
||||
SearchProviderName = Constants.ProviderName,
|
||||
ImageUrl = ThumbUrl,
|
||||
ImageUrl = Utils.HasImageUrl(ThumbUrl) ? ThumbUrl : null,
|
||||
ProviderIds = new Dictionary<string, string>() { { Constants.ProviderName, Id } }
|
||||
};
|
||||
}
|
||||
@@ -118,15 +118,19 @@ namespace Jellyfin.Plugin.TubeArchivistMetadata.TubeArchivist
|
||||
Constants.ProviderName, Id
|
||||
}
|
||||
},
|
||||
ImageInfos = new[]
|
||||
{
|
||||
ImageInfos = Utils.HasImageUrl(ThumbUrl) ?
|
||||
[
|
||||
new ItemImageInfo
|
||||
{
|
||||
Path = ThumbUrl,
|
||||
Type = ImageType.Primary
|
||||
}
|
||||
},
|
||||
Tags = this.Tags.ToArray<string>()
|
||||
|
||||
] :
|
||||
[],
|
||||
Tags = this.Tags != null ?
|
||||
this.Tags.ToArray<string>() :
|
||||
[]
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
using System.Collections.ObjectModel;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace Jellyfin.Plugin.TubeArchivistMetadata.TubeArchivist
|
||||
{
|
||||
/// <summary>
|
||||
/// A class representing pagination information from the TubeArchivist API.
|
||||
/// </summary>
|
||||
public class PaginationInfo
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="PaginationInfo"/> class.
|
||||
/// </summary>
|
||||
/// <param name="pageSize">The size of the page.</param>
|
||||
/// <param name="pageFrom">The starting page number.</param>
|
||||
/// <param name="prevPages">The collection of previous page numbers.</param>
|
||||
/// <param name="currentPage">The current page number.</param>
|
||||
/// <param name="maxHits">A value indicating whether the max hits have been reached.</param>
|
||||
/// <param name="parameters">The parameters used for pagination.</param>
|
||||
/// <param name="lastPage">The last page number.</param>
|
||||
/// <param name="nextPages">The collection of next page numbers.</param>
|
||||
/// <param name="totalHits">The total number of hits.</param>
|
||||
public PaginationInfo(
|
||||
int pageSize,
|
||||
int pageFrom,
|
||||
Collection<int> prevPages,
|
||||
int currentPage,
|
||||
bool maxHits,
|
||||
string parameters,
|
||||
int lastPage,
|
||||
Collection<int> nextPages,
|
||||
int totalHits)
|
||||
{
|
||||
this.PageSize = pageSize;
|
||||
this.PageFrom = pageFrom;
|
||||
this.PrevPages = prevPages;
|
||||
this.CurrentPage = currentPage;
|
||||
this.MaxHits = maxHits;
|
||||
this.Parameters = parameters;
|
||||
this.LastPage = lastPage;
|
||||
this.NextPages = nextPages;
|
||||
this.TotalHits = totalHits;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the page size.
|
||||
/// </summary>
|
||||
[JsonProperty(PropertyName = "page_size")]
|
||||
public int PageSize { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the page from.
|
||||
/// </summary>
|
||||
[JsonProperty(PropertyName = "page_from")]
|
||||
public int PageFrom { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the previous pages.
|
||||
/// </summary>
|
||||
[JsonProperty(PropertyName = "prev_pages")]
|
||||
public Collection<int> PrevPages { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the current page.
|
||||
/// </summary>
|
||||
[JsonProperty(PropertyName = "current_page")]
|
||||
public int CurrentPage { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the max hits have been reached.
|
||||
/// </summary>
|
||||
[JsonProperty(PropertyName = "max_hits")]
|
||||
public bool MaxHits { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the parameters.
|
||||
/// </summary>
|
||||
[JsonProperty(PropertyName = "params")]
|
||||
public string Parameters { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the last page.
|
||||
/// </summary>
|
||||
[JsonProperty(PropertyName = "last_page")]
|
||||
public int LastPage { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the next pages.
|
||||
/// </summary>
|
||||
[JsonProperty(PropertyName = "next_pages")]
|
||||
public Collection<int> NextPages { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the total hits.
|
||||
/// </summary>
|
||||
[JsonProperty(PropertyName = "total_hits")]
|
||||
public int TotalHits { get; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace Jellyfin.Plugin.TubeArchivistMetadata.TubeArchivist
|
||||
{
|
||||
/// <summary>
|
||||
/// A class representing TubeArchivist API ping response data.
|
||||
/// </summary>
|
||||
public class PingResponse
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="PingResponse"/> class.
|
||||
/// </summary>
|
||||
/// <param name="response">Response from the API ("pong").</param>
|
||||
/// <param name="user">TubeArchivist user id.</param>
|
||||
/// <param name="version">TubeArchivist version.</param>
|
||||
public PingResponse(
|
||||
string response,
|
||||
int user,
|
||||
string version)
|
||||
{
|
||||
this.Response = response;
|
||||
this.User = user;
|
||||
this.Version = version;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets response to the ping ("pong").
|
||||
/// </summary>
|
||||
[JsonProperty(PropertyName = "response")]
|
||||
public string Response { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets user id.
|
||||
/// </summary>
|
||||
[JsonProperty(PropertyName = "user")]
|
||||
public int User { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets TubeArchivist version.
|
||||
/// </summary>
|
||||
[JsonProperty(PropertyName = "version")]
|
||||
public string Version { get; }
|
||||
}
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace Jellyfin.Plugin.TubeArchivistMetadata.TubeArchivist
|
||||
{
|
||||
/// <summary>
|
||||
/// A class representing TubeArchivist API custom playlist creation request.
|
||||
/// </summary>
|
||||
public class CustomPlaylistCreation
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="CustomPlaylistCreation"/> class.
|
||||
/// </summary>
|
||||
/// <param name="playlistName">The name of the new custom playlist to create.</param>
|
||||
public CustomPlaylistCreation(string playlistName)
|
||||
{
|
||||
this.PlaylistName = playlistName;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the name of the new custom playlist to create.
|
||||
/// </summary>
|
||||
[JsonProperty(PropertyName = "playlist_name")]
|
||||
public string PlaylistName { get; set; }
|
||||
}
|
||||
}
|
||||
+72
@@ -0,0 +1,72 @@
|
||||
using System.Globalization;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace Jellyfin.Plugin.TubeArchivistMetadata.TubeArchivist
|
||||
{
|
||||
/// <summary>
|
||||
/// Enum representing the possible actions to execute on a TubeArchivist custom playlist video.
|
||||
/// </summary>
|
||||
public enum CustomPlaylistAction
|
||||
{
|
||||
/// <summary>
|
||||
/// Create a new custom playlist video.
|
||||
/// </summary>
|
||||
Create,
|
||||
|
||||
/// <summary>
|
||||
/// Removea custom playlist video.
|
||||
/// </summary>
|
||||
Remove,
|
||||
|
||||
/// <summary>
|
||||
/// Move a custom playlist video to the top of the list.
|
||||
/// </summary>
|
||||
Top,
|
||||
|
||||
/// <summary>
|
||||
/// Move a custom playlist video to the bottom of the list.
|
||||
/// </summary>
|
||||
Bottom,
|
||||
|
||||
/// <summary>
|
||||
/// Move a custom playlist video up in the list.
|
||||
/// </summary>
|
||||
Up,
|
||||
|
||||
/// <summary>
|
||||
/// Move a custom playlist video down in the list.
|
||||
/// </summary>
|
||||
Down
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A class representing TubeArchivist API custom playlist entry operation request.
|
||||
/// </summary>
|
||||
public class CustomPlaylistEntryAction
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="CustomPlaylistEntryAction"/> class.
|
||||
/// </summary>
|
||||
/// <param name="action">The standard action to create a new entry.</param>
|
||||
/// <param name="videoId">The entry YoutubeId.</param>
|
||||
public CustomPlaylistEntryAction(
|
||||
CustomPlaylistAction action,
|
||||
string videoId)
|
||||
{
|
||||
this.Action = action.ToString().ToLower(CultureInfo.CurrentCulture);
|
||||
this.VideoId = videoId;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the standard action to create a new entry.
|
||||
/// </summary>
|
||||
[JsonProperty(PropertyName = "action")]
|
||||
public string Action { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the entry YoutubeId.
|
||||
/// </summary>
|
||||
[JsonProperty(PropertyName = "video_id")]
|
||||
public string VideoId { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
using System.Collections.ObjectModel;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace Jellyfin.Plugin.TubeArchivistMetadata.TubeArchivist
|
||||
{
|
||||
/// <summary>
|
||||
/// Enum representing whether the playlist on TubeArchivist belongs from YouTube or is created by the user.
|
||||
/// </summary>
|
||||
public enum PlaylistType
|
||||
{
|
||||
/// <summary>
|
||||
/// Playlist belongs from YouTube.
|
||||
/// </summary>
|
||||
Regular,
|
||||
|
||||
/// <summary>
|
||||
/// Playlist created by the user.
|
||||
/// </summary>
|
||||
Custom
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A class representing TubeArchivist API playlist data.
|
||||
/// </summary>
|
||||
public class Playlist
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Playlist"/> class.
|
||||
/// </summary>
|
||||
/// <param name="isActive">URL of the channel banner image.</param>
|
||||
/// <param name="channel">Playlist channel name.</param>
|
||||
/// <param name="channelId">Playlist channel YouTube id.</param>
|
||||
/// <param name="description">Playlist description.</param>
|
||||
/// <param name="entries">Playlist videos.</param>
|
||||
/// <param name="id">Playlist YouTube id.</param>
|
||||
/// <param name="name">Playlist name.</param>
|
||||
/// <param name="thumbnailUrl">URL of the playlist thumbnail.</param>
|
||||
/// <param name="type">Type of the playlist. See <see cref="PlaylistType"/>.</param>
|
||||
public Playlist(
|
||||
bool isActive,
|
||||
string channel,
|
||||
string channelId,
|
||||
string description,
|
||||
Collection<PlaylistEntry> entries,
|
||||
string id,
|
||||
string name,
|
||||
string thumbnailUrl, // TODO: Check if settable on Jellyfin
|
||||
PlaylistType type)
|
||||
{
|
||||
this.IsActive = isActive;
|
||||
this.Channel = channel;
|
||||
this.ChannelId = channelId;
|
||||
this.Description = description;
|
||||
this.Entries = entries;
|
||||
this.Id = id;
|
||||
this.Name = name;
|
||||
this.ThumbnailUrl = thumbnailUrl;
|
||||
this.Type = type;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether the playlist is active or not.
|
||||
/// </summary>
|
||||
[JsonProperty(PropertyName = "playlist_active")]
|
||||
public bool IsActive { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the playlist channel name.
|
||||
/// </summary>
|
||||
[JsonProperty(PropertyName = "playlist_channel")]
|
||||
public string Channel { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the playlist channel YouTube id.
|
||||
/// </summary>
|
||||
[JsonProperty(PropertyName = "playlist_channel_id")]
|
||||
public string ChannelId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the playlist description.
|
||||
/// </summary>
|
||||
[JsonProperty(PropertyName = "playlist_description")]
|
||||
public string Description { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the playlist videos.
|
||||
/// </summary>
|
||||
[JsonProperty(PropertyName = "playlist_entries")]
|
||||
public Collection<PlaylistEntry> Entries { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the playlist YouTube id.
|
||||
/// </summary>
|
||||
[JsonProperty(PropertyName = "playlist_id")]
|
||||
public string Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the playlist name.
|
||||
/// </summary>
|
||||
[JsonProperty(PropertyName = "playlist_name")]
|
||||
public string Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the playlist thumbnail URL.
|
||||
/// </summary>
|
||||
[JsonProperty(PropertyName = "playlist_thumbnail")]
|
||||
public string ThumbnailUrl { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the playlist type.
|
||||
/// </summary>
|
||||
/// <value>One of the <see cref="PlaylistType"/> values indicating the playlist's type.</value>
|
||||
[JsonProperty(PropertyName = "playlist_type")]
|
||||
public PlaylistType Type { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Returns a string of the playlist's key fields for logging.
|
||||
/// </summary>
|
||||
/// <returns>A string containing the playlist id, name, and entries count.</returns>
|
||||
public override string ToString()
|
||||
{
|
||||
return "PlaylistId: " + this.Id +
|
||||
", Name: " + this.Name +
|
||||
", EntriesCount: " + this.Entries.Count;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
using ICU4N.Text;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace Jellyfin.Plugin.TubeArchivistMetadata.TubeArchivist
|
||||
{
|
||||
/// <summary>
|
||||
/// A class representing TubeArchivist API playlist entries data.
|
||||
/// </summary>
|
||||
public class PlaylistEntry
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="PlaylistEntry"/> class.
|
||||
/// </summary>
|
||||
/// <param name="youtubeId">Video YouTube id.</param>
|
||||
/// <param name="title">Video title.</param>
|
||||
/// <param name="uploader">Video uploader.</param>
|
||||
/// <param name="index">Video index in the playlist.</param>
|
||||
/// <param name="isDownloaded">A value indicating whether the video has been already downloaded or not.</param>
|
||||
public PlaylistEntry(
|
||||
string youtubeId,
|
||||
string title,
|
||||
string uploader,
|
||||
int index,
|
||||
bool isDownloaded)
|
||||
{
|
||||
this.YoutubeId = youtubeId;
|
||||
this.Title = title;
|
||||
this.Uploader = uploader;
|
||||
this.Index = index;
|
||||
this.IsDownloaded = isDownloaded;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the video YouTube id.
|
||||
/// </summary>
|
||||
[JsonProperty(PropertyName = "youtube_id")]
|
||||
public string YoutubeId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the video title.
|
||||
/// </summary>
|
||||
[JsonProperty(PropertyName = "title")]
|
||||
public string Title { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the video uploader.
|
||||
/// </summary>
|
||||
[JsonProperty(PropertyName = "uploader")]
|
||||
public string Uploader { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the video index in the playlist.
|
||||
/// </summary>
|
||||
[JsonProperty(PropertyName = "idx")]
|
||||
public int Index { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether the video has been already downloaded or not.
|
||||
/// </summary>
|
||||
[JsonProperty(PropertyName = "downloaded")]
|
||||
public bool IsDownloaded { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -10,5 +10,10 @@ namespace Jellyfin.Plugin.TubeArchivistMetadata.TubeArchivist
|
||||
/// Gets or sets the contained data object.
|
||||
/// </summary>
|
||||
public T? Data { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the pagination info object.
|
||||
/// </summary>
|
||||
public PaginationInfo? Paginate { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Data;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Plugin.TubeArchivistMetadata.Utilities;
|
||||
using MediaBrowser.Model.Playlists;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
@@ -64,7 +67,7 @@ namespace Jellyfin.Plugin.TubeArchivistMetadata.TubeArchivist
|
||||
/// <returns>A task.</returns>
|
||||
public async Task<Channel?> GetChannel(string channelId)
|
||||
{
|
||||
ResponseContainer<Channel>? channel = null;
|
||||
Channel? channel = null;
|
||||
|
||||
var channelsEndpoint = "/api/channel/";
|
||||
var url = new Uri(Utils.SanitizeUrl(Plugin.Instance?.Configuration.TubeArchivistUrl + channelsEndpoint + channelId));
|
||||
@@ -72,19 +75,19 @@ namespace Jellyfin.Plugin.TubeArchivistMetadata.TubeArchivist
|
||||
while (response.StatusCode == HttpStatusCode.Moved)
|
||||
{
|
||||
url = response.Headers.Location;
|
||||
_logger.LogInformation("{Message}", "Received redirect to: " + url);
|
||||
_logger.LogDebug("{Message}", "Received redirect to: " + url);
|
||||
response = await client.GetAsync(url).ConfigureAwait(true);
|
||||
}
|
||||
|
||||
_logger.LogInformation("{Message}", url + ": " + response.StatusCode);
|
||||
_logger.LogDebug("{Message}", url + ": " + response.StatusCode);
|
||||
|
||||
if (response.IsSuccessStatusCode)
|
||||
{
|
||||
string rawData = await response.Content.ReadAsStringAsync().ConfigureAwait(true);
|
||||
channel = JsonConvert.DeserializeObject<ResponseContainer<Channel>>(rawData);
|
||||
channel = JsonConvert.DeserializeObject<Channel>(rawData);
|
||||
}
|
||||
|
||||
return channel?.Data;
|
||||
return channel;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -94,7 +97,7 @@ namespace Jellyfin.Plugin.TubeArchivistMetadata.TubeArchivist
|
||||
/// <returns>A task.</returns>
|
||||
public async Task<Video?> GetVideo(string videoId)
|
||||
{
|
||||
ResponseContainer<Video>? video = null;
|
||||
Video? video = null;
|
||||
|
||||
var videosEndpoint = "/api/video/";
|
||||
var url = new Uri(Utils.SanitizeUrl(Plugin.Instance?.Configuration.TubeArchivistUrl + videosEndpoint + videoId));
|
||||
@@ -102,19 +105,235 @@ namespace Jellyfin.Plugin.TubeArchivistMetadata.TubeArchivist
|
||||
while (response.StatusCode == HttpStatusCode.Moved)
|
||||
{
|
||||
url = response.Headers.Location;
|
||||
_logger.LogInformation("{Message}", "Received redirect to: " + url);
|
||||
_logger.LogDebug("{Message}", "Received redirect to: " + url);
|
||||
response = await client.GetAsync(url).ConfigureAwait(true);
|
||||
}
|
||||
|
||||
_logger.LogDebug("{Message}", url + ": " + response.StatusCode);
|
||||
|
||||
if (response.IsSuccessStatusCode)
|
||||
{
|
||||
string rawData = await response.Content.ReadAsStringAsync().ConfigureAwait(true);
|
||||
_logger.LogDebug("{Message}", rawData);
|
||||
video = JsonConvert.DeserializeObject<Video>(rawData);
|
||||
}
|
||||
|
||||
return video;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates connection and authentication to TubeArchivist.
|
||||
/// </summary>
|
||||
/// <returns>The ping response.</returns>
|
||||
public async Task<PingResponse?> Ping()
|
||||
{
|
||||
PingResponse? pong = null;
|
||||
|
||||
var pingEndpoint = "/api/ping/";
|
||||
var url = new Uri(Utils.SanitizeUrl(Plugin.Instance!.Configuration.TubeArchivistUrl + pingEndpoint));
|
||||
var response = await client.GetAsync(url).ConfigureAwait(true);
|
||||
|
||||
while (response.StatusCode == HttpStatusCode.Moved)
|
||||
{
|
||||
url = response.Headers.Location;
|
||||
_logger.LogDebug("{Message}", "Received redirect to: " + url);
|
||||
response = await client.GetAsync(url).ConfigureAwait(true);
|
||||
}
|
||||
|
||||
_logger.LogDebug("{Message}", url + ": " + response.StatusCode);
|
||||
|
||||
if (response.IsSuccessStatusCode)
|
||||
{
|
||||
string rawData = await response.Content.ReadAsStringAsync().ConfigureAwait(true);
|
||||
pong = JsonConvert.DeserializeObject<PingResponse>(rawData);
|
||||
}
|
||||
|
||||
return pong;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sends video playback progress to TubeArchivist.
|
||||
/// </summary>
|
||||
/// <param name="videoId">Video id.</param>
|
||||
/// <param name="progress">Progress in seconds.</param>
|
||||
/// <returns>The response <see cref="HttpStatusCode"/>.</returns>
|
||||
public async Task<HttpStatusCode> SetProgress(string videoId, long progress)
|
||||
{
|
||||
var progressEndpoint = $"/api/video/{videoId}/progress/";
|
||||
var url = new Uri(Utils.SanitizeUrl(Plugin.Instance!.Configuration.TubeArchivistUrl + progressEndpoint));
|
||||
var body = JsonConvert.SerializeObject(new Progress(progress));
|
||||
|
||||
var response = await client.PostAsync(url, new StringContent(body, Encoding.UTF8, "application/json")).ConfigureAwait(true);
|
||||
|
||||
return response.StatusCode;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sends video playback progress to TubeArchivist.
|
||||
/// </summary>
|
||||
/// <param name="videoId">Video id.</param>
|
||||
/// <returns>Video playback progress data.</returns>
|
||||
public async Task<Progress?> GetProgress(string videoId)
|
||||
{
|
||||
Progress? progress = null;
|
||||
|
||||
var video = await GetVideo(videoId).ConfigureAwait(true);
|
||||
if (video != null)
|
||||
{
|
||||
progress = new Progress((long)video.Player.Position);
|
||||
_logger.LogDebug("{Message}", $"Retrieved progress {video.Player.Position}");
|
||||
}
|
||||
|
||||
return progress;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets video/channel/playlist as watched on TubeArchivist.
|
||||
/// </summary>
|
||||
/// <param name="itemId">Video/channel/playlist id.</param>
|
||||
/// <param name="isWatched">Whether the item has been watched or not.</param>
|
||||
/// <returns>The response <see cref="HttpStatusCode"/>.</returns>
|
||||
public async Task<HttpStatusCode> SetWatchedStatus(string itemId, bool isWatched)
|
||||
{
|
||||
var watchedEndpoint = $"/api/watched/";
|
||||
var url = new Uri(Utils.SanitizeUrl(Plugin.Instance!.Configuration.TubeArchivistUrl + watchedEndpoint));
|
||||
var body = JsonConvert.SerializeObject(new Watched(itemId, isWatched));
|
||||
|
||||
var response = await client.PostAsync(url, new StringContent(body, Encoding.UTF8, "application/json")).ConfigureAwait(true);
|
||||
|
||||
return response.StatusCode;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves the playlists from TubeArchivist.
|
||||
/// </summary>
|
||||
/// <returns>A task.</returns>
|
||||
public async Task<ISet<Playlist>?> GetPlaylists()
|
||||
{
|
||||
ResponseContainer<ISet<Playlist>?>? playlists = null;
|
||||
|
||||
var playlistsEndpoint = "/api/playlist/";
|
||||
var url = new Uri(Utils.SanitizeUrl(Plugin.Instance?.Configuration.TubeArchivistUrl + playlistsEndpoint));
|
||||
var response = await client.GetAsync(url).ConfigureAwait(true);
|
||||
while (response.StatusCode == HttpStatusCode.Moved)
|
||||
{
|
||||
url = response.Headers.Location;
|
||||
_logger.LogInformation("{Message}", "Received redirect to: " + url);
|
||||
response = await client.GetAsync(Utils.SanitizeUrl(Plugin.Instance?.Configuration.TubeArchivistUrl + url)).ConfigureAwait(true);
|
||||
}
|
||||
|
||||
_logger.LogInformation("{Message}", url + ": " + response.StatusCode);
|
||||
|
||||
if (response.IsSuccessStatusCode)
|
||||
{
|
||||
string rawData = await response.Content.ReadAsStringAsync().ConfigureAwait(true);
|
||||
video = JsonConvert.DeserializeObject<ResponseContainer<Video>>(rawData);
|
||||
playlists = JsonConvert.DeserializeObject<ResponseContainer<ISet<Playlist>?>>(rawData);
|
||||
if (playlists?.Paginate != null)
|
||||
{
|
||||
var lastPage = playlists.Paginate.LastPage;
|
||||
_logger.LogInformation("Pagination info: Current page {CurrentPage} / Last page {LastPage}, Total hits: {TotalHits}", playlists.Paginate.CurrentPage, playlists.Paginate.LastPage, playlists.Paginate.TotalHits);
|
||||
|
||||
while (playlists.Paginate.CurrentPage < lastPage)
|
||||
{
|
||||
var nextPage = playlists.Paginate.CurrentPage + 1;
|
||||
var pagedUrl = new Uri(Utils.SanitizeUrl(Plugin.Instance?.Configuration.TubeArchivistUrl + playlistsEndpoint + "?page=" + nextPage));
|
||||
response = await client.GetAsync(pagedUrl).ConfigureAwait(true);
|
||||
while (response.StatusCode == HttpStatusCode.Moved)
|
||||
{
|
||||
url = response.Headers.Location;
|
||||
_logger.LogInformation("{Message}", "Received redirect to: " + url);
|
||||
response = await client.GetAsync(Utils.SanitizeUrl(Plugin.Instance?.Configuration.TubeArchivistUrl + url)).ConfigureAwait(true);
|
||||
}
|
||||
|
||||
_logger.LogInformation("{Message}", pagedUrl + ": " + response.StatusCode);
|
||||
|
||||
if (response.IsSuccessStatusCode)
|
||||
{
|
||||
rawData = await response.Content.ReadAsStringAsync().ConfigureAwait(true);
|
||||
var nextPagePlaylists = JsonConvert.DeserializeObject<ResponseContainer<ISet<Playlist>?>>(rawData);
|
||||
if (nextPagePlaylists?.Data != null)
|
||||
{
|
||||
foreach (var playlist in nextPagePlaylists.Data)
|
||||
{
|
||||
playlists.Data?.Add(playlist);
|
||||
}
|
||||
}
|
||||
|
||||
if (nextPagePlaylists?.Paginate != null)
|
||||
{
|
||||
playlists.Paginate = nextPagePlaylists.Paginate;
|
||||
_logger.LogInformation("Pagination info: Current page {CurrentPage} / Last page {LastPage}, Total hits: {TotalHits}", playlists.Paginate.CurrentPage, playlists.Paginate.LastPage, playlists.Paginate.TotalHits);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogCritical("Failed to retrieve page {PageNumber} of playlists during pagination.", nextPage);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return video?.Data;
|
||||
return playlists?.Data;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new custom playlist.
|
||||
/// </summary>
|
||||
/// <param name="creationRequest">Playlist creation request.</param>
|
||||
/// <returns>The created <see cref="Playlist"/>.</returns>
|
||||
public async Task<Playlist?> CreateCustomPlaylist(CustomPlaylistCreation creationRequest)
|
||||
{
|
||||
Playlist? playlist = null;
|
||||
var customPlaylistEndpoint = $"/api/playlist/custom/";
|
||||
var url = new Uri(Utils.SanitizeUrl(Plugin.Instance!.Configuration.TubeArchivistUrl + customPlaylistEndpoint));
|
||||
var body = JsonConvert.SerializeObject(creationRequest);
|
||||
_logger.LogInformation("{Message}", body);
|
||||
|
||||
var response = await client.PostAsync(url, new StringContent(body, Encoding.UTF8, "application/json")).ConfigureAwait(true);
|
||||
_logger.LogInformation("POST {Message}", url + ": " + response.StatusCode);
|
||||
|
||||
if (response.IsSuccessStatusCode)
|
||||
{
|
||||
string rawData = await response.Content.ReadAsStringAsync().ConfigureAwait(true);
|
||||
playlist = JsonConvert.DeserializeObject<Playlist>(rawData);
|
||||
}
|
||||
|
||||
return playlist;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new custom playlist.
|
||||
/// </summary>
|
||||
/// <param name="playlistId">Playlist id.</param>
|
||||
/// <param name="entryAction">Playlist creation request.</param>
|
||||
/// <returns>The response <see cref="HttpStatusCode"/>.</returns>
|
||||
public async Task<HttpStatusCode> CustomPlaylistEntryAction(string playlistId, CustomPlaylistEntryAction entryAction)
|
||||
{
|
||||
var customPlaylistEndpoint = $"/api/playlist/custom/";
|
||||
var url = new Uri(Utils.SanitizeUrl(Plugin.Instance!.Configuration.TubeArchivistUrl + customPlaylistEndpoint + playlistId));
|
||||
var body = JsonConvert.SerializeObject(entryAction);
|
||||
_logger.LogDebug("CustomPlaylistEntryAction body: {Message}", body);
|
||||
|
||||
var response = await client.PostAsync(url, new StringContent(body, Encoding.UTF8, "application/json")).ConfigureAwait(true);
|
||||
_logger.LogDebug("Response code: {Message}", response.StatusCode);
|
||||
|
||||
return response.StatusCode;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deletes a playlist.
|
||||
/// </summary>
|
||||
/// <param name="playlistId">Playlist id.</param>
|
||||
/// <returns>Whether the playlist has been deleted successfully or not.</returns>
|
||||
public async Task<bool> DeletePlaylist(string playlistId)
|
||||
{
|
||||
var deletePlaylistEndpoint = $"/api/playlist/";
|
||||
var url = new Uri(Utils.SanitizeUrl(Plugin.Instance!.Configuration.TubeArchivistUrl + deletePlaylistEndpoint + playlistId));
|
||||
var response = await client.DeleteAsync(url).ConfigureAwait(true);
|
||||
_logger.LogDebug("Response code: {Message}", response.StatusCode);
|
||||
|
||||
return response.StatusCode == HttpStatusCode.NoContent;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace Jellyfin.Plugin.TubeArchivistMetadata.TubeArchivist
|
||||
{
|
||||
/// <summary>
|
||||
/// A class representing TubeArchivist API video player data.
|
||||
/// </summary>
|
||||
public class Player
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Player"/> class.
|
||||
/// </summary>
|
||||
/// <param name="duration">Duration of the video.</param>
|
||||
/// <param name="isWatched">Whether the video is marked as watched.</param>
|
||||
/// <param name="position">Video watched seconds.</param>
|
||||
public Player(long duration, bool isWatched, double position)
|
||||
{
|
||||
Duration = duration;
|
||||
IsWatched = isWatched;
|
||||
Position = position;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the duration of the video.
|
||||
/// </summary>
|
||||
[JsonProperty(PropertyName = "duration")]
|
||||
public long Duration { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the video is marked as watched.
|
||||
/// </summary>
|
||||
[JsonProperty(PropertyName = "watched")]
|
||||
public bool IsWatched { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the video watched seconds.
|
||||
/// </summary>
|
||||
[JsonProperty(PropertyName = "position")]
|
||||
public double Position { get; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace Jellyfin.Plugin.TubeArchivistMetadata.TubeArchivist
|
||||
{
|
||||
/// <summary>
|
||||
/// A class representing TubeArchivist API video progress data.
|
||||
/// </summary>
|
||||
public class Progress
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Progress"/> class.
|
||||
/// </summary>
|
||||
/// <param name="position">Playback progress in seconds.</param>
|
||||
/// <param name="youtubeId">Video YouTube id.</param>
|
||||
/// <param name="userId">Playback progress user id.</param>
|
||||
[JsonConstructor]
|
||||
public Progress(
|
||||
long position,
|
||||
string? youtubeId,
|
||||
long? userId)
|
||||
{
|
||||
YoutubeId = youtubeId;
|
||||
UserId = userId;
|
||||
Position = position;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Progress"/> class.
|
||||
/// </summary>
|
||||
/// <param name="position">Playback progress in seconds.</param>
|
||||
public Progress(long position)
|
||||
{
|
||||
Position = position;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the video YouTube id.
|
||||
/// </summary>
|
||||
[JsonProperty(PropertyName = "youtube_id")]
|
||||
public string? YoutubeId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets playback progress user id.
|
||||
/// </summary>
|
||||
[JsonProperty(PropertyName = "user_id")]
|
||||
public long? UserId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets video playback progress (in seconds).
|
||||
/// </summary>
|
||||
[JsonProperty(PropertyName = "position")]
|
||||
public long Position { get; set; }
|
||||
}
|
||||
}
|
||||
+22
-5
@@ -3,6 +3,7 @@ using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using Jellyfin.Plugin.TubeArchivistMetadata.Configuration;
|
||||
using Jellyfin.Plugin.TubeArchivistMetadata.Utilities;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Entities.TV;
|
||||
@@ -27,6 +28,7 @@ namespace Jellyfin.Plugin.TubeArchivistMetadata.TubeArchivist
|
||||
/// <param name="published">Video published date.</param>
|
||||
/// <param name="vidThumbUrl">Video thuumb image URL.</param>
|
||||
/// <param name="youtubeId">Video YouTube id.</param>
|
||||
/// <param name="player">Player info.</param>
|
||||
public Video(
|
||||
Channel channel,
|
||||
Collection<string> tags,
|
||||
@@ -34,7 +36,8 @@ namespace Jellyfin.Plugin.TubeArchivistMetadata.TubeArchivist
|
||||
string description,
|
||||
DateTime published,
|
||||
string vidThumbUrl,
|
||||
string youtubeId)
|
||||
string youtubeId,
|
||||
Player player)
|
||||
{
|
||||
this.Channel = channel;
|
||||
this.Tags = tags;
|
||||
@@ -43,6 +46,7 @@ namespace Jellyfin.Plugin.TubeArchivistMetadata.TubeArchivist
|
||||
this.Published = published;
|
||||
this.VidThumbUrl = vidThumbUrl;
|
||||
this.YoutubeId = youtubeId;
|
||||
Player = player;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -88,6 +92,12 @@ namespace Jellyfin.Plugin.TubeArchivistMetadata.TubeArchivist
|
||||
[JsonProperty(PropertyName = "youtube_id")]
|
||||
public string YoutubeId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Getsthe player related info.
|
||||
/// </summary>
|
||||
[JsonProperty(PropertyName = "player")]
|
||||
public Player Player { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Converts the TubeArchivist API video to a Jellyfin <see cref="RemoteSearchResult"/> object.
|
||||
/// </summary>
|
||||
@@ -99,7 +109,7 @@ namespace Jellyfin.Plugin.TubeArchivistMetadata.TubeArchivist
|
||||
Name = Title,
|
||||
SearchProviderName = Constants.ProviderName,
|
||||
ProductionYear = Published.Year,
|
||||
ImageUrl = VidThumbUrl,
|
||||
ImageUrl = Utils.HasImageUrl(VidThumbUrl) ? VidThumbUrl : null,
|
||||
PremiereDate = Published,
|
||||
ProviderIds = new Dictionary<string, string>() { { Constants.ProviderName, YoutubeId } }
|
||||
};
|
||||
@@ -117,6 +127,11 @@ namespace Jellyfin.Plugin.TubeArchivistMetadata.TubeArchivist
|
||||
Overview = Utils.FormatDescription(Description),
|
||||
SeasonName = Published.Year.ToString(CultureInfo.CurrentCulture),
|
||||
ParentIndexNumber = Published.Year,
|
||||
IndexNumber = Plugin.Instance?.Configuration?.EpisodeNumberingScheme switch
|
||||
{
|
||||
NumberingScheme.YYYYMMDD => (Published.Year * 10000) + (Published.Month * 100) + Published.Day,
|
||||
_ => null
|
||||
},
|
||||
SeriesName = Channel.Name,
|
||||
ProductionYear = Published.Year,
|
||||
PremiereDate = Published,
|
||||
@@ -127,14 +142,16 @@ namespace Jellyfin.Plugin.TubeArchivistMetadata.TubeArchivist
|
||||
Constants.ProviderName, YoutubeId
|
||||
}
|
||||
},
|
||||
ImageInfos = new[]
|
||||
{
|
||||
ImageInfos = Utils.HasImageUrl(VidThumbUrl) ?
|
||||
[
|
||||
new ItemImageInfo
|
||||
{
|
||||
Path = VidThumbUrl,
|
||||
Type = ImageType.Primary
|
||||
}
|
||||
},
|
||||
|
||||
] :
|
||||
[],
|
||||
Tags = this.Tags.ToArray<string>()
|
||||
};
|
||||
}
|
||||
@@ -1,4 +1,7 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
|
||||
namespace Jellyfin.Plugin.TubeArchivistMetadata.Utilities
|
||||
{
|
||||
@@ -7,45 +10,191 @@ namespace Jellyfin.Plugin.TubeArchivistMetadata.Utilities
|
||||
/// </summary>
|
||||
public static class Utils
|
||||
{
|
||||
private const string TAPlaylistIdRegex = @"^(.*)\((.*)\)$";
|
||||
private const string YTTAPlaylistNameFormatRegex = @"^(.*)\s\-\s(.*)\s\((.*)\)$";
|
||||
private const string TAPlaylistNameFormatRegex = @"^(.*)\s\((.*)\)$";
|
||||
|
||||
/// <summary>
|
||||
/// Sanitize the given URL.
|
||||
/// Sanitizes the given URL.
|
||||
/// </summary>
|
||||
/// <param name="inputUrl">An URL string.</param>
|
||||
/// <returns>The URL string without spaces, doubled slashes and with a trailing slash.</returns>
|
||||
public static string SanitizeUrl(string inputUrl)
|
||||
{
|
||||
// Extract the schema part
|
||||
Match schemaMatch = Regex.Match(inputUrl, @"^(?<schema>https?://)");
|
||||
if (string.IsNullOrWhiteSpace(inputUrl))
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
// Remove double slashes and spaces from the remaining part of the URL
|
||||
string cleanedPath = Regex.Replace(inputUrl.Substring(schemaMatch.Length), @"[/\s]+", "/");
|
||||
// Extract the schema part (http:// or https://)
|
||||
Match schemaMatch = Regex.Match(inputUrl, @"^(?<schema>https?://)", RegexOptions.IgnoreCase);
|
||||
|
||||
// If no schema found, treat whole string as the rest
|
||||
int schemaLength = schemaMatch.Success ? schemaMatch.Length : 0;
|
||||
|
||||
// Separate the main part from query (?) and fragment (#)
|
||||
string rest = inputUrl.Substring(schemaLength);
|
||||
string pathPart = rest;
|
||||
string queryAndFragment = string.Empty;
|
||||
|
||||
int qIndex = rest.IndexOf('?', StringComparison.Ordinal);
|
||||
int fIndex = rest.IndexOf('#', StringComparison.Ordinal);
|
||||
|
||||
int splitIndex = -1;
|
||||
if (qIndex >= 0 && fIndex >= 0)
|
||||
{
|
||||
splitIndex = Math.Min(qIndex, fIndex);
|
||||
}
|
||||
else if (qIndex >= 0)
|
||||
{
|
||||
splitIndex = qIndex;
|
||||
}
|
||||
else if (fIndex >= 0)
|
||||
{
|
||||
splitIndex = fIndex;
|
||||
}
|
||||
|
||||
if (splitIndex >= 0)
|
||||
{
|
||||
pathPart = rest.Substring(0, splitIndex);
|
||||
queryAndFragment = rest.Substring(splitIndex);
|
||||
}
|
||||
|
||||
// Remove double slashes and spaces from the path part
|
||||
string cleanedPath = Regex.Replace(pathPart, @"[/\s]+", "/");
|
||||
|
||||
// Remove slashes at the start
|
||||
cleanedPath = cleanedPath.TrimStart('/');
|
||||
|
||||
// Add a trailing slash if not already present
|
||||
cleanedPath = cleanedPath.TrimEnd('/') + "/";
|
||||
// Add a trailing slash only when there are no query or fragment parts
|
||||
if (string.IsNullOrEmpty(queryAndFragment))
|
||||
{
|
||||
cleanedPath = cleanedPath.TrimEnd('/') + "/";
|
||||
}
|
||||
|
||||
// Combine the schema and cleaned path
|
||||
string cleanedUrl = schemaMatch.Groups["schema"].Value + cleanedPath;
|
||||
// Combine the schema and cleaned path and re-append query/fragment
|
||||
string cleanedUrl = (schemaMatch.Success ? schemaMatch.Groups["schema"].Value : string.Empty) + cleanedPath + queryAndFragment;
|
||||
|
||||
return cleanedUrl;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Format episodes and series descriptions replacing newlines with br tags.
|
||||
/// Formats episodes and series descriptions replacing newlines with br tags.
|
||||
/// </summary>
|
||||
/// <param name="description">String to format.</param>
|
||||
/// <returns>A string with \n replaced by br tags.</returns>
|
||||
public static string FormatDescription(string description)
|
||||
{
|
||||
if (description == null)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
var maxLength = 500;
|
||||
if (Plugin.Instance != null)
|
||||
{
|
||||
maxLength = Plugin.Instance.Configuration.MaxDescriptionLength;
|
||||
}
|
||||
|
||||
return description.Replace("\n", "<br>", System.StringComparison.CurrentCulture).Substring(0, maxLength);
|
||||
if (description.Length > maxLength)
|
||||
{
|
||||
description = description.Substring(0, maxLength);
|
||||
}
|
||||
|
||||
description = description.Replace("\n", "<br>", System.StringComparison.CurrentCulture);
|
||||
|
||||
return description;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets video name from file path on the disk.
|
||||
/// </summary>
|
||||
/// <param name="path">File path on disk.</param>
|
||||
/// <returns>The video name.</returns>
|
||||
public static string GetVideoNameFromPath(string path)
|
||||
{
|
||||
return path.Split(DetectDirectorySeparator(path)).Last().Split(".").First();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets channel name from directory path on the disk.
|
||||
/// </summary>
|
||||
/// <param name="path">Directory path on disk.</param>
|
||||
/// <returns>The channel name.</returns>
|
||||
public static string GetChannelNameFromPath(string path)
|
||||
{
|
||||
return path.Split(DetectDirectorySeparator(path)).Last();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks whether the Jellyfin item belongs to the configured TubeArchivist collection.
|
||||
/// </summary>
|
||||
/// <param name="item">Jellyfin item.</param>
|
||||
/// <returns>True when the item belongs to the configured TubeArchivist collection.</returns>
|
||||
public static bool IsTubeArchivistItem(BaseItem item)
|
||||
{
|
||||
if (Plugin.Instance == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var topParent = item.GetTopParent();
|
||||
return string.Equals(topParent?.Name, Plugin.Instance.Configuration.CollectionTitle, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks whether a TubeArchivist image URL can be fetched safely.
|
||||
/// </summary>
|
||||
/// <param name="url">Image URL returned by TubeArchivist.</param>
|
||||
/// <returns>True when the image URL is non-empty.</returns>
|
||||
public static bool HasImageUrl(string? url)
|
||||
{
|
||||
return !string.IsNullOrWhiteSpace(url);
|
||||
}
|
||||
|
||||
private static char DetectDirectorySeparator(string path)
|
||||
{
|
||||
int backslashCount = path.Count(c => c == '\\');
|
||||
int forwardSlashCount = path.Count(c => c == '/');
|
||||
|
||||
if (backslashCount > forwardSlashCount)
|
||||
{
|
||||
return '\\'; // Windows directory separator
|
||||
}
|
||||
else
|
||||
{
|
||||
return '/'; // Unix directory separator
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the TubeArchivist playlist id from Jellyfin playlist name.
|
||||
/// </summary>
|
||||
/// <param name="playlistName">The Jellyfin playlist name.</param>
|
||||
/// <returns>The TubeArchvist playlist id.</returns>
|
||||
public static string? GetTAPlaylistIdFromName(string playlistName)
|
||||
{
|
||||
var regex = new Regex(TAPlaylistIdRegex);
|
||||
return regex.Match(playlistName).Groups[2].ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the TubeArchivist playlist name from Jellyfin playlist name.
|
||||
/// </summary>
|
||||
/// <param name="playlistName">The Jellyfin playlist name.</param>
|
||||
/// <returns>The TubeArchivist playlist name.</returns>
|
||||
public static string? GetTAPlaylistNameFromName(string playlistName)
|
||||
{
|
||||
var ytRegex = new Regex(YTTAPlaylistNameFormatRegex);
|
||||
var regex = new Regex(TAPlaylistNameFormatRegex);
|
||||
|
||||
var name = ytRegex.Match(playlistName).Groups[1].ToString();
|
||||
if (string.IsNullOrEmpty(name))
|
||||
{
|
||||
name = regex.Match(playlistName).Groups[1].ToString();
|
||||
}
|
||||
|
||||
return name;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,18 +1,24 @@
|
||||
<h1 align="center">Jellyfin TubeArchivist Plugin</h1>
|
||||
|
||||
<p align="center">
|
||||
<img alt="Plugin Banner" src="https://raw.githubusercontent.com/DarkFighterLuke/TubeArchivistMetadata/master/images/logo.png"/>
|
||||
<img alt="Plugin Banner" src="https://git.felixfoertsch.de/felixfoertsch/tubearchivist-jf-plugin/raw/branch/main/images/logo.png"/>
|
||||
<br/>
|
||||
</p>
|
||||
|
||||
> [!IMPORTANT]
|
||||
> Jellyfin release cycle has changed in the past few months and now it is shorter than before. The plugin supports only the latest Jellyfin release, in order to continue using this plugin with all the latest features you will need to upgrade your Jellyfin installation.
|
||||
> The same rule applies to TubeArchivist: the plugin is only guaranteed to work with the latest TubeArchivist version.
|
||||
|
||||
> [!WARNING]
|
||||
> Jellyfin 10.10 introduced a bug that prevents the plugin to correctly create Season folders by year. The bug has been finally solved on the Jellyfin codebase, but, until the next minor release, a manual build of the Jellyfin branch `release-10.10.z` is required in order to get the fix running.<br>
|
||||
> This is not a plugin bug, any issue opened about this bug will be immediately closed!
|
||||
|
||||
## About
|
||||
|
||||
<p>This plugin adds the metadata provider for <a href="https://www.tubearchivist.com/">TubeArchivist</a>.</p>
|
||||
<p>This plugin offers improved flexibility and native integration with Jellyfin compared to <a href="https://github.com/tubearchivist/tubearchivist-jf">tubearchivist-jf</a> solution.<br> This solution runs also more smoothly than the cited one since it is direcly integrated in Jellyfin and it is not based on strict folder paths on the disk.<br>
|
||||
Furthermore, it allows to configure a custom collection name.</p>
|
||||
<p>This plugin adds the metadata provider for <a href="https://www.tubearchivist.com/">TubeArchivist</a>, offering improved flexibility and native integration with Jellyfin compared to previous solutions.</p>
|
||||
|
||||
## How it works
|
||||
The media organization is the same used in [tubearchivist-jf](https://github.com/tubearchivist/tubearchivist-jf), so the collection will be a `Shows` collection, where each channel is a show and its videos are the episodes, organized in seasons by year.<br>
|
||||
The media organization is a `Shows` collection, where each channel is a show and its videos are the episodes, organized in seasons by year.<br>
|
||||
The plugin interacts with TubeArchivist APIs to fetch videos and channels metadata.
|
||||
|
||||
### Features
|
||||
@@ -21,32 +27,43 @@ The plugin interacts with TubeArchivist APIs to fetch videos and channels metada
|
||||
- Add images for videos (episodes), ie. thumb images
|
||||
- Add images for channels (shows), ie. thumb, tvart and banner images
|
||||
- Organize videos (episodes) by year (seasons)
|
||||
- Bidirectional playback progress synchronization
|
||||
- Bidirectional playlists synchronization
|
||||
|
||||
> [!WARNING]
|
||||
> Enabling synchronization in both directions you can run in race conditions and unexpected results.
|
||||
|
||||
## Installation
|
||||
### From official repository (recommended)
|
||||
1. Go to `Dashboard -> Plugins` and select the `Repositories` tab
|
||||
1. Go to `Dashboard -> Plugins` and click on the `Manage Repositories` button
|
||||
2. Add a new repository with the following details:
|
||||
- Repository name: `TubeArchivistMetadata`
|
||||
- Repository URL: `https://github.com/DarkFighterLuke/TubeArchivistMetadata/raw/master/manifest.json`
|
||||

|
||||
3. Go to the `Catalog` tab
|
||||
4. Find `TubeArchivistMetadata` in the `Metadata` section and install it
|
||||

|
||||
- Repository URL: `https://git.felixfoertsch.de/felixfoertsch/tubearchivist-jf-plugin/raw/branch/main/manifest.json`
|
||||

|
||||
|
||||
3. Go back to the catalog
|
||||
4. Find `TubeArchivistMetadata` and install it
|
||||

|
||||
5. Restart Jellyfin to apply the changes
|
||||
|
||||
|
||||
### From ZIP in GitHub releases
|
||||
1. Download the latest available release (`TubeArchivist-*.zip`) from the repository releases section
|
||||
1. Download the latest available release (`tubearchivistmetadata_*.zip`) from the repository releases section
|
||||
2. Extract the contained files in the `plugins/TubeArchivistMetadata` folder (you might need to create the folders) of your Jellyfin installation
|
||||
3. Restart Jellyfin to apply the changes
|
||||
3. Restart Jellyfin to apply the changes
|
||||
|
||||
## Configuration
|
||||
<p>This plugin requires that you have already an instance of TubeArchivist up and running.</p>
|
||||
Once installed, you have to configure the following parameters in the plugin configuration:
|
||||
- Collection display name
|
||||
- TubeArchivist instance address
|
||||
- TubeArchivist API key
|
||||
<ul>
|
||||
<li>Collection display name</li>
|
||||
<li>TubeArchivist instance address</li>
|
||||
<li>TubeArchivist API key</li>
|
||||
<li>Overviews length (channels and videos descriptions)</li>
|
||||
<li>Playback synchronization settings discussed in the <a href="#playback-synchronization">Playback synchronization</a> paragraph</li>
|
||||
</ul>
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
## Use the plugin
|
||||
<p>Using the plugin is very simple. Let's start from the beginning:</p>
|
||||
@@ -54,20 +71,70 @@ Once installed, you have to configure the following parameters in the plugin con
|
||||
_NOTE: If you are using Docker containers, it is important to mount the TubeArchivist media path into Jellyfin container as **read-only**, in order to avoid possible operations on the media files that will break TubeArchivist._ <br>
|
||||
1. Go to `Dashboard -> Libraries` and add a media library
|
||||
2. In the form select `Shows` as Content type, set a display name for the library and set the TubeArchivist media folder in the `Folders` section
|
||||

|
||||
3. Scrolling down, uncheck all metadata and image providers except `TubeArchivist`. (You won't find TubeArchivist in seasons providers, so just disable everything there)
|
||||
4. Save and come back to Home, you will see the newly added library. Jellyfin will have executed the metadata fetching for you after the collection creation and then you will see the metadata and the images of channels and videos
|
||||

|
||||
|
||||
4. Scrolling down, uncheck all metadata and image providers except `TubeArchivist`. (You won't find TubeArchivist in seasons providers, so just disable everything there)
|
||||
5. Save and come back to Home, you will see the newly added library. Jellyfin will have executed the metadata fetching for you after the collection creation and then you will see the metadata and the images of channels and videos
|
||||
|
||||
|
||||
## Synchronization
|
||||
This plugin has different bidirectional sycnhronization features, that can be configured in the specific section in the plugin configuration page:
|
||||

|
||||
|
||||
### Playback synchronization
|
||||
<p>Starting from v1.3.1 this plugin offers playback progress and watched status bidirectional synchronization, but you can choose to enable only a one way synchronization (Jellyfin -> TubeArchivist or TubeArchivist -> Jellyfin) too.</p>
|
||||
|
||||
#### Jellyfin -> TubeArchivist playback synchronization
|
||||
<p>This kind of synchronization is done listening for progress and watched status changes while playing the videos for the specified users.<br>Furthermore, there is a task that runs at Jellyfin startup to synchronize the whole library.</p>
|
||||
<p>In the text field you can specify one Jellyfin username to synchronize data of to TubeArchivist.</p>
|
||||
|
||||
#### TubeArchivist -> Jellyfin playback synchronization
|
||||
<p>This kind of synchronization is done using a Jellyfin scheduled task that regularly synchronizes data from TubeArchivist API to Jellyfin.</p>
|
||||
<p>In the text field you can specify one or more Jellyfin usernames to update data for.</p>
|
||||
|
||||
### Playlists synchronization
|
||||
<p>Starting from v.1.4.1 this plugin offers playlists bidirectional synchronization, but you can choose to enable only a one way synchronization (Jellyfin -> TubeArchivist or TubeArchivist -> Jellyfin) too.</p>
|
||||
|
||||
#### Jellyfin -> TubeArchivist playlists synchronization
|
||||
<p>There is a task that retrieves playlists and recreates them on TubeArchivist with the videos in the same order. Please note that playlists can also have videos not beloging from TubeArchivist, they will be simply ignored, so you won't find them on TubeArchivist playlist.</p>
|
||||
<p>It is present also a setting to automatically delete playlists from TubeArchivist when they are no more available on Jellyfin.</p>
|
||||
|
||||
#### TubeArchivist -> Jellyfin playlists synchronization
|
||||
<p>There is a task that retrieves playlists from TubeArchivist and recreates them on Jellyfin with videos in the same order.</p>
|
||||
<p>It is present, also in this case, a setting to automatically delete playlists from Jellyfin when they are no more present on TubeArchivist, but beware that the will be deleted also if they contain videos not beloning to TubeArchivist.</p>
|
||||
|
||||
> [!CAUTION]
|
||||
> Pay attention when you enable the automatic deletion options, be sure that is your wanted behavior, especially when playlists contain also other videos not belonging from TubeArchivist, playlists removed won't be available again, there's no undo!
|
||||
|
||||
|
||||
## Tasks intervals
|
||||
<p>Since many of the feature are implemented as background tasks periodically executing, in the `Tasks intervals` section you will find the settings to adjust this period in seconds.<br>
|
||||
Keep in mind that Jellyfin lowest accepted period is of 1 minute (60 seconds) and the lower is the interval the higher will be the resources consuption on your system.</p>
|
||||
<p>Here are the configurable intervals:</p>
|
||||
|
||||

|
||||
|
||||
## Episode numbering
|
||||
|
||||
<p>There are different ways to number the episodes as they are configured in Jellyfin.<br>
|
||||
This changes the number after E in S--E-- (for example S2024E100 for episode number 100 of season 2024).</p>
|
||||
|
||||

|
||||
|
||||
The options correlate with:
|
||||
- Default - leave the numbering to what Jellyfin does by default (this is what the plugin has always done)
|
||||
- YYYYMMDD - numbers the episode by the year, month, day (e.g. 20250804 for a video published on the 4th of August 2025)
|
||||
|
||||
## Build
|
||||
|
||||
1. To build this plugin you will need [.Net 6.x](https://dotnet.microsoft.com/download/dotnet/6.0).
|
||||
1. To build this plugin you will need [.Net 9.x](https://dotnet.microsoft.com/download/dotnet/9.0).
|
||||
|
||||
2. Build plugin with following command
|
||||
```
|
||||
dotnet publish Jellyfin.Plugin.TubeArchivistMetadata --configuration Release --output bin
|
||||
$ dotnet publish Jellyfin.Plugin.TubeArchivistMetadata --configuration Release --output bin
|
||||
```
|
||||
|
||||
3. Place the dll-file in the `plugins/TubeArchivistMetadata` folder (you might need to create the folders) of your Jellyfin installation
|
||||
3. Place the dll-file in the `plugins/tubearchivist-jf-plugin` folder (you might need to create the folders) of your Jellyfin installation
|
||||
|
||||
## License
|
||||
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio Version 17
|
||||
VisualStudioVersion = 17.0.31903.59
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Jellyfin.Plugin.TubeArchivistMetadata", "Jellyfin.Plugin.TubeArchivistMetadata\Jellyfin.Plugin.TubeArchivistMetadata.csproj", "{7A112ACB-79E8-4FC9-BCF6-B3AC849DA2D4}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
Release|Any CPU = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{7A112ACB-79E8-4FC9-BCF6-B3AC849DA2D4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{7A112ACB-79E8-4FC9-BCF6-B3AC849DA2D4}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{7A112ACB-79E8-4FC9-BCF6-B3AC849DA2D4}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{7A112ACB-79E8-4FC9-BCF6-B3AC849DA2D4}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
+5
-5
@@ -1,9 +1,10 @@
|
||||
---
|
||||
name: "TubeArchivistMetadata"
|
||||
guid: "dc97d0c6-28b0-4242-afb4-5833ae1b3715"
|
||||
version: "1.2.1.0"
|
||||
targetAbi: "10.8.0.0"
|
||||
framework: "net6.0"
|
||||
imageUrl: https://git.felixfoertsch.de/felixfoertsch/tubearchivist-jf-plugin/raw/branch/main/images/logo.png
|
||||
version: "1.4.5.0"
|
||||
targetAbi: "10.11.0.0"
|
||||
framework: "net9.0"
|
||||
overview: "Metadata for your TubeArchivist library on Jellyfin"
|
||||
description: >
|
||||
TubeArchivistMetadata is a plugin that automatically manages
|
||||
@@ -13,5 +14,4 @@ owner: "DarkFighterLuke"
|
||||
artifacts:
|
||||
- "Jellyfin.Plugin.TubeArchivistMetadata.dll"
|
||||
changelog: >
|
||||
Make maximum description length configurable
|
||||
Truncate descriptions to 500 characters
|
||||
Limit providers to TubeArchivist paths, skip empty image URLs, and fix playlist sync intervals
|
||||
|
||||
+156
-12
@@ -3,50 +3,194 @@
|
||||
"category": "Metadata",
|
||||
"guid": "dc97d0c6-28b0-4242-afb4-5833ae1b3715",
|
||||
"name": "TubeArchivistMetadata",
|
||||
"description": "A new and fully integrated way to manage metadata and organize your TubeArchivist library videos on Jellyfin.",
|
||||
"description": "TubeArchivistMetadata is a plugin that automatically manages metadata for videos downloaded with TubeArchivist from YouTube.\n",
|
||||
"owner": "DarkFighterLuke",
|
||||
"overview": "Metadata for your TubeArchivist library on Jellyfin",
|
||||
"imageUrl": "https://raw.githubusercontent.com/DarkFighterLuke/TubeArchivistMetadata/master/images/logo.png",
|
||||
"imageUrl": "https://git.felixfoertsch.de/felixfoertsch/tubearchivist-jf-plugin/raw/branch/main/images/logo.png",
|
||||
"versions": [
|
||||
{
|
||||
"version": "1.4.5.0",
|
||||
"changelog": "Limit providers to TubeArchivist paths, skip empty image URLs, and fix playlist sync intervals\n",
|
||||
"targetAbi": "10.11.0.0",
|
||||
"sourceUrl": "https://git.felixfoertsch.de/felixfoertsch/tubearchivist-jf-plugin/releases/download/v1.4.5/tubearchivistmetadata_1.4.5.0.zip",
|
||||
"checksum": "2b3c47c6468fac2c6e42a005f66630c6",
|
||||
"timestamp": "2026-07-01T06:20:00Z"
|
||||
},
|
||||
{
|
||||
"version": "1.4.4.0",
|
||||
"changelog": "Handle TubeArchivist playlists pagination\n",
|
||||
"targetAbi": "10.11.0.0",
|
||||
"sourceUrl": "https://github.com/tubearchivist/tubearchivist-jf-plugin/releases/download/v1.4.4/tubearchivistmetadata_1.4.4.0.zip",
|
||||
"checksum": "90f1d388f7363537df8287e996a96bbd",
|
||||
"timestamp": "2025-12-02T13:40:47Z"
|
||||
},
|
||||
{
|
||||
"version": "1.4.3.0",
|
||||
"changelog": "Introduce episode numbering alternative schema\n",
|
||||
"targetAbi": "10.11.0.0",
|
||||
"sourceUrl": "https://github.com/tubearchivist/tubearchivist-jf-plugin/releases/download/v1.4.3/tubearchivistmetadata_1.4.3.0.zip",
|
||||
"checksum": "297d4f15af1132939f43c905b1ad393e",
|
||||
"timestamp": "2025-11-13T19:31:49Z"
|
||||
},
|
||||
{
|
||||
"version": "1.4.2.0",
|
||||
"changelog": "Introduce episode numbering alternative schema\n",
|
||||
"targetAbi": "10.11.0.0",
|
||||
"sourceUrl": "https://github.com/tubearchivist/tubearchivist-jf-plugin/releases/download/v1.4.2/tubearchivistmetadata_1.4.2.0.zip",
|
||||
"checksum": "8df142d2f1f0c0dffa302a567b40ef84",
|
||||
"timestamp": "2025-10-23T20:27:32Z"
|
||||
},
|
||||
{
|
||||
"version": "1.4.1.0",
|
||||
"changelog": "Introduce bidirectional playlists synchronization\n",
|
||||
"targetAbi": "10.11.0.0",
|
||||
"sourceUrl": "https://github.com/tubearchivist/tubearchivist-jf-plugin/releases/download/v1.4.1/tubearchivistmetadata_1.4.1.0.zip",
|
||||
"checksum": "2dc0b9140472a4f783f093a7ee425493",
|
||||
"timestamp": "2025-10-22T07:00:27Z"
|
||||
},
|
||||
{
|
||||
"version": "1.4.0.0",
|
||||
"changelog": "Introduce bidirectional playlists synchronization\n",
|
||||
"targetAbi": "10.11.0.0",
|
||||
"sourceUrl": "https://github.com/tubearchivist/tubearchivist-jf-plugin/releases/download/v1.4.0/tubearchivistmetadata_1.4.0.0.zip",
|
||||
"checksum": "4b81ab60022b532a64bb5eeb7be67dcc",
|
||||
"timestamp": "2025-10-22T06:57:53Z"
|
||||
},
|
||||
{
|
||||
"version": "1.3.7.0",
|
||||
"changelog": "Fix JF->TA Sync\n",
|
||||
"targetAbi": "10.10.0.0",
|
||||
"sourceUrl": "https://github.com/tubearchivist/tubearchivist-jf-plugin/releases/download/v1.3.7/tubearchivistmetadata_1.3.7.0.zip",
|
||||
"checksum": "60d8e9f134912f907dccd30b531da4f6",
|
||||
"timestamp": "2025-05-27T18:36:55Z"
|
||||
},
|
||||
{
|
||||
"version": "1.3.6.0",
|
||||
"changelog": "Fix TA auth header\n",
|
||||
"targetAbi": "10.10.0.0",
|
||||
"sourceUrl": "https://github.com/tubearchivist/tubearchivist-jf-plugin/releases/download/v1.3.6/tubearchivistmetadata_1.3.6.0.zip",
|
||||
"checksum": "513cde64268867d0047009eb2d986054",
|
||||
"timestamp": "2025-03-10T22:01:13Z"
|
||||
},
|
||||
{
|
||||
"version": "1.3.5.0",
|
||||
"changelog": "Adapt to the new TA API changes Fix JF crash when setting watched status with TA offline\n",
|
||||
"targetAbi": "10.10.0.0",
|
||||
"sourceUrl": "https://github.com/tubearchivist/tubearchivist-jf-plugin/releases/download/v1.3.5/tubearchivistmetadata_1.3.5.0.zip",
|
||||
"checksum": "2536622c0ec73e6401860929f24849f7",
|
||||
"timestamp": "2025-03-10T17:47:43Z"
|
||||
},
|
||||
{
|
||||
"version": "1.3.4.0",
|
||||
"changelog": "Merge workaround for JF crashing when scanning Series pull request by wolffshots\n",
|
||||
"targetAbi": "10.10.0.0",
|
||||
"sourceUrl": "https://github.com/tubearchivist/tubearchivist-jf-plugin/releases/download/v1.3.4/tubearchivistmetadata_1.3.4.0.zip",
|
||||
"checksum": "1ee6041139a6fdc3cecc8b2a427c60a8",
|
||||
"timestamp": "2024-11-04T21:30:24Z"
|
||||
},
|
||||
{
|
||||
"version": "1.3.3.0",
|
||||
"changelog": "Add compatibility with Jellyfin 10.10\n",
|
||||
"targetAbi": "10.10.0.0",
|
||||
"sourceUrl": "https://github.com/tubearchivist/tubearchivist-jf-plugin/releases/download/v1.3.3/tubearchivistmetadata_1.3.3.0.zip",
|
||||
"checksum": "ee6431d8bdaf844672d167b6f3ee0a73",
|
||||
"timestamp": "2024-11-04T20:41:46Z"
|
||||
},
|
||||
{
|
||||
"version": "1.3.2.0",
|
||||
"changelog": "Add tasks progress status Adjust logs levels\n",
|
||||
"targetAbi": "10.9.1.0",
|
||||
"sourceUrl": "https://github.com/tubearchivist/tubearchivist-jf-plugin/releases/download/v1.3.2/tubearchivistmetadata_1.3.2.0.zip",
|
||||
"checksum": "6162235807535a5c2a7570276798e69e",
|
||||
"timestamp": "2024-09-14T13:41:58Z"
|
||||
},
|
||||
{
|
||||
"version": "1.3.1.0",
|
||||
"changelog": "Add support for Jellyfin 10.9.1\n",
|
||||
"targetAbi": "10.9.1.0",
|
||||
"sourceUrl": "https://github.com/tubearchivist/tubearchivist-jf-plugin/releases/download/v1.3.1/tubearchivistmetadata_1.3.1.0.zip",
|
||||
"checksum": "b0b526bbd1702970108e3b9a8c3709de",
|
||||
"timestamp": "2024-09-12T13:36:12Z"
|
||||
},
|
||||
{
|
||||
"version": "1.3.0.0",
|
||||
"changelog": "Add support for Jellyfin 10.9.1\n",
|
||||
"targetAbi": "10.9.1.0",
|
||||
"sourceUrl": "https://github.com/tubearchivist/tubearchivist-jf-plugin/releases/download/v1.3.0/tubearchivistmetadata_1.3.0.0.zip",
|
||||
"checksum": "e1fde6f75a10a5f17bc9cc5814949180",
|
||||
"timestamp": "2024-05-14T20:20:13Z"
|
||||
},
|
||||
{
|
||||
"version": "1.2.5.0",
|
||||
"changelog": "Ping the TA API to validate connection\n",
|
||||
"targetAbi": "10.8.0.0",
|
||||
"sourceUrl": "https://github.com/tubearchivist/tubearchivist-jf-plugin/releases/download/v1.2.5/tubearchivistmetadata_1.2.5.0.zip",
|
||||
"checksum": "e4a0d076ffd84c976ad66dde91dfb840",
|
||||
"timestamp": "2024-05-14T17:37:17Z"
|
||||
},
|
||||
{
|
||||
"version": "1.2.4.0",
|
||||
"changelog": "Fix issues with directory separators\n",
|
||||
"targetAbi": "10.8.0.0",
|
||||
"sourceUrl": "https://github.com/tubearchivist/tubearchivist-jf-plugin/releases/download/v1.2.4/tubearchivistmetadata_1.2.4.0.zip",
|
||||
"checksum": "b418a46d458fe28354580d70748b1ced",
|
||||
"timestamp": "2024-05-09T15:41:00Z"
|
||||
},
|
||||
{
|
||||
"version": "1.2.3.0",
|
||||
"changelog": "Add fix for unclosed HTML breaks\n",
|
||||
"targetAbi": "10.8.0.0",
|
||||
"sourceUrl": "https://github.com/tubearchivist/tubearchivist-jf-plugin/releases/download/v1.2.3/tubearchivistmetadata_1.2.3.0.zip",
|
||||
"checksum": "f5328a662444739eaa7c59855bfcb203",
|
||||
"timestamp": "2024-04-18T21:45:00Z"
|
||||
},
|
||||
{
|
||||
"version": "1.2.2.0",
|
||||
"changelog": "Fix bug on description length substringing\n",
|
||||
"targetAbi": "10.8.0.0",
|
||||
"sourceUrl": "https://github.com/tubearchivist/tubearchivist-jf-plugin/releases/download/v1.2.2/tubearchivistmetadata_1.2.2.0.zip",
|
||||
"checksum": "82d04546916e1ba76d56de28e648447a",
|
||||
"timestamp": "2024-03-15T10:12:00Z"
|
||||
},
|
||||
{
|
||||
"version": "1.2.1.0",
|
||||
"changelog": "Make maximum description length configurable\nTruncate descriptions to 500 characters\n",
|
||||
"targetAbi": "10.8.0.0",
|
||||
"sourceUrl": "https://github.com/DarkFighterLuke/TubeArchivistMetadata/releases/download/v1.2.1/TubeArchivistMetadata-1.2.1.zip",
|
||||
"sourceUrl": "https://github.com/tubearchivist/tubearchivist-jf-plugin/releases/download/v1.2.1/tubearchivistmetadata_1.2.1.0.zip",
|
||||
"checksum": "cf68d169956f166a6ee68a06192651c2",
|
||||
"timestamp": "2024-03-14 21:50:00"
|
||||
"timestamp": "2024-03-14T21:50:00Z"
|
||||
},
|
||||
{
|
||||
"version": "1.2.0.0",
|
||||
"changelog": "Add episodes and series overview\n",
|
||||
"targetAbi": "10.8.0.0",
|
||||
"sourceUrl": "https://github.com/DarkFighterLuke/TubeArchivistMetadata/releases/download/v1.2.0/TubeArchivistMetadata-1.2.0.zip",
|
||||
"sourceUrl": "https://github.com/tubearchivist/tubearchivist-jf-plugin/releases/download/v1.2.0/tubearchivistmetadata_1.2.0.0.zip",
|
||||
"checksum": "75812a4464bf68c43fb9dcdb74d924e3",
|
||||
"timestamp": "2024-03-14 20:00:00"
|
||||
"timestamp": "2024-03-14T20:00:00Z"
|
||||
},
|
||||
{
|
||||
"version": "1.1.1.0",
|
||||
"changelog": "Handle empty tags for videos and channels\nFix URL by removing trailing slash for images\n",
|
||||
"targetAbi": "10.8.0.0",
|
||||
"sourceUrl": "https://github.com/DarkFighterLuke/TubeArchivistMetadata/releases/download/v1.1.1/TubeArchivistMetadata-1.1.1.zip",
|
||||
"sourceUrl": "https://github.com/tubearchivist/tubearchivist-jf-plugin/releases/download/v1.1.1/tubearchivistmetadata_1.1.1.0.zip",
|
||||
"checksum": "84ce752ce662e834690512c88ba48664",
|
||||
"timestamp": "2024-03-14 16:35:00"
|
||||
"timestamp": "2024-03-14T16:35:00Z"
|
||||
},
|
||||
{
|
||||
"version": "1.1.0.0",
|
||||
"changelog": "Fix field name and add placeholders\nFix URL in logs after redirection\nAdd schema to TubeArchivist URL if not present in settings\nFix a particular case\nLog configuration settings at initialization\nSanitize URLs\n",
|
||||
"targetAbi": "10.8.0.0",
|
||||
"sourceUrl": "https://github.com/DarkFighterLuke/TubeArchivistMetadata/releases/download/v1.1.0/TubeArchivistMetadata-1.1.0.zip",
|
||||
"sourceUrl": "https://github.com/tubearchivist/tubearchivist-jf-plugin/releases/download/v1.1.0/tubearchivistmetadata_1.1.0.0.zip",
|
||||
"checksum": "de4b6562973b7f75b6afe3a441bb0e19",
|
||||
"timestamp": "2024-03-13 15:02:00"
|
||||
"timestamp": "2024-03-13T15:02:00Z"
|
||||
},
|
||||
{
|
||||
"version": "1.0.0.0",
|
||||
"changelog": "First release\n",
|
||||
"targetAbi": "10.8.0.0",
|
||||
"sourceUrl": "https://github.com/DarkFighterLuke/TubeArchivistMetadata/releases/download/v1.0.0/TubeArchivistMetadata-1.0.0.zip",
|
||||
"sourceUrl": "https://github.com/tubearchivist/tubearchivist-jf-plugin/releases/download/v1.0.0/tubearchivistmetadata_1.0.0.0.zip",
|
||||
"checksum": "444f8980671de494b1875ee7d1657bcf",
|
||||
"timestamp": "2024-03-11 16:02:00"
|
||||
"timestamp": "2024-03-11T16:02:00Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Executable
+59
@@ -0,0 +1,59 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Description: Build a deterministic Jellyfin plugin release zip.
|
||||
# Usage: ./scripts/package-release.sh
|
||||
|
||||
main() {
|
||||
local root
|
||||
root="$(git rev-parse --show-toplevel)"
|
||||
cd "$root"
|
||||
|
||||
local version
|
||||
version="$(awk -F'\"' '/^version:/ { print $2 }' build.yaml)"
|
||||
|
||||
local dll_path
|
||||
dll_path="Jellyfin.Plugin.TubeArchivistMetadata/bin/Release/net9.0/Jellyfin.Plugin.TubeArchivistMetadata.dll"
|
||||
if [[ ! -f "$dll_path" ]]; then
|
||||
echo "Missing release DLL: $dll_path" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
local output_dir
|
||||
output_dir="artifacts"
|
||||
mkdir -p "$output_dir"
|
||||
|
||||
local zip_path
|
||||
zip_path="$output_dir/tubearchivistmetadata_${version}.zip"
|
||||
rm -f "$zip_path"
|
||||
|
||||
local package_dir
|
||||
package_dir="$(mktemp -d)"
|
||||
trap 'rm -rf "'"$package_dir"'"' EXIT
|
||||
|
||||
cp "$dll_path" "$package_dir/Jellyfin.Plugin.TubeArchivistMetadata.dll"
|
||||
TZ=UTC touch -t 198001010000 "$package_dir/Jellyfin.Plugin.TubeArchivistMetadata.dll"
|
||||
(
|
||||
cd "$package_dir"
|
||||
TZ=UTC zip -X -q "$root/$zip_path" Jellyfin.Plugin.TubeArchivistMetadata.dll
|
||||
)
|
||||
|
||||
local entry_count
|
||||
entry_count="$(zipinfo -1 "$zip_path" | wc -l | tr -d ' ')"
|
||||
if [[ "$entry_count" != "1" ]]; then
|
||||
echo "Unexpected zip entry count: $entry_count" >&2
|
||||
zipinfo -1 "$zip_path" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
local entry_name
|
||||
entry_name="$(zipinfo -1 "$zip_path")"
|
||||
if [[ "$entry_name" != "Jellyfin.Plugin.TubeArchivistMetadata.dll" ]]; then
|
||||
echo "Unexpected zip entry: $entry_name" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
md5sum "$zip_path" | cut -d' ' -f1
|
||||
}
|
||||
|
||||
main "$@"
|
||||
Reference in New Issue
Block a user