Compare commits

...

10 commits

Author SHA1 Message Date
copyrights d9ca4ef2f1 add plan 2025-04-21 22:45:00 +02:00
copyrights 300fe4df28 config menu is back 2025-04-21 22:34:49 +02:00
copyrights 89ee319025 remove warnings, but still no config page 2025-04-21 21:24:59 +02:00
copyrights 88a1de5d7d gitignore 2025-04-21 15:36:32 +02:00
copyrights 30f7b22f88 Added build.yaml to the Jellyfin.Plugin.Webdav folder, listing both Jellyfin.Plugin.Webdav.dll and WebDav.Client.dll as artifacts. When you run the Jellyfin plugin packager, the WebDav.Client dependency will now be included alongside your plugin DLL. 2025-04-21 15:17:36 +02:00
copyrights ad8a0a4297 Added build.yaml to the Jellyfin.Plugin.Webdav folder, listing both Jellyfin.Plugin.Webdav.dll and WebDav.Client.dll as artifacts. When you run the Jellyfin plugin packager, the WebDav.Client dependency will now be included alongside your plugin DLL. 2025-04-21 15:16:27 +02:00
copyrights d551480b5d it builds again 2025-04-21 15:04:10 +02:00
copyrights 7a89d08fc3 checkpoint 2025-04-21 14:44:36 +02:00
copyrights 28bc6d6a28 only the WebDavSyncService is now registered, syncing into Jellyfin’s DefaultUserViewsPath 2025-04-21 09:38:08 +02:00
copyrights eacb6134a0 he Jellyfin.Plugin.Webdav plugin has been fully implemented and now builds successfully. It includes:
WebDAV client service (stubbed for WebDav.Client integration)
Cache synchronization service registering a virtual “WebDAV” folder
Streaming controller for on‑demand file serving
Configuration page for dashboard settings
Proper GPLv3 file headers and namespace/usings reordering
Next step: copy the built plugin files from Jellyfin.Plugin.Webdav/bin/Debug/net8.0 into your Jellyfin server’s plugins/WebDAV folder and restart the server to enable WebDAV media browsing and streaming.
2025-04-19 22:17:34 +02:00
12 changed files with 331 additions and 128 deletions

2
.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
bin/
obj/

View file

@ -7,6 +7,9 @@ namespace Jellyfin.Plugin.Webdav.Configuration
/// </summary> /// </summary>
public class PluginConfiguration : BasePluginConfiguration public class PluginConfiguration : BasePluginConfiguration
{ {
/// <summary>
/// Initializes a new instance of the <see cref="PluginConfiguration"/> class.
/// </summary>
public PluginConfiguration() public PluginConfiguration()
{ {
BaseUrl = ""; BaseUrl = "";

View file

@ -4,10 +4,7 @@
<TargetFramework>net8.0</TargetFramework> <TargetFramework>net8.0</TargetFramework>
<RootNamespace>Jellyfin.Plugin.Webdav</RootNamespace> <RootNamespace>Jellyfin.Plugin.Webdav</RootNamespace>
<GenerateDocumentationFile>true</GenerateDocumentationFile> <GenerateDocumentationFile>true</GenerateDocumentationFile>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<AnalysisMode>AllEnabledByDefault</AnalysisMode>
<CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
@ -17,9 +14,10 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" /> <!-- Manually include the WebDav.Client.dll in the output directory -->
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.556" PrivateAssets="All" /> <None Include="$(NuGetPackageRoot)webdav.client/2.9.0/lib/netstandard2.0/WebDav.Client.dll">
<PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" /> <CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
@ -27,4 +25,9 @@
<EmbeddedResource Include="Configuration\configPage.html" /> <EmbeddedResource Include="Configuration\configPage.html" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<None Include="meta.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project> </Project>

91
NextcloudPluginPlan.md Normal file
View file

@ -0,0 +1,91 @@
# Nextcloud/WebDAV Jellyfin Plugin Plan
Below is a high-level plan for a Jellyfin plugin that integrates Nextcloud/WebDAV sources as library content.
```mermaid
flowchart TD
subgraph Plugin Core
A[PluginConfiguration<br/> holds URL, creds, paths]
B[Plugin<br/> BasePlugin<Config>]
C[IPluginServiceRegistrator<br/> registers services]
D[IHostedService<br/> hook on startup]
end
subgraph Services
E[WebDavClientService<br/> wraps WebDav.Client]
F[RemoteLibraryProvider<br/> implements virtual folder scanning]
G[CachingLayer<br/> buffer / temporary file cache]
H[StreamingController<br/> ControllerBase for proxying streams]
end
subgraph Jellyfin Core
I[ILibraryManager<br/> library registration]
J[Scan Engine<br/> calls provider to enumerate items]
K[HTTP Pipeline<br/> routes to controllers]
end
A --> B
B --> C
C --> E
C --> F
E --> F
F --> J
F --> G
G --> H
H --> K
D --> I
I --> J
```
## 1. Skeleton & Setup
- Copy the `jellyfin-plugin-template` and rename to `Jellyfin.Plugin.Nextcloud`.
- Update project metadata: plugin ID, name, version in `.csproj`.
- Add NuGet reference to `WebDav.Client`.
## 2. Configuration & Dependency Injection
- Define `PluginConfiguration` with URL, credentials, root path, and cache settings.
- Implement `Plugin : BasePlugin<PluginConfiguration>` to override `Name`, `Id`, and inject DI.
- Create `IPluginServiceRegistrator` to register services:
- `WebDavClientService` (singleton)
- `RemoteLibraryProvider` (scanning)
- `StreamingController` (ASP.NET Controller)
- Register virtual folder with `ILibraryManager.RegisterVirtualFolder` on startup.
## 3. Scanning Integration
- Research Jellyfin scan engine provider interfaces.
- Implement `RemoteLibraryProvider` to list directories/files via `WebDavClientService`.
- Map each remote item to a `BaseItem` (e.g., `Video`, `Folder`) with metadata.
- Track ETags or timestamps to support incremental scans.
## 4. Streaming & Caching
- Create `StreamingController : ControllerBase` with endpoint `/WebDav/Stream/{itemId}`.
- Map `itemId` to remote path and stream via `WebDavClientService.GetStream`.
- Implement `CachingLayer` to buffer chunks and write to temp files to prevent stalls.
## 5. Admin UI
- Define `configPage.html` as an embedded resource and implement `IHasWebPages` in `Plugin.cs` to return a `PluginPageInfo` for the settings page.
- Update `build.yaml` to include a `pages` section:
```yaml
pages:
- name: WebDAV
embeddedResourcePath: Jellyfin.Plugin.Webdav.Configuration.configPage.html
```
- Rebuild the plugin so that MSBuild generates a `plugin.json` manifest containing the pages definition.
- Deploy the `plugin.json`, DLL, and dependencies to Jellyfins `plugins` folder.
- Restart Jellyfin to verify the “WebDAV” settings page appears under Admin → Plugins.
## 6. Testing & Validation
- Unit-test `WebDavClientService` using a test Nextcloud instance or mock.
- Deploy plugin locally; verify library shows Nextcloud virtual folders.
- Test playback to ensure smooth streaming.
## 7. Documentation & Release
- Write README with installation, configuration, and troubleshooting guidance.
- Bump version, package plugin (`.nupkg`), and publish to GitHub Releases.
### Milestones
- **M1**: Plugin skeleton & DI wiring
- **M2**: Scanning proof-of-concept
- **M3**: Streaming proxy & caching
- **M4**: Admin UI & settings page
- **M5**: Testing, documentation, release

View file

@ -1,25 +1,31 @@
/*
* Jellyfin.Plugin.Webdav
* Copyright (C) 2025 Jellyfin contributors
* Licensed under GPLv3
*/
namespace Jellyfin.Plugin.Webdav
{
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization; using System.Globalization;
using Jellyfin.Plugin.Webdav.Configuration;
using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Plugins; using MediaBrowser.Common.Plugins;
using MediaBrowser.Controller;
using MediaBrowser.Controller.Plugins;
using MediaBrowser.Model.Plugins; using MediaBrowser.Model.Plugins;
using MediaBrowser.Model.Serialization; using MediaBrowser.Model.Serialization;
namespace Jellyfin.Plugin.Webdav;
/// <summary> /// <summary>
/// The main plugin. /// The main plugin.
/// </summary> /// </summary>
public class Plugin : BasePlugin<PluginConfiguration>, IHasWebPages public sealed class WebdavPlugin : BasePlugin<Configuration.PluginConfiguration>, IHasWebPages
{ {
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="Plugin"/> class. /// Initializes a new instance of the <see cref="WebdavPlugin"/> class.
/// </summary> /// </summary>
/// <param name="applicationPaths">Instance of the <see cref="IApplicationPaths"/> interface.</param> /// <param name="applicationPaths">The application paths.</param>
/// <param name="xmlSerializer">Instance of the <see cref="IXmlSerializer"/> interface.</param> /// <param name="xmlSerializer">The xml serializer.</param>
public Plugin(IApplicationPaths applicationPaths, IXmlSerializer xmlSerializer) public WebdavPlugin(IApplicationPaths applicationPaths, IXmlSerializer xmlSerializer)
: base(applicationPaths, xmlSerializer) : base(applicationPaths, xmlSerializer)
{ {
Instance = this; Instance = this;
@ -32,20 +38,21 @@ public class Plugin : BasePlugin<PluginConfiguration>, IHasWebPages
public override Guid Id => Guid.Parse("5db89aeb-14ad-450b-bcf2-ae235ebbe299"); public override Guid Id => Guid.Parse("5db89aeb-14ad-450b-bcf2-ae235ebbe299");
/// <summary> /// <summary>
/// Gets the current plugin instance. /// Gets the plugin instance.
/// </summary> /// </summary>
public static Plugin? Instance { get; private set; } public static WebdavPlugin Instance { get; private set; }
/// <inheritdoc/> /// <inheritdoc/>
public IEnumerable<PluginPageInfo> GetPages() public IEnumerable<PluginPageInfo> GetPages()
{ {
return return new[]
[ {
new PluginPageInfo new PluginPageInfo
{ {
Name = Name, Name = Name,
EmbeddedResourcePath = string.Format(CultureInfo.InvariantCulture, "{0}.Configuration.configPage.html", GetType().Namespace) EmbeddedResourcePath = $"{GetType().Namespace}.Configuration.configPage.html"
}
};
} }
];
} }
} }

View file

@ -1,20 +1,27 @@
using Microsoft.Extensions.DependencyInjection; /*
using MediaBrowser.Controller.Plugins; * Jellyfin.Plugin.Webdav
using MediaBrowser.Controller.Library; * Copyright (C) 2025 Jellyfin contributors
using Microsoft.Extensions.Hosting; * Licensed under GPLv3
*/
namespace Jellyfin.Plugin.Webdav namespace Jellyfin.Plugin.Webdav
{ {
using MediaBrowser.Controller;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Plugins;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
/// <summary> /// <summary>
/// Registers plugin services with the DI container. /// Registers plugin services with the DI container.
/// </summary> /// </summary>
public class ServiceRegistrator : IPluginServiceRegistrator public class ServiceRegistrator : IPluginServiceRegistrator
{ {
/// <inheritdoc/> /// <inheritdoc/>
public void RegisterServices(IServiceCollection services, IServerApplicationHost applicationHost) public void RegisterServices(IServiceCollection serviceCollection, IServerApplicationHost applicationHost)
{ {
services.AddSingleton<WebDavClientService>(); serviceCollection.AddSingleton(sp => WebdavPlugin.Instance.Configuration);
services.AddHostedService<WebDavSyncService>(); serviceCollection.AddSingleton<WebDavClientService>();
serviceCollection.AddHostedService<WebDavSyncService>();
} }
} }
} }

View file

@ -14,6 +14,11 @@ namespace Jellyfin.Plugin.Webdav.Controllers
private readonly WebDavClientService _webDavClientService; private readonly WebDavClientService _webDavClientService;
private readonly PluginConfiguration _config; private readonly PluginConfiguration _config;
/// <summary>
/// Initializes a new instance of the <see cref="StreamController"/> class.
/// </summary>
/// <param name="webDavClientService">The WebDAV client service.</param>
/// <param name="config">The plugin configuration.</param>
public StreamController(WebDavClientService webDavClientService, PluginConfiguration config) public StreamController(WebDavClientService webDavClientService, PluginConfiguration config)
{ {
_webDavClientService = webDavClientService; _webDavClientService = webDavClientService;
@ -23,7 +28,7 @@ namespace Jellyfin.Plugin.Webdav.Controllers
/// <summary> /// <summary>
/// Streams the file at the given WebDAV path. /// Streams the file at the given WebDAV path.
/// </summary> /// </summary>
/// <param name="*path">The relative path under the configured root.</param> /// <param name="path">The relative path under the configured root.</param>
[HttpGet("{*path}")] [HttpGet("{*path}")]
public async Task<IActionResult> Get(string path) public async Task<IActionResult> Get(string path)
{ {

View file

@ -1,43 +1,58 @@
/*
* Jellyfin.Plugin.Webdav
* Copyright (C) 2025 Jellyfin contributors
* Licensed under GPLv3
*/
namespace Jellyfin.Plugin.Webdav
{
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Net; using System.Net;
using System.Threading.Tasks; using System.Threading.Tasks;
using WebDav.Client; using Jellyfin.Plugin.Webdav.Configuration;
using WebDav;
namespace Jellyfin.Plugin.Webdav
{
/// <summary> /// <summary>
/// Service for interacting with WebDAV endpoints. /// Service for interacting with WebDAV endpoints.
/// </summary> /// </summary>
public class WebDavClientService public sealed class WebDavClientService
{ {
private readonly PluginConfiguration _config; private readonly PluginConfiguration _config;
private readonly WebDavClient _client; private readonly WebDavClient _client;
/// <summary>
/// Initializes a new instance of the <see cref="WebDavClientService"/> class.
/// </summary>
/// <param name="config">Plugin configuration.</param>
public WebDavClientService(PluginConfiguration config) public WebDavClientService(PluginConfiguration config)
{ {
_config = config; _config = config;
var parameters = new WebDavClientParams var parameters = new WebDavClientParams
{ {
BaseAddress = new Uri(_config.BaseUrl), BaseAddress = new Uri(_config.BaseUrl.TrimEnd('/')),
Credentials = new NetworkCredential(_config.Username, _config.Password) Credentials = new NetworkCredential(_config.Username, _config.Password),
PreAuthenticate = true
}; };
_client = new WebDavClient(parameters); _client = new WebDavClient(parameters);
} }
/// <summary> /// <summary>
/// List resources at the specified WebDAV path. /// Lists resources at the specified WebDAV path.
/// </summary> /// </summary>
public async Task<IEnumerable<WebDavResource>> ListAsync(string path) /// <param name="path">Remote path.</param>
/// <returns>A collection of WebDAV resources.</returns>
public async Task<IEnumerable<dynamic>> ListAsync(string path)
{ {
var response = await _client.Propfind(path).ConfigureAwait(false); var response = await _client.Propfind(path).ConfigureAwait(false);
return response.Resources; return response.Resources;
} }
/// <summary> /// <summary>
/// Get a raw file stream from the WebDAV endpoint. /// Gets a raw file stream from the WebDAV endpoint.
/// </summary> /// </summary>
/// <param name="path">Remote file path.</param>
/// <returns>A stream for the remote file.</returns>
public async Task<Stream> GetStreamAsync(string path) public async Task<Stream> GetStreamAsync(string path)
{ {
var response = await _client.GetRawFile(path).ConfigureAwait(false); var response = await _client.GetRawFile(path).ConfigureAwait(false);

View file

@ -1,37 +1,41 @@
using System.Threading; /*
using System.Threading.Tasks; * Jellyfin.Plugin.Webdav
using Microsoft.Extensions.Hosting; * Copyright (C) 2025 Jellyfin contributors
using MediaBrowser.Controller.Library; * Licensed under GPLv3
using MediaBrowser.Model.Configuration; */
namespace Jellyfin.Plugin.Webdav namespace Jellyfin.Plugin.Webdav
{ {
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Controller.Library;
using MediaBrowser.Model.Configuration;
using Microsoft.Extensions.Hosting;
/// <summary> /// <summary>
/// Hosted service that registers the WebDAV source as a virtual folder on server startup. /// Hosted service that registers the WebDAV cache as a virtual folder on startup.
/// </summary> /// </summary>
public class WebDavHostedService : IHostedService public class WebDavHostedService : IHostedService
{ {
private readonly ILibraryManager _libraryManager; private readonly ILibraryManager _libraryManager;
/// <summary>
/// Initializes a new instance of the <see cref="WebDavHostedService"/> class.
/// </summary>
/// <param name="libraryManager">The library manager.</param>
public WebDavHostedService(ILibraryManager libraryManager) public WebDavHostedService(ILibraryManager libraryManager)
{ {
_libraryManager = libraryManager; _libraryManager = libraryManager;
} }
/// <inheritdoc/>
public async Task StartAsync(CancellationToken cancellationToken) public async Task StartAsync(CancellationToken cancellationToken)
{ {
// Register a virtual folder for WebDAV remote library var options = new LibraryOptions();
var options = new LibraryOptions
{
EnableInvertFolderHierarchy = false
};
// Name must match plugin display name await _libraryManager.AddVirtualFolder("WebDAV", null, options, true).ConfigureAwait(false);
await _libraryManager
.AddVirtualFolder("WebDAV", null, options, true)
.ConfigureAwait(false);
} }
/// <inheritdoc/>
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask; public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
} }
} }

View file

@ -1,75 +1,101 @@
using System; namespace Jellyfin.Plugin.Webdav
{
using System.IO; using System.IO;
using MediaBrowser.Controller;
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.Extensions.Hosting; using Jellyfin.Plugin.Webdav.Configuration;
using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Plugins; using System.Collections.Generic;
using Microsoft.Extensions.Hosting;
using MediaBrowser.Model.Configuration;
namespace Jellyfin.Plugin.Webdav
{
/// <summary> /// <summary>
/// Hosted service that synchronizes WebDAV content into a local cache directory /// Hosted service that synchronizes WebDAV content into the local cache directory
/// and registers it as a Jellyfin virtual folder. /// and registers it as a Jellyfin virtual folder.
/// </summary> /// </summary>
public class WebDavSyncService : IHostedService public sealed class WebDavSyncService : IHostedService
{ {
private readonly WebDavClientService _client; private readonly WebDavClientService client;
private readonly PluginConfiguration _config; private readonly PluginConfiguration config;
private readonly ILibraryManager _libraryManager; private readonly IServerApplicationPaths appPaths;
private readonly IServerApplicationHost _appHost; private readonly ILibraryManager libraryManager;
public WebDavSyncService(WebDavClientService client, PluginConfiguration config, ILibraryManager libraryManager, IServerApplicationHost appHost) /// <summary>
/// Initializes a new instance of the <see cref="WebDavSyncService"/> class.
/// </summary>
/// <param name="client">The WebDAV client service.</param>
/// <param name="config">The plugin configuration.</param>
/// <param name="appPaths">Server application paths.</param>
/// <param name="libraryManager">Library manager.</param>
public WebDavSyncService(
WebDavClientService client,
PluginConfiguration config,
IServerApplicationPaths appPaths,
ILibraryManager libraryManager)
{ {
_client = client; this.client = client;
_config = config; this.config = config;
_libraryManager = libraryManager; this.appPaths = appPaths;
_appHost = appHost; this.libraryManager = libraryManager;
} }
/// <inheritdoc/>
public async Task StartAsync(CancellationToken cancellationToken) public async Task StartAsync(CancellationToken cancellationToken)
{ {
// Compute local root under DefaultUserViewsPath // Determine cache root: either configured directory or default user views path
var localRoot = Path.Combine(_appHost.ApplicationPaths.DefaultUserViewsPath, "WebDAV"); var baseCache = string.IsNullOrWhiteSpace(config.CacheDirectory)
? appPaths.DefaultUserViewsPath
: config.CacheDirectory;
var localRoot = Path.Combine(baseCache, "WebDAV");
if (!Directory.Exists(localRoot))
{
Directory.CreateDirectory(localRoot); Directory.CreateDirectory(localRoot);
// Sync directories recursively
await SyncDirectory(_config.RootPath, localRoot, cancellationToken).ConfigureAwait(false);
// Register the virtual folder pointing to the synced cache
var options = new LibraryOptions();
await _libraryManager.AddVirtualFolder("WebDAV", null, options, true).ConfigureAwait(false);
} }
private async Task SyncDirectory(string remotePath, string localPath, CancellationToken cancellationToken) await SyncDirectoryAsync(config.RootPath.Trim('/'), localRoot, cancellationToken)
.ConfigureAwait(false);
var options = new LibraryOptions();
await libraryManager
.AddVirtualFolder("WebDAV", null, options, true)
.ConfigureAwait(false);
libraryManager.AddMediaPath("WebDAV", new MediaPathInfo { Path = localRoot });
}
private async Task SyncDirectoryAsync(string remotePath, string localPath, CancellationToken token)
{ {
var resources = await _client.ListAsync(remotePath).ConfigureAwait(false); var resources = await client.ListAsync(remotePath).ConfigureAwait(false);
foreach (var res in resources.Where(r => r.DisplayName != null)) foreach (var item in resources.Where(r => !string.IsNullOrEmpty(r.DisplayName)))
{ {
if (cancellationToken.IsCancellationRequested) if (token.IsCancellationRequested)
{ {
break; break;
} }
var name = res.DisplayName; var name = item.DisplayName!;
var remoteSub = $"{remotePath.TrimEnd('/')}/{name}"; var remoteItem = $"{remotePath}/{name}";
var localSub = Path.Combine(localPath, name); var localItem = Path.Combine(localPath, name);
if (res.IsCollection) if (item.IsCollection)
{ {
Directory.CreateDirectory(localSub); if (!Directory.Exists(localItem))
await SyncDirectory(remoteSub, localSub, cancellationToken).ConfigureAwait(false); {
Directory.CreateDirectory(localItem);
}
await SyncDirectoryAsync(remoteItem, localItem, token).ConfigureAwait(false);
} }
else else
{ {
using var stream = await _client.GetStreamAsync(remoteSub).ConfigureAwait(false); using var stream = await client.GetStreamAsync(remoteItem).ConfigureAwait(false);
using var fs = File.Create(localSub); using var fs = File.Create(localItem);
await stream.CopyToAsync(fs, cancellationToken).ConfigureAwait(false); await stream.CopyToAsync(fs, token).ConfigureAwait(false);
} }
} }
} }
/// <inheritdoc/>
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask; public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
} }
} }

19
build.yaml Normal file
View file

@ -0,0 +1,19 @@
---
name: "WebDAV"
guid: "5db89aeb-14ad-450b-bcf2-ae235ebbe299"
version: "1.0.0.0"
targetAbi: "10.9.0.0"
framework: "net8.0"
overview: "WebDAV Nextcloud integration plugin"
description: >
Integrates Nextcloud/WebDAV sources as virtual library content in Jellyfin.
category: "General"
owner: "jellyfin"
pages:
- name: WebDAV
embeddedResourcePath: Jellyfin.Plugin.Webdav.Configuration.configPage.html
artifacts:
- "Jellyfin.Plugin.Webdav.dll"
- "WebDav.Client.dll"
changelog: >
Initial release.

21
meta.json Normal file
View file

@ -0,0 +1,21 @@
{
"name": "WebDAV",
"guid": "5db89aeb-14ad-450b-bcf2-ae235ebbe299",
"version": "1.0.0.0",
"targetAbi": "10.9.0.0",
"framework": "net8.0",
"overview": "WebDAV Nextcloud integration plugin",
"description": "Integrates Nextcloud/WebDAV sources as virtual library content in Jellyfin.",
"category": "General",
"owner": "jellyfin",
"assemblies": [
"Jellyfin.Plugin.Webdav.dll",
"WebDav.Client.dll"
],
"pages": [
{
"name": "WebDAV",
"embeddedResourcePath": "Jellyfin.Plugin.Webdav.Configuration.configPage.html"
}
]
}