Added OPML support
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Net.Mime;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Plugin.Jellypod.Api.Models;
|
||||
@@ -26,6 +27,8 @@ public class JellypodController : ControllerBase
|
||||
private readonly IRssFeedService _rssFeedService;
|
||||
private readonly IPodcastStorageService _storageService;
|
||||
private readonly IPodcastDownloadService _downloadService;
|
||||
private readonly IOpmlService _opmlService;
|
||||
private readonly IHttpClientFactory _httpClientFactory;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="JellypodController"/> class.
|
||||
@@ -34,16 +37,22 @@ public class JellypodController : ControllerBase
|
||||
/// <param name="rssFeedService">RSS feed service.</param>
|
||||
/// <param name="storageService">Storage service.</param>
|
||||
/// <param name="downloadService">Download service.</param>
|
||||
/// <param name="opmlService">OPML service.</param>
|
||||
/// <param name="httpClientFactory">HTTP client factory.</param>
|
||||
public JellypodController(
|
||||
ILogger<JellypodController> logger,
|
||||
IRssFeedService rssFeedService,
|
||||
IPodcastStorageService storageService,
|
||||
IPodcastDownloadService downloadService)
|
||||
IPodcastDownloadService downloadService,
|
||||
IOpmlService opmlService,
|
||||
IHttpClientFactory httpClientFactory)
|
||||
{
|
||||
_logger = logger;
|
||||
_rssFeedService = rssFeedService;
|
||||
_storageService = storageService;
|
||||
_downloadService = downloadService;
|
||||
_opmlService = opmlService;
|
||||
_httpClientFactory = httpClientFactory;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -474,4 +483,102 @@ public class JellypodController : ControllerBase
|
||||
PlayCount = episode.PlayCount
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Exports all podcast subscriptions as OPML.
|
||||
/// </summary>
|
||||
/// <returns>OPML XML file.</returns>
|
||||
[HttpGet("opml/export")]
|
||||
[Produces("application/xml")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
public async Task<IActionResult> ExportOpml()
|
||||
{
|
||||
var opml = await _opmlService.ExportToOpmlAsync().ConfigureAwait(false);
|
||||
var bytes = System.Text.Encoding.UTF8.GetBytes(opml);
|
||||
|
||||
var fileName = $"jellypod-subscriptions-{DateTime.UtcNow:yyyy-MM-dd}.opml";
|
||||
return File(bytes, "application/xml", fileName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Imports podcasts from an uploaded OPML file.
|
||||
/// </summary>
|
||||
/// <param name="file">The OPML file to import.</param>
|
||||
/// <returns>Import results.</returns>
|
||||
[HttpPost("opml/import")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
public async Task<ActionResult<OpmlImportResult>> ImportOpmlFile(IFormFile file)
|
||||
{
|
||||
if (file == null || file.Length == 0)
|
||||
{
|
||||
return BadRequest("No file uploaded");
|
||||
}
|
||||
|
||||
if (file.Length > 5 * 1024 * 1024) // 5MB limit
|
||||
{
|
||||
return BadRequest("File too large. Maximum size is 5MB.");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
using var stream = file.OpenReadStream();
|
||||
var outlines = _opmlService.ParseOpml(stream);
|
||||
|
||||
if (outlines.Count == 0)
|
||||
{
|
||||
return BadRequest("No podcast feeds found in OPML file");
|
||||
}
|
||||
|
||||
var result = await _opmlService.ImportPodcastsAsync(outlines).ConfigureAwait(false);
|
||||
return Ok(result);
|
||||
}
|
||||
catch (System.Xml.XmlException ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Invalid OPML XML");
|
||||
return BadRequest("Invalid OPML file format");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Imports podcasts from an OPML URL.
|
||||
/// </summary>
|
||||
/// <param name="request">Request containing the OPML URL.</param>
|
||||
/// <returns>Import results.</returns>
|
||||
[HttpPost("opml/import-url")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
public async Task<ActionResult<OpmlImportResult>> ImportOpmlUrl([FromBody] OpmlImportRequest request)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(request.Url))
|
||||
{
|
||||
return BadRequest("URL is required");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var httpClient = _httpClientFactory.CreateClient("Jellypod");
|
||||
var opmlContent = await httpClient.GetStringAsync(request.Url).ConfigureAwait(false);
|
||||
|
||||
var outlines = _opmlService.ParseOpml(opmlContent);
|
||||
|
||||
if (outlines.Count == 0)
|
||||
{
|
||||
return BadRequest("No podcast feeds found in OPML");
|
||||
}
|
||||
|
||||
var result = await _opmlService.ImportPodcastsAsync(outlines).ConfigureAwait(false);
|
||||
return Ok(result);
|
||||
}
|
||||
catch (HttpRequestException ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Failed to fetch OPML from URL");
|
||||
return BadRequest("Failed to fetch OPML from URL");
|
||||
}
|
||||
catch (System.Xml.XmlException ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Invalid OPML XML from URL");
|
||||
return BadRequest("Invalid OPML format");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
namespace Jellyfin.Plugin.Jellypod.Api.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Details about a failed OPML feed import.
|
||||
/// </summary>
|
||||
public class OpmlImportError
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the feed URL that failed.
|
||||
/// </summary>
|
||||
public string FeedUrl { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the feed title from OPML (if available).
|
||||
/// </summary>
|
||||
public string? Title { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the error message.
|
||||
/// </summary>
|
||||
public string Error { get; set; } = string.Empty;
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
namespace Jellyfin.Plugin.Jellypod.Api.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Request to import podcasts from an OPML URL.
|
||||
/// </summary>
|
||||
public class OpmlImportRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the URL to fetch OPML from.
|
||||
/// </summary>
|
||||
public string Url { get; set; } = string.Empty;
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
using System.Collections.ObjectModel;
|
||||
|
||||
namespace Jellyfin.Plugin.Jellypod.Api.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Result of an OPML import operation.
|
||||
/// </summary>
|
||||
public class OpmlImportResult
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the total number of feeds found in the OPML.
|
||||
/// </summary>
|
||||
public int TotalFeeds { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the number of feeds successfully imported.
|
||||
/// </summary>
|
||||
public int ImportedCount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the number of feeds skipped (already subscribed).
|
||||
/// </summary>
|
||||
public int SkippedCount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the number of feeds that failed to import.
|
||||
/// </summary>
|
||||
public int FailedCount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the list of imported podcast titles.
|
||||
/// </summary>
|
||||
public Collection<string> ImportedPodcasts { get; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the list of skipped feed URLs (already subscribed).
|
||||
/// </summary>
|
||||
public Collection<string> SkippedFeeds { get; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the list of failed imports with error details.
|
||||
/// </summary>
|
||||
public Collection<OpmlImportError> Errors { get; } = new();
|
||||
}
|
||||
Reference in New Issue
Block a user