The WebDAV integration plugin (Jellyfin.Plugin.Webdav) has been scaffolded according to the approved plan:

• Project renamed and updated (Jellyfin.Plugin.Webdav.csproj) with WebDav.Client reference

• Plugin.cs set to “WebDAV” with the new GUID

• Configuration/PluginConfiguration.cs defines the connection and cache settings

• ServiceRegistrator.cs wires DI for WebDavClientService and the WebDavSyncService hosted service

• WebDavClientService.cs wraps WebDav.Client for directory listing and streaming

• WebDavSyncService.cs syncs remote files into a local cache and registers it as a virtual folder

• StreamingController.cs exposes /WebDav/Stream/{*path} for streaming

• Configuration/configPage.html implements the dashboard configuration UI

Next step: build and deploy the plugin to your Jellyfin server’s plugins folder.
This commit is contained in:
copyrights 2025-04-19 21:59:22 +02:00
parent 2d5084ab29
commit 2811ed4aad
3 changed files with 122 additions and 45 deletions

View file

@ -2,35 +2,36 @@
<html lang="en">
<head>
<meta charset="utf-8">
<title>Template</title>
<title>WebDAV Configuration</title>
</head>
<body>
<div id="TemplateConfigPage" data-role="page" class="page type-interior pluginConfigurationPage" data-require="emby-input,emby-button,emby-select,emby-checkbox">
<div id="WebdavConfigPage" data-role="page" class="page type-interior pluginConfigurationPage" data-require="emby-input,emby-button,emby-checkbox">
<div data-role="content">
<div class="content-primary">
<form id="TemplateConfigForm">
<div class="selectContainer">
<label class="selectLabel" for="Options">Several Options</label>
<select is="emby-select" id="Options" name="Options" class="emby-select-withcolor emby-select">
<option id="optOneOption" value="OneOption">One Option</option>
<option id="optAnotherOption" value="AnotherOption">Another Option</option>
</select>
<form id="WebdavConfigForm">
<div class="inputContainer">
<label class="inputLabel" for="BaseUrl">WebDAV URL</label>
<input id="BaseUrl" name="BaseUrl" type="text" is="emby-input" />
</div>
<div class="inputContainer">
<label class="inputLabel inputLabelUnfocused" for="AnInteger">An Integer</label>
<input id="AnInteger" name="AnInteger" type="number" is="emby-input" min="0" />
<div class="fieldDescription">A Description</div>
</div>
<div class="checkboxContainer checkboxContainer-withDescription">
<label class="emby-checkbox-label">
<input id="TrueFalseSetting" name="TrueFalseCheckBox" type="checkbox" is="emby-checkbox" />
<span>A Checkbox</span>
</label>
<label class="inputLabel" for="Username">Username</label>
<input id="Username" name="Username" type="text" is="emby-input" />
</div>
<div class="inputContainer">
<label class="inputLabel inputLabelUnfocused" for="AString">A String</label>
<input id="AString" name="AString" type="text" is="emby-input" />
<div class="fieldDescription">Another Description</div>
<label class="inputLabel" for="Password">Password</label>
<input id="Password" name="Password" type="password" is="emby-input" />
</div>
<div class="inputContainer">
<label class="inputLabel" for="RootPath">Root Path</label>
<input id="RootPath" name="RootPath" type="text" is="emby-input" />
</div>
<div class="inputContainer">
<label class="inputLabel" for="CacheDirectory">Cache Directory</label>
<input id="CacheDirectory" name="CacheDirectory" type="text" is="emby-input" />
</div>
<div class="inputContainer">
<label class="inputLabel" for="CacheSizeMb">Cache Size (MB)</label>
<input id="CacheSizeMb" name="CacheSizeMb" type="number" is="emby-input" min="0" />
</div>
<div>
<button is="emby-button" type="submit" class="raised button-submit block emby-button">
@ -41,38 +42,39 @@
</div>
</div>
<script type="text/javascript">
var TemplateConfig = {
pluginUniqueId: 'eb5d7894-8eef-4b36-aa6f-5d124e828ce1'
var Config = {
pluginUniqueId: '5db89aeb-14ad-450b-bcf2-ae235ebbe299'
};
document.querySelector('#TemplateConfigPage')
document.querySelector('#WebdavConfigPage')
.addEventListener('pageshow', function() {
Dashboard.showLoadingMsg();
ApiClient.getPluginConfiguration(TemplateConfig.pluginUniqueId).then(function (config) {
document.querySelector('#Options').value = config.Options;
document.querySelector('#AnInteger').value = config.AnInteger;
document.querySelector('#TrueFalseSetting').checked = config.TrueFalseSetting;
document.querySelector('#AString').value = config.AString;
ApiClient.getPluginConfiguration(Config.pluginUniqueId).then(function (config) {
document.querySelector('#BaseUrl').value = config.BaseUrl;
document.querySelector('#Username').value = config.Username;
document.querySelector('#Password').value = config.Password;
document.querySelector('#RootPath').value = config.RootPath;
document.querySelector('#CacheDirectory').value = config.CacheDirectory;
document.querySelector('#CacheSizeMb').value = config.CacheSizeMb;
Dashboard.hideLoadingMsg();
});
});
document.querySelector('#TemplateConfigForm')
document.querySelector('#WebdavConfigForm')
.addEventListener('submit', function(e) {
Dashboard.showLoadingMsg();
ApiClient.getPluginConfiguration(TemplateConfig.pluginUniqueId).then(function (config) {
config.Options = document.querySelector('#Options').value;
config.AnInteger = document.querySelector('#AnInteger').value;
config.TrueFalseSetting = document.querySelector('#TrueFalseSetting').checked;
config.AString = document.querySelector('#AString').value;
ApiClient.updatePluginConfiguration(TemplateConfig.pluginUniqueId, config).then(function (result) {
Dashboard.processPluginConfigurationUpdateResult(result);
Dashboard.showLoadingMsg();
ApiClient.getPluginConfiguration(Config.pluginUniqueId).then(function (config) {
config.BaseUrl = document.querySelector('#BaseUrl').value;
config.Username = document.querySelector('#Username').value;
config.Password = document.querySelector('#Password').value;
config.RootPath = document.querySelector('#RootPath').value;
config.CacheDirectory = document.querySelector('#CacheDirectory').value;
config.CacheSizeMb = parseInt(document.querySelector('#CacheSizeMb').value, 10);
ApiClient.updatePluginConfiguration(Config.pluginUniqueId, config).then(function (result) {
Dashboard.processPluginConfigurationUpdateResult(result);
});
});
e.preventDefault();
return false;
});
e.preventDefault();
return false;
});
</script>
</div>
</body>

View file

@ -14,7 +14,7 @@ namespace Jellyfin.Plugin.Webdav
public void RegisterServices(IServiceCollection services, IServerApplicationHost applicationHost)
{
services.AddSingleton<WebDavClientService>();
services.AddSingleton<IHostedService, WebDavHostedService>();
services.AddHostedService<WebDavSyncService>();
}
}
}

75
WebDavSyncService.cs Normal file
View file

@ -0,0 +1,75 @@
using System;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Hosting;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Plugins;
namespace Jellyfin.Plugin.Webdav
{
/// <summary>
/// Hosted service that synchronizes WebDAV content into a local cache directory
/// and registers it as a Jellyfin virtual folder.
/// </summary>
public class WebDavSyncService : IHostedService
{
private readonly WebDavClientService _client;
private readonly PluginConfiguration _config;
private readonly ILibraryManager _libraryManager;
private readonly IServerApplicationHost _appHost;
public WebDavSyncService(WebDavClientService client, PluginConfiguration config, ILibraryManager libraryManager, IServerApplicationHost appHost)
{
_client = client;
_config = config;
_libraryManager = libraryManager;
_appHost = appHost;
}
public async Task StartAsync(CancellationToken cancellationToken)
{
// Compute local root under DefaultUserViewsPath
var localRoot = Path.Combine(_appHost.ApplicationPaths.DefaultUserViewsPath, "WebDAV");
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)
{
var resources = await _client.ListAsync(remotePath).ConfigureAwait(false);
foreach (var res in resources.Where(r => r.DisplayName != null))
{
if (cancellationToken.IsCancellationRequested)
{
break;
}
var name = res.DisplayName;
var remoteSub = $"{remotePath.TrimEnd('/')}/{name}";
var localSub = Path.Combine(localPath, name);
if (res.IsCollection)
{
Directory.CreateDirectory(localSub);
await SyncDirectory(remoteSub, localSub, cancellationToken).ConfigureAwait(false);
}
else
{
using var stream = await _client.GetStreamAsync(remoteSub).ConfigureAwait(false);
using var fs = File.Create(localSub);
await stream.CopyToAsync(fs, cancellationToken).ConfigureAwait(false);
}
}
}
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
}
}