Compare commits
10 commits
2811ed4aad
...
d9ca4ef2f1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d9ca4ef2f1 | ||
|
|
300fe4df28 | ||
|
|
89ee319025 | ||
|
|
88a1de5d7d | ||
|
|
30f7b22f88 | ||
|
|
ad8a0a4297 | ||
|
|
d551480b5d | ||
|
|
7a89d08fc3 | ||
|
|
28bc6d6a28 | ||
|
|
eacb6134a0 |
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
bin/
|
||||||
|
obj/
|
||||||
|
|
@ -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 = "";
|
||||||
|
|
|
||||||
|
|
@ -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
91
NextcloudPluginPlan.md
Normal 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 Jellyfin’s `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
|
||||||
63
Plugin.cs
63
Plugin.cs
|
|
@ -1,51 +1,58 @@
|
||||||
using System;
|
/*
|
||||||
using System.Collections.Generic;
|
* Jellyfin.Plugin.Webdav
|
||||||
using System.Globalization;
|
* Copyright (C) 2025 Jellyfin contributors
|
||||||
using Jellyfin.Plugin.Webdav.Configuration;
|
* Licensed under GPLv3
|
||||||
using MediaBrowser.Common.Configuration;
|
*/
|
||||||
using MediaBrowser.Common.Plugins;
|
namespace Jellyfin.Plugin.Webdav
|
||||||
using MediaBrowser.Model.Plugins;
|
|
||||||
using MediaBrowser.Model.Serialization;
|
|
||||||
|
|
||||||
namespace Jellyfin.Plugin.Webdav;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The main plugin.
|
|
||||||
/// </summary>
|
|
||||||
public class Plugin : BasePlugin<PluginConfiguration>, IHasWebPages
|
|
||||||
{
|
{
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Globalization;
|
||||||
|
using MediaBrowser.Common.Configuration;
|
||||||
|
using MediaBrowser.Common.Plugins;
|
||||||
|
using MediaBrowser.Controller;
|
||||||
|
using MediaBrowser.Controller.Plugins;
|
||||||
|
using MediaBrowser.Model.Plugins;
|
||||||
|
using MediaBrowser.Model.Serialization;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="Plugin"/> class.
|
/// The main plugin.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="applicationPaths">Instance of the <see cref="IApplicationPaths"/> interface.</param>
|
public sealed class WebdavPlugin : BasePlugin<Configuration.PluginConfiguration>, IHasWebPages
|
||||||
/// <param name="xmlSerializer">Instance of the <see cref="IXmlSerializer"/> interface.</param>
|
{
|
||||||
public Plugin(IApplicationPaths applicationPaths, IXmlSerializer xmlSerializer)
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="WebdavPlugin"/> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="applicationPaths">The application paths.</param>
|
||||||
|
/// <param name="xmlSerializer">The xml serializer.</param>
|
||||||
|
public WebdavPlugin(IApplicationPaths applicationPaths, IXmlSerializer xmlSerializer)
|
||||||
: base(applicationPaths, xmlSerializer)
|
: base(applicationPaths, xmlSerializer)
|
||||||
{
|
{
|
||||||
Instance = this;
|
Instance = this;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc/>
|
||||||
public override string Name => "WebDAV";
|
public override string Name => "WebDAV";
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc/>
|
||||||
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"
|
||||||
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
];
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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>();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -1,43 +1,58 @@
|
||||||
using System;
|
/*
|
||||||
using System.Collections.Generic;
|
* Jellyfin.Plugin.Webdav
|
||||||
using System.IO;
|
* Copyright (C) 2025 Jellyfin contributors
|
||||||
using System.Net;
|
* Licensed under GPLv3
|
||||||
using System.Threading.Tasks;
|
*/
|
||||||
using WebDav.Client;
|
|
||||||
|
|
||||||
namespace Jellyfin.Plugin.Webdav
|
namespace Jellyfin.Plugin.Webdav
|
||||||
{
|
{
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Net;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Jellyfin.Plugin.Webdav.Configuration;
|
||||||
|
using 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);
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,75 +1,101 @@
|
||||||
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
|
namespace Jellyfin.Plugin.Webdav
|
||||||
{
|
{
|
||||||
|
using System.IO;
|
||||||
|
using MediaBrowser.Controller;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Jellyfin.Plugin.Webdav.Configuration;
|
||||||
|
using MediaBrowser.Controller.Library;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using Microsoft.Extensions.Hosting;
|
||||||
|
using MediaBrowser.Model.Configuration;
|
||||||
|
|
||||||
/// <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
19
build.yaml
Normal 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
21
meta.json
Normal 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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue