Compare commits
2 Commits
c698879cda
...
b988cdf8db
| Author | SHA1 | Date | |
|---|---|---|---|
| b988cdf8db | |||
| 1dbeb724fc |
39
.gitignore
vendored
Normal file
39
.gitignore
vendored
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
# Build outputs
|
||||||
|
bin/
|
||||||
|
obj/
|
||||||
|
artifacts/
|
||||||
|
|
||||||
|
# User-specific files
|
||||||
|
*.user
|
||||||
|
*.rsuser
|
||||||
|
*.suo
|
||||||
|
*.userprefs
|
||||||
|
|
||||||
|
# IDE/editor files
|
||||||
|
.idea/
|
||||||
|
.vscode/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*~
|
||||||
|
|
||||||
|
# OS files
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# Logs and temp
|
||||||
|
*.log
|
||||||
|
*.tmp
|
||||||
|
*.temp
|
||||||
|
|
||||||
|
# .NET test and coverage artifacts
|
||||||
|
TestResults/
|
||||||
|
*.trx
|
||||||
|
*.coverage
|
||||||
|
*.coveragexml
|
||||||
|
coverage/
|
||||||
|
|
||||||
|
# NuGet
|
||||||
|
packages/
|
||||||
|
*.nupkg
|
||||||
|
*.snupkg
|
||||||
|
|
||||||
27
Api/NewReleasesController.cs
Normal file
27
Api/NewReleasesController.cs
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
using Jellyfin.Plugin.NewReleases.Models;
|
||||||
|
using Jellyfin.Plugin.NewReleases.Services;
|
||||||
|
using MediaBrowser.Controller.Library;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
|
namespace Jellyfin.Plugin.NewReleases.Api;
|
||||||
|
|
||||||
|
[ApiController]
|
||||||
|
[Route("Plugins/NewReleases")]
|
||||||
|
public sealed class NewReleasesController : ControllerBase
|
||||||
|
{
|
||||||
|
private readonly NewReleasesService _newReleasesService;
|
||||||
|
|
||||||
|
public NewReleasesController(ILibraryManager libraryManager)
|
||||||
|
{
|
||||||
|
_newReleasesService = new NewReleasesService(libraryManager);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("Movies")]
|
||||||
|
[ProducesResponseType(typeof(NewReleasesResponse), StatusCodes.Status200OK)]
|
||||||
|
public ActionResult<NewReleasesResponse> GetNewlyReleasedMovies()
|
||||||
|
{
|
||||||
|
var response = _newReleasesService.GetNewlyReleasedMovies();
|
||||||
|
return Ok(response);
|
||||||
|
}
|
||||||
|
}
|
||||||
17
Jellyfin.Plugin.NewReleases.csproj
Normal file
17
Jellyfin.Plugin.NewReleases.csproj
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<LangVersion>latest</LangVersion>
|
||||||
|
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Jellyfin.Controller" Version="10.10.2" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<FrameworkReference Include="Microsoft.AspNetCore.App" />
|
||||||
|
</ItemGroup>
|
||||||
|
</Project>
|
||||||
10
Models/NewReleaseMovieDto.cs
Normal file
10
Models/NewReleaseMovieDto.cs
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
namespace Jellyfin.Plugin.NewReleases.Models;
|
||||||
|
|
||||||
|
public sealed class NewReleaseMovieDto
|
||||||
|
{
|
||||||
|
public string Id { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public string Name { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public DateTime PremiereDate { get; set; }
|
||||||
|
}
|
||||||
8
Models/NewReleasesResponse.cs
Normal file
8
Models/NewReleasesResponse.cs
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
namespace Jellyfin.Plugin.NewReleases.Models;
|
||||||
|
|
||||||
|
public sealed class NewReleasesResponse
|
||||||
|
{
|
||||||
|
public int TotalRecordCount { get; set; }
|
||||||
|
|
||||||
|
public List<NewReleaseMovieDto> Items { get; set; } = [];
|
||||||
|
}
|
||||||
22
Plugin.cs
Normal file
22
Plugin.cs
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
using MediaBrowser.Common.Configuration;
|
||||||
|
using MediaBrowser.Common.Plugins;
|
||||||
|
using MediaBrowser.Model.Serialization;
|
||||||
|
|
||||||
|
namespace Jellyfin.Plugin.NewReleases;
|
||||||
|
|
||||||
|
public sealed class Plugin : BasePlugin<PluginConfiguration>
|
||||||
|
{
|
||||||
|
public static Plugin? Instance { get; private set; }
|
||||||
|
|
||||||
|
public Plugin(IApplicationPaths applicationPaths, IXmlSerializer xmlSerializer)
|
||||||
|
: base(applicationPaths, xmlSerializer)
|
||||||
|
{
|
||||||
|
Instance = this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string Name => "Jellyfin New Releases";
|
||||||
|
|
||||||
|
public override Guid Id => new("cc3f7cd7-e910-4f09-bf89-e9c7ba7fca77");
|
||||||
|
|
||||||
|
public override string Description => "Provides newly released movies based on PremiereDate.";
|
||||||
|
}
|
||||||
10
PluginConfiguration.cs
Normal file
10
PluginConfiguration.cs
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
using MediaBrowser.Model.Plugins;
|
||||||
|
|
||||||
|
namespace Jellyfin.Plugin.NewReleases;
|
||||||
|
|
||||||
|
public sealed class PluginConfiguration : BasePluginConfiguration
|
||||||
|
{
|
||||||
|
public int MaxItems { get; set; } = 20;
|
||||||
|
|
||||||
|
public int ReleaseWindowDays { get; set; } = 365;
|
||||||
|
}
|
||||||
52
Services/NewReleasesService.cs
Normal file
52
Services/NewReleasesService.cs
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
using Jellyfin.Plugin.NewReleases.Models;
|
||||||
|
using Jellyfin.Data.Enums;
|
||||||
|
using MediaBrowser.Controller.Entities;
|
||||||
|
using MediaBrowser.Controller.Library;
|
||||||
|
|
||||||
|
namespace Jellyfin.Plugin.NewReleases.Services;
|
||||||
|
|
||||||
|
public sealed class NewReleasesService
|
||||||
|
{
|
||||||
|
private const int DefaultMaxItems = 20;
|
||||||
|
private const int DefaultReleaseWindowDays = 365;
|
||||||
|
|
||||||
|
private readonly ILibraryManager _libraryManager;
|
||||||
|
|
||||||
|
public NewReleasesService(ILibraryManager libraryManager)
|
||||||
|
{
|
||||||
|
_libraryManager = libraryManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
public NewReleasesResponse GetNewlyReleasedMovies()
|
||||||
|
{
|
||||||
|
var configuration = Plugin.Instance?.Configuration ?? new PluginConfiguration();
|
||||||
|
var maxItems = configuration.MaxItems > 0 ? configuration.MaxItems : DefaultMaxItems;
|
||||||
|
var releaseWindowDays = configuration.ReleaseWindowDays > 0 ? configuration.ReleaseWindowDays : DefaultReleaseWindowDays;
|
||||||
|
var cutoffUtc = DateTime.UtcNow.AddDays(-releaseWindowDays);
|
||||||
|
|
||||||
|
var query = new InternalItemsQuery
|
||||||
|
{
|
||||||
|
IncludeItemTypes = [BaseItemKind.Movie],
|
||||||
|
Recursive = true
|
||||||
|
};
|
||||||
|
|
||||||
|
var items = _libraryManager.GetItemList(query)
|
||||||
|
.Where(x => x.PremiereDate.HasValue)
|
||||||
|
.Where(x => x.PremiereDate!.Value.ToUniversalTime() >= cutoffUtc)
|
||||||
|
.OrderByDescending(x => x.PremiereDate!.Value)
|
||||||
|
.Take(maxItems)
|
||||||
|
.Select(x => new NewReleaseMovieDto
|
||||||
|
{
|
||||||
|
Id = x.Id.ToString("N"),
|
||||||
|
Name = x.Name,
|
||||||
|
PremiereDate = x.PremiereDate!.Value.ToUniversalTime()
|
||||||
|
})
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
return new NewReleasesResponse
|
||||||
|
{
|
||||||
|
TotalRecordCount = items.Count,
|
||||||
|
Items = items
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
20
manifest.json
Normal file
20
manifest.json
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"guid": "cc3f7cd7-e910-4f09-bf89-e9c7ba7fca77",
|
||||||
|
"name": "Jellyfin New Releases",
|
||||||
|
"description": "Newly Released Movies based on PremiereDate.",
|
||||||
|
"overview": "Newly released movies based on PremiereDate.",
|
||||||
|
"owner": "your-username",
|
||||||
|
"category": "General",
|
||||||
|
"versions": [
|
||||||
|
{
|
||||||
|
"version": "1.0.0.0",
|
||||||
|
"changelog": "Initial release",
|
||||||
|
"targetAbi": "10.11.0.0",
|
||||||
|
"sourceUrl": "https://gitea.example.invalid/Jellyfin/plugins/releases/download/v1/Jellyfin.Plugin.NewReleases_1.0.0.0.zip",
|
||||||
|
"checksum": "a0d305168c315c0a0df28b8e1adad836",
|
||||||
|
"timestamp": "2026-03-23T15:24:20Z"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
157
scripts/package-new-releases-plugin.sh
Executable file
157
scripts/package-new-releases-plugin.sh
Executable file
@@ -0,0 +1,157 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
PLUGIN_NAME="Jellyfin.Plugin.NewReleases"
|
||||||
|
DEFAULT_VERSION="1.0.0.0"
|
||||||
|
DEFAULT_TARGET_ABI="10.11.0.0"
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<'EOF'
|
||||||
|
Usage:
|
||||||
|
./scripts/package-new-releases-plugin.sh [options]
|
||||||
|
|
||||||
|
Options:
|
||||||
|
--version <x.y.z.w> Plugin version (default: 1.0.0.0)
|
||||||
|
--target-abi <abi> Jellyfin ABI (default: 10.10.0.0)
|
||||||
|
--owner <owner> Owner for metadata (default: your-username)
|
||||||
|
--category <category> Category for metadata (default: General)
|
||||||
|
--out <dir> Output directory for the zip (default: ./artifacts)
|
||||||
|
--zip-name <name> Zip file name (default: Jellyfin.Plugin.NewReleases_<version>.zip)
|
||||||
|
--source-url <url> Direct URL to the zip on your host (required if --update-manifest)
|
||||||
|
--update-manifest Update ./manifest.json with sourceUrl/checksum/timestamp/targetAbi/version
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
- You must host the resulting zip somewhere Jellyfin can download (Gitea release asset URL, for example).
|
||||||
|
- After packaging, upload the zip and set manifest.json 'sourceUrl' to the direct zip URL.
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
VERSION="$DEFAULT_VERSION"
|
||||||
|
TARGET_ABI="$DEFAULT_TARGET_ABI"
|
||||||
|
OWNER="your-username"
|
||||||
|
CATEGORY="General"
|
||||||
|
OUT_DIR="./artifacts"
|
||||||
|
ZIP_NAME="${PLUGIN_NAME}_${VERSION}.zip"
|
||||||
|
UPDATE_MANIFEST="false"
|
||||||
|
|
||||||
|
SOURCE_URL=""
|
||||||
|
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--version) VERSION="$2"; shift 2 ;;
|
||||||
|
--target-abi) TARGET_ABI="$2"; shift 2 ;;
|
||||||
|
--owner) OWNER="$2"; shift 2 ;;
|
||||||
|
--category) CATEGORY="$2"; shift 2 ;;
|
||||||
|
--out) OUT_DIR="$2"; shift 2 ;;
|
||||||
|
--zip-name) ZIP_NAME="$2"; shift 2 ;;
|
||||||
|
--source-url) SOURCE_URL="$2"; shift 2 ;;
|
||||||
|
--update-manifest) UPDATE_MANIFEST="true"; shift 1 ;;
|
||||||
|
-h|--help) usage; exit 0 ;;
|
||||||
|
*) echo "Unknown arg: $1"; usage; exit 1 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ -z "$ZIP_NAME" ]]; then
|
||||||
|
ZIP_NAME="${PLUGIN_NAME}_${VERSION}.zip"
|
||||||
|
fi
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
|
||||||
|
|
||||||
|
DLL_PATH="${REPO_ROOT}/bin/Release/net8.0/${PLUGIN_NAME}.dll"
|
||||||
|
PDB_PATH="${REPO_ROOT}/bin/Release/net8.0/${PLUGIN_NAME}.pdb"
|
||||||
|
|
||||||
|
if [[ ! -f "$DLL_PATH" ]]; then
|
||||||
|
echo "Build output not found: $DLL_PATH"
|
||||||
|
echo "Run: dotnet build -c Release"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
mkdir -p "${REPO_ROOT}/artifacts"
|
||||||
|
if [[ "$OUT_DIR" == /* ]]; then
|
||||||
|
OUT_DIR_ABS="$OUT_DIR"
|
||||||
|
else
|
||||||
|
OUT_DIR_ABS="${REPO_ROOT}/${OUT_DIR#./}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
mkdir -p "${OUT_DIR_ABS}"
|
||||||
|
|
||||||
|
TMP_DIR="$(mktemp -d)"
|
||||||
|
cleanup() { rm -rf "$TMP_DIR"; }
|
||||||
|
trap cleanup EXIT
|
||||||
|
|
||||||
|
META_JSON_PATH="${TMP_DIR}/meta.json"
|
||||||
|
ZIP_PATH="${OUT_DIR_ABS}/${ZIP_NAME}"
|
||||||
|
|
||||||
|
TIMESTAMP="$(date -u +"%Y-%m-%dT%H:%M:%SZ")"
|
||||||
|
|
||||||
|
cp "$DLL_PATH" "${TMP_DIR}/"
|
||||||
|
if [[ -f "$PDB_PATH" ]]; then
|
||||||
|
cp "$PDB_PATH" "${TMP_DIR}/"
|
||||||
|
fi
|
||||||
|
|
||||||
|
cat > "$META_JSON_PATH" <<EOF
|
||||||
|
{
|
||||||
|
"category": "${CATEGORY}",
|
||||||
|
"changelog": "Initial release",
|
||||||
|
"description": "Newly Released Movies based on PremiereDate.",
|
||||||
|
"guid": "cc3f7cd7-e910-4f09-bf89-e9c7ba7fca77",
|
||||||
|
"name": "Jellyfin New Releases",
|
||||||
|
"overview": "Newly released movies based on PremiereDate.",
|
||||||
|
"owner": "${OWNER}",
|
||||||
|
"targetAbi": "${TARGET_ABI}",
|
||||||
|
"timestamp": "${TIMESTAMP}",
|
||||||
|
"version": "${VERSION}"
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
( cd "$TMP_DIR" && zip -q -r "$ZIP_PATH" . )
|
||||||
|
|
||||||
|
MD5="$(md5sum "$ZIP_PATH" | awk '{print $1}')"
|
||||||
|
|
||||||
|
echo "Created: $ZIP_PATH"
|
||||||
|
echo "MD5: $MD5"
|
||||||
|
echo ""
|
||||||
|
echo "Next steps:"
|
||||||
|
echo "1) Upload the zip to your Gitea so you have a direct download URL."
|
||||||
|
echo "2) Update ./manifest.json:"
|
||||||
|
echo " - sourceUrl: set to the direct zip URL"
|
||||||
|
echo " - checksum: set to the MD5 value above"
|
||||||
|
echo " - targetAbi/version/timestamp (if different) to match your choices"
|
||||||
|
|
||||||
|
if [[ "$UPDATE_MANIFEST" == "true" ]]; then
|
||||||
|
if [[ ! -f "${REPO_ROOT}/manifest.json" ]]; then
|
||||||
|
echo "manifest.json not found at ${REPO_ROOT}/manifest.json"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if [[ -z "$SOURCE_URL" ]]; then
|
||||||
|
echo "Update requested but --source-url not provided."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
python3 - <<PY
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
manifest_path = Path("${REPO_ROOT}") / "manifest.json"
|
||||||
|
data = json.loads(manifest_path.read_text(encoding="utf-8"))
|
||||||
|
if not isinstance(data, list) or len(data) == 0:
|
||||||
|
raise SystemExit("manifest.json must be a JSON array with at least one plugin entry.")
|
||||||
|
|
||||||
|
plugin = data[0]
|
||||||
|
versions = plugin.get("versions") or []
|
||||||
|
if not versions:
|
||||||
|
raise SystemExit("manifest.json plugin entry must include a non-empty 'versions' array.")
|
||||||
|
|
||||||
|
v0 = versions[0]
|
||||||
|
v0["version"] = "${VERSION}"
|
||||||
|
v0["targetAbi"] = "${TARGET_ABI}"
|
||||||
|
v0["sourceUrl"] = "${SOURCE_URL}"
|
||||||
|
v0["checksum"] = "${MD5}"
|
||||||
|
v0["timestamp"] = "${TIMESTAMP}"
|
||||||
|
|
||||||
|
manifest_path.write_text(json.dumps(data, indent=2) + "\\n", encoding="utf-8")
|
||||||
|
print("Updated manifest.json")
|
||||||
|
PY
|
||||||
|
fi
|
||||||
|
|
||||||
Reference in New Issue
Block a user