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:
parent
2d5084ab29
commit
2811ed4aad
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
75
WebDavSyncService.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue