Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b5cd185b57 | |||
| 945f1ed6dd | |||
| 9433aff28a | |||
| 754d4abd45 | |||
| 5f4afcc28e | |||
| 42c02b33eb | |||
| 13553ef1c6 | |||
| ac2e4dac9c | |||
| 0bbb3c36cd | |||
| db0d9b115c | |||
| bc343dd0c8 | |||
| 9ed4869ece | |||
| c2eebbe1a9 | |||
| e198b0fb92 |
@@ -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.3.0</Version>
|
||||
<AssemblyVersion>1.4.3.0</AssemblyVersion>
|
||||
<FileVersion>1.4.3.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>
|
||||
|
||||
@@ -172,10 +172,17 @@ namespace Jellyfin.Plugin.TubeArchivistMetadata
|
||||
{
|
||||
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)
|
||||
try
|
||||
{
|
||||
Logger.LogCritical("{Message}", $"POST /video/{videoId}/progress returned {statusCode} for video {eventArgs.Item.Name} with progress {progress} seconds");
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
@@ -79,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
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
@@ -201,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;
|
||||
|
||||
@@ -109,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 } }
|
||||
};
|
||||
@@ -142,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,8 +1,7 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
|
||||
namespace Jellyfin.Plugin.TubeArchivistMetadata.Utilities
|
||||
{
|
||||
@@ -22,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;
|
||||
}
|
||||
@@ -88,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
|
||||
|
||||
+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.3.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: >
|
||||
Fix JF->TA watched status sync
|
||||
Limit providers to TubeArchivist paths, skip empty image URLs, and fix playlist sync intervals
|
||||
|
||||
+26
-2
@@ -6,8 +6,32 @@
|
||||
"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",
|
||||
@@ -170,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