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

View file

@ -14,7 +14,7 @@ namespace Jellyfin.Plugin.Webdav
public void RegisterServices(IServiceCollection services, IServerApplicationHost applicationHost) public void RegisterServices(IServiceCollection services, IServerApplicationHost applicationHost)
{ {
services.AddSingleton<WebDavClientService>(); 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;
}
}