Compare commits
56 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 | |||
| cf87c6e829 | |||
| d7f4f5cbcc | |||
| 6e8b9ca744 | |||
| b0ca3a49a5 | |||
| 4358072001 | |||
| 38ed5dd9e0 | |||
| ff55b9f517 | |||
| 93a2de5d46 | |||
| 5b62e4c001 | |||
| 3e95ca26aa | |||
| a58139eca0 | |||
| 5ab5224328 | |||
| d36d677179 | |||
| 784592b362 | |||
| 2f90d6a03e | |||
| af375987ee | |||
| ee683d5737 |
@@ -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
|
||||
@@ -1,86 +0,0 @@
|
||||
name: TubeArchivistMetadata Release
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- v*
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
name: Build & Release
|
||||
permissions:
|
||||
contents: write
|
||||
packages: read
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: master
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Get release version
|
||||
id: newtag
|
||||
uses: "WyriHaximus/github-action-get-previous-tag@v1"
|
||||
|
||||
- name: Setup .Net
|
||||
uses: actions/setup-dotnet@v1
|
||||
with:
|
||||
dotnet-version: 9.0.x
|
||||
|
||||
- name: Add Jellyfin GitHub Nuget registry to sources
|
||||
run: |
|
||||
dotnet nuget add source \
|
||||
--username DarkFighterLuke \
|
||||
--password ${{ secrets.GITHUB_TOKEN }} \
|
||||
--store-password-in-clear-text \
|
||||
--name github https://nuget.pkg.github.com/jellyfin/index.json
|
||||
|
||||
- name: Get version number only
|
||||
run: |
|
||||
TAG=${{ steps.newtag.outputs.tag }}
|
||||
echo "VERSION=${TAG#v}" >> $GITHUB_ENV
|
||||
|
||||
- name: Generate release message
|
||||
run: |
|
||||
# Read the YAML file and extract the value of the 'changelog' field
|
||||
changelog=$(awk '/changelog:/ {flag=1; next} /^[^ ]/ {flag=0} flag' build.yaml | awk '{$1=$1};1')
|
||||
|
||||
# Check if changelog contains multiple lines
|
||||
if [[ $(echo "$changelog" | wc -l) -gt 1 ]]; then
|
||||
changelog=$(echo "$changelog" | sed 's/^/* /')
|
||||
else
|
||||
changelog="* $changelog"
|
||||
fi
|
||||
|
||||
# Create release-changelog.md
|
||||
echo "Changelog:" > release-changelog.md
|
||||
echo "$changelog" >> release-changelog.md
|
||||
|
||||
|
||||
- name: Build Jellyfin Plugin
|
||||
uses: DarkFighterLuke/jellyfin-plugin-repository-manager@7a96768accc155ac7b597351b5033532f3c48173
|
||||
id: jprm
|
||||
with:
|
||||
dotnet-target: net9.0
|
||||
version: ${VERSION}
|
||||
update-manifest: "true"
|
||||
|
||||
- name: Create release
|
||||
uses: ncipollo/release-action@v1
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
artifacts: ${{ steps.jprm.outputs.artifact }}
|
||||
makeLatest: latest
|
||||
bodyFile: release-changelog.md
|
||||
tag: ${{ steps.newtag.outputs.tag }}
|
||||
allowUpdates: true
|
||||
|
||||
- name: Commit manifest.json
|
||||
uses: EndBug/add-and-commit@v9
|
||||
with:
|
||||
add: manifest.json
|
||||
commit: --signoff
|
||||
message: "Update manifest.json"
|
||||
pull: "--no-rebase -X ours origin master"
|
||||
@@ -1,7 +1,8 @@
|
||||
<Project>
|
||||
<PropertyGroup>
|
||||
<Version>1.4.1.0</Version>
|
||||
<AssemblyVersion>1.4.1.0</AssemblyVersion>
|
||||
<FileVersion>1.4.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,
|
||||
}
|
||||
@@ -186,6 +186,11 @@ namespace Jellyfin.Plugin.TubeArchivistMetadata.Configuration
|
||||
/// </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>
|
||||
|
||||
@@ -41,6 +41,18 @@
|
||||
<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>
|
||||
@@ -160,6 +172,7 @@
|
||||
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;
|
||||
@@ -181,6 +194,7 @@
|
||||
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;
|
||||
|
||||
@@ -162,21 +162,28 @@ namespace Jellyfin.Plugin.TubeArchivistMetadata
|
||||
return;
|
||||
}
|
||||
|
||||
if (Instance!.Configuration.JFTAProgressSync && eventArgs.Users.Any(u => Instance!.Configuration.JFUsernameFrom.Equals(u.Username, StringComparison.Ordinal)))
|
||||
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)
|
||||
)
|
||||
{
|
||||
BaseItem? season = LibraryManager.GetItemById(eventArgs.Item.ParentId);
|
||||
BaseItem? channel = LibraryManager.GetItemById(season!.ParentId);
|
||||
BaseItem? collection = LibraryManager.GetItemById(channel!.ParentId);
|
||||
if (collection?.Name.ToLower(CultureInfo.CurrentCulture) == Instance?.Configuration.CollectionTitle.ToLower(CultureInfo.CurrentCulture) && eventArgs.PlaybackPositionTicks != null)
|
||||
long progress = (long)eventArgs.PlaybackPositionTicks / TimeSpan.TicksPerSecond;
|
||||
var videoId = Utils.GetVideoNameFromPath(eventArgs.Item.Path);
|
||||
try
|
||||
{
|
||||
long progress = (long)eventArgs.PlaybackPositionTicks / TimeSpan.TicksPerSecond;
|
||||
var videoId = Utils.GetVideoNameFromPath(eventArgs.Item.Path);
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -188,16 +195,26 @@ namespace Jellyfin.Plugin.TubeArchivistMetadata
|
||||
return;
|
||||
}
|
||||
|
||||
var topParent = eventArgs.Item.GetTopParent();
|
||||
var user = _userManager.GetUserById(eventArgs.UserId);
|
||||
if (user == null)
|
||||
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;
|
||||
}
|
||||
|
||||
var userItemData = _userDataManager.GetUserData(user, eventArgs.Item);
|
||||
if (Configuration.JFTAProgressSync && user != null && Configuration.GetJFUsernamesToArray().Contains(user!.Username))
|
||||
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;
|
||||
|
||||
@@ -47,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)
|
||||
@@ -65,7 +65,7 @@ namespace Jellyfin.Plugin.TubeArchivistMetadata.Providers
|
||||
_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
|
||||
{
|
||||
@@ -87,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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,6 +50,11 @@ 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 = Utils.GetVideoNameFromPath(info.Path);
|
||||
var video = await taApi.GetVideo(videoTAId).ConfigureAwait(true);
|
||||
@@ -67,6 +72,7 @@ namespace Jellyfin.Plugin.TubeArchivistMetadata.Providers
|
||||
});
|
||||
result.HasMetadata = true;
|
||||
result.Item = video.ToEpisode();
|
||||
result.Item.Path = info.Path;
|
||||
result.Provider = Name;
|
||||
result.People = peopleInfo;
|
||||
}
|
||||
@@ -78,6 +84,10 @@ 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 = Utils.GetVideoNameFromPath(searchInfo.Path);
|
||||
|
||||
@@ -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 />
|
||||
@@ -62,10 +62,8 @@ namespace Jellyfin.Plugin.TubeArchivistMetadata.Providers
|
||||
var channel = await taApi.GetChannel(channelTAId).ConfigureAwait(true);
|
||||
_logger.LogDebug("{Message}", string.Format(CultureInfo.CurrentCulture, "Getting images for channel: {0} ({1})", channel?.Name, channelTAId));
|
||||
_logger.LogDebug("{Message}", "Thumb URI: " + channel?.ThumbUrl);
|
||||
_logger.LogDebug("{Message}", "TVArt URI: " + channel?.TvartUrl);
|
||||
_logger.LogDebug("{Message}", "Banner URI: " + channel?.BannerUrl);
|
||||
|
||||
if (channel != null)
|
||||
if (channel != null && Utils.HasImageUrl(channel.ThumbUrl))
|
||||
{
|
||||
list.Add(new RemoteImageInfo
|
||||
{
|
||||
@@ -73,18 +71,17 @@ 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.Banner,
|
||||
Url = channel.BannerUrl
|
||||
});
|
||||
|
||||
list.Add(new RemoteImageInfo
|
||||
{
|
||||
ProviderName = Name,
|
||||
@@ -93,6 +90,16 @@ namespace Jellyfin.Plugin.TubeArchivistMetadata.Providers
|
||||
});
|
||||
}
|
||||
|
||||
if (channel != null && Utils.HasImageUrl(channel.BannerUrl))
|
||||
{
|
||||
list.Add(new RemoteImageInfo
|
||||
{
|
||||
ProviderName = Name,
|
||||
Type = ImageType.Banner,
|
||||
Url = channel.BannerUrl
|
||||
});
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
@@ -105,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,6 +49,11 @@ 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 = Utils.GetChannelNameFromPath(info.Path);
|
||||
var channel = await taApi.GetChannel(channelTAId).ConfigureAwait(true);
|
||||
@@ -77,6 +82,10 @@ 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 = Utils.GetChannelNameFromPath(searchInfo.Path);
|
||||
|
||||
@@ -379,7 +379,7 @@ namespace Jellyfin.Plugin.TubeArchivistMetadata.Tasks
|
||||
new TaskTriggerInfo
|
||||
{
|
||||
Type = TaskTriggerInfoType.IntervalTrigger,
|
||||
IntervalTicks = TimeSpan.FromSeconds(Plugin.Instance!.Configuration.TAJFProgressTaskInterval).Ticks
|
||||
IntervalTicks = TimeSpan.FromSeconds(Plugin.Instance!.Configuration.JFTAPlaylistsSyncTaskInterval).Ticks
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
@@ -153,13 +153,13 @@ namespace Jellyfin.Plugin.TubeArchivistMetadata.Tasks
|
||||
foreach (Episode video in videos)
|
||||
{
|
||||
var videoYTId = Utils.GetVideoNameFromPath(video.Path);
|
||||
_logger.LogDebug("{VideoYtId}", videoYTId);
|
||||
_logger.LogDebug("Current video extracted YouTube id: {VideoYtId}", videoYTId);
|
||||
HttpStatusCode statusCode;
|
||||
var userItemData = _userDataManager.GetUserData(user, channel);
|
||||
var channelItemData = _userDataManager.GetUserData(user, channel);
|
||||
|
||||
if (!isChannelCheckedForWatched && channel.IsPlayed(user, userItemData))
|
||||
if (!isChannelCheckedForWatched && channel.IsPlayed(user, channelItemData))
|
||||
{
|
||||
var isChannelPlayed = channel.IsPlayed(user, userItemData);
|
||||
var isChannelPlayed = channel.IsPlayed(user, channelItemData);
|
||||
statusCode = await taApi.SetWatchedStatus(channelYTId, isChannelPlayed).ConfigureAwait(true);
|
||||
if (statusCode != System.Net.HttpStatusCode.OK)
|
||||
{
|
||||
@@ -173,13 +173,26 @@ namespace Jellyfin.Plugin.TubeArchivistMetadata.Tasks
|
||||
isChannelCheckedForWatched = true;
|
||||
}
|
||||
|
||||
var videoItemData = _userDataManager.GetUserData(user, video);
|
||||
if (!isChannelWatched)
|
||||
{
|
||||
var isVideoPlayed = video.IsPlayed(user, userItemData);
|
||||
statusCode = await taApi.SetWatchedStatus(videoYTId, isVideoPlayed).ConfigureAwait(true);
|
||||
if (statusCode != System.Net.HttpStatusCode.OK)
|
||||
var isVideoPlayed = video.IsPlayed(user, videoItemData);
|
||||
var taVideo = await taApi.GetVideo(videoYTId).ConfigureAwait(true);
|
||||
if (taVideo != null)
|
||||
{
|
||||
_logger.LogCritical("{Message}", $"POST /watched returned {statusCode} for video {video.Name} ({videoYTId}) with wacthed status {isVideoPlayed}");
|
||||
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);
|
||||
@@ -188,10 +201,17 @@ namespace Jellyfin.Plugin.TubeArchivistMetadata.Tasks
|
||||
var playbackProgress = _userDataManager.GetUserData(user, video)?.PlaybackPositionTicks / TimeSpan.TicksPerSecond;
|
||||
if (playbackProgress != null)
|
||||
{
|
||||
statusCode = await taApi.SetProgress(videoYTId, playbackProgress.Value).ConfigureAwait(true);
|
||||
if (statusCode != System.Net.HttpStatusCode.OK)
|
||||
try
|
||||
{
|
||||
_logger.LogCritical("{Message}", $"POST /video/{videoYTId}/progress returned {statusCode} for video {video.Name} with progress {progress} seconds");
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -170,7 +170,15 @@ namespace Jellyfin.Plugin.TubeArchivistMetadata.Tasks
|
||||
}
|
||||
|
||||
processedVideosCount += currentPlaylistVideos;
|
||||
progress.Report(processedVideosCount * 100 / totalVideosCount);
|
||||
|
||||
if (totalVideosCount == 0)
|
||||
{
|
||||
progress.Report(100);
|
||||
}
|
||||
else
|
||||
{
|
||||
progress.Report(processedVideosCount * 100 / totalVideosCount);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -193,7 +201,7 @@ namespace Jellyfin.Plugin.TubeArchivistMetadata.Tasks
|
||||
new TaskTriggerInfo
|
||||
{
|
||||
Type = TaskTriggerInfoType.IntervalTrigger,
|
||||
IntervalTicks = TimeSpan.FromSeconds(Plugin.Instance!.Configuration.TAJFProgressTaskInterval).Ticks
|
||||
IntervalTicks = TimeSpan.FromSeconds(Plugin.Instance!.Configuration.TAJFPlaylistsSyncTaskInterval).Ticks
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
@@ -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,14 +118,16 @@ namespace Jellyfin.Plugin.TubeArchivistMetadata.TubeArchivist
|
||||
Constants.ProviderName, Id
|
||||
}
|
||||
},
|
||||
ImageInfos = new[]
|
||||
{
|
||||
ImageInfos = Utils.HasImageUrl(ThumbUrl) ?
|
||||
[
|
||||
new ItemImageInfo
|
||||
{
|
||||
Path = ThumbUrl,
|
||||
Type = ImageType.Primary
|
||||
}
|
||||
},
|
||||
|
||||
] :
|
||||
[],
|
||||
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; }
|
||||
}
|
||||
}
|
||||
@@ -112,5 +112,16 @@ namespace Jellyfin.Plugin.TubeArchivistMetadata.TubeArchivist
|
||||
/// <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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -219,7 +219,7 @@ namespace Jellyfin.Plugin.TubeArchivistMetadata.TubeArchivist
|
||||
{
|
||||
url = response.Headers.Location;
|
||||
_logger.LogInformation("{Message}", "Received redirect to: " + url);
|
||||
response = await client.GetAsync(url).ConfigureAwait(true);
|
||||
response = await client.GetAsync(Utils.SanitizeUrl(Plugin.Instance?.Configuration.TubeArchivistUrl + url)).ConfigureAwait(true);
|
||||
}
|
||||
|
||||
_logger.LogInformation("{Message}", url + ": " + response.StatusCode);
|
||||
@@ -228,6 +228,50 @@ namespace Jellyfin.Plugin.TubeArchivistMetadata.TubeArchivist
|
||||
{
|
||||
string rawData = await response.Content.ReadAsStringAsync().ConfigureAwait(true);
|
||||
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 playlists?.Data;
|
||||
|
||||
@@ -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;
|
||||
@@ -108,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 } }
|
||||
};
|
||||
@@ -126,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,
|
||||
@@ -136,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,6 +1,7 @@
|
||||
using System.IO;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
|
||||
namespace Jellyfin.Plugin.TubeArchivistMetadata.Utilities
|
||||
{
|
||||
@@ -20,20 +21,59 @@ namespace Jellyfin.Plugin.TubeArchivistMetadata.Utilities
|
||||
/// <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;
|
||||
}
|
||||
@@ -45,6 +85,11 @@ namespace Jellyfin.Plugin.TubeArchivistMetadata.Utilities
|
||||
/// <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)
|
||||
{
|
||||
@@ -54,9 +99,10 @@ namespace Jellyfin.Plugin.TubeArchivistMetadata.Utilities
|
||||
if (description.Length > maxLength)
|
||||
{
|
||||
description = description.Substring(0, maxLength);
|
||||
description = description.Replace("\n", "<br>", System.StringComparison.CurrentCulture);
|
||||
}
|
||||
|
||||
description = description.Replace("\n", "<br>", System.StringComparison.CurrentCulture);
|
||||
|
||||
return description;
|
||||
}
|
||||
|
||||
@@ -80,6 +126,32 @@ namespace Jellyfin.Plugin.TubeArchivistMetadata.Utilities
|
||||
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 == '\\');
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<h1 align="center">Jellyfin TubeArchivist Plugin</h1>
|
||||
|
||||
<p align="center">
|
||||
<img alt="Plugin Banner" src="https://raw.githubusercontent.com/tubearchivist/tubearchivist-jf-plugin/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>
|
||||
|
||||
@@ -38,7 +38,7 @@ The plugin interacts with TubeArchivist APIs to fetch videos and channels metada
|
||||
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/tubearchivist/tubearchivist-jf-plugin/raw/master/manifest.json`
|
||||
- Repository URL: `https://git.felixfoertsch.de/felixfoertsch/tubearchivist-jf-plugin/raw/branch/main/manifest.json`
|
||||

|
||||
|
||||
3. Go back to the catalog
|
||||
@@ -77,24 +77,53 @@ _NOTE: If you are using Docker containers, it is important to mount the TubeArch
|
||||
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
|
||||
|
||||
|
||||
## 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>
|
||||
## Synchronization
|
||||
This plugin has different bidirectional sycnhronization features, that can be configured in the specific section in the plugin configuration page:
|
||||

|
||||
|
||||
### Jellyfin->TubeArchivist synchronization
|
||||
### 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 plugin configuration you will find these settings:</p>
|
||||
|
||||

|
||||
<p>In the text field you can specify one Jellyfin username to synchronize data of to TubeArchivist.</p>
|
||||
|
||||
### TubeArchivist->Jellyfin synchronization
|
||||
#### 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 plugin configuration you will find these settings:</p>
|
||||
<p>In the text field you can specify one or more Jellyfin usernames to update data for.</p>
|
||||
|
||||

|
||||
<p>In the first text field you can specify one or more Jellyfin usernames to update data for.<br>
|
||||
In the second field you can specify the interval in seconds the task should run at, so that you can choose according to your system requirements. The lower is the interval the higher will be the resources consuption on your system.</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
|
||||
|
||||
|
||||
+3
-3
@@ -1,8 +1,8 @@
|
||||
---
|
||||
name: "TubeArchivistMetadata"
|
||||
guid: "dc97d0c6-28b0-4242-afb4-5833ae1b3715"
|
||||
imageUrl: https://raw.githubusercontent.com/tubearchivist/tubearchivist-jf-plugin/master/images/logo.png
|
||||
version: "1.4.1.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"
|
||||
@@ -14,4 +14,4 @@ owner: "DarkFighterLuke"
|
||||
artifacts:
|
||||
- "Jellyfin.Plugin.TubeArchivistMetadata.dll"
|
||||
changelog: >
|
||||
Introduce bidirectional playlists synchronization
|
||||
Limit providers to TubeArchivist paths, skip empty image URLs, and fix playlist sync intervals
|
||||
|
||||
+45
-5
@@ -6,15 +6,55 @@
|
||||
"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/tubearchivist/tubearchivist-jf-plugin/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": "Align compatibility with Jellyfin 10.11.x\n",
|
||||
"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": "7afb9abd4361ee991c1b3eae2a8d4cec",
|
||||
"timestamp": "2025-10-21T21:34:51Z"
|
||||
"checksum": "4b81ab60022b532a64bb5eeb7be67dcc",
|
||||
"timestamp": "2025-10-22T06:57:53Z"
|
||||
},
|
||||
{
|
||||
"version": "1.3.7.0",
|
||||
@@ -154,4 +194,4 @@
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
]
|
||||
|
||||
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