mirror of
https://github.com/Sonarr/Sonarr
synced 2025-12-06 08:28:37 +01:00
Add v5 calendar endpoints
This commit is contained in:
parent
5867cd5f47
commit
6a3e1278a5
4 changed files with 320 additions and 1 deletions
67
src/Sonarr.Api.V5/Calendar/CalendarController.cs
Normal file
67
src/Sonarr.Api.V5/Calendar/CalendarController.cs
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
using Microsoft.AspNetCore.Mvc;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Core.CustomFormats;
|
||||
using NzbDrone.Core.DecisionEngine.Specifications;
|
||||
using NzbDrone.Core.Tags;
|
||||
using NzbDrone.Core.Tv;
|
||||
using NzbDrone.SignalR;
|
||||
using Sonarr.Api.V5.Episodes;
|
||||
using Sonarr.Http;
|
||||
|
||||
namespace Sonarr.Api.V5.Calendar
|
||||
{
|
||||
[V5ApiController]
|
||||
public class CalendarController : EpisodeControllerWithSignalR
|
||||
{
|
||||
private readonly ITagService _tagService;
|
||||
|
||||
public CalendarController(IBroadcastSignalRMessage signalR,
|
||||
IEpisodeService episodeService,
|
||||
ISeriesService seriesService,
|
||||
IUpgradableSpecification qualityUpgradableSpecification,
|
||||
ITagService tagService,
|
||||
ICustomFormatCalculationService formatCalculator)
|
||||
: base(episodeService, seriesService, qualityUpgradableSpecification, formatCalculator, signalR)
|
||||
{
|
||||
_tagService = tagService;
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Produces("application/json")]
|
||||
public List<EpisodeResource> GetCalendar(DateTime? start, DateTime? end, bool unmonitored = false, bool includeSeries = false, bool includeEpisodeFile = false, bool includeEpisodeImages = false, string tags = "")
|
||||
{
|
||||
var startUse = start ?? DateTime.Today;
|
||||
var endUse = end ?? DateTime.Today.AddDays(2);
|
||||
var episodes = _episodeService.EpisodesBetweenDates(startUse, endUse, unmonitored);
|
||||
var allSeries = _seriesService.GetAllSeries();
|
||||
var parsedTags = new List<int>();
|
||||
var result = new List<Episode>();
|
||||
|
||||
if (tags.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
parsedTags.AddRange(tags.Split(',').Select(_tagService.GetTag).Select(t => t.Id));
|
||||
}
|
||||
|
||||
foreach (var episode in episodes)
|
||||
{
|
||||
var series = allSeries.SingleOrDefault(s => s.Id == episode.SeriesId);
|
||||
|
||||
if (series == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (parsedTags.Any() && parsedTags.None(series.Tags.Contains))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
result.Add(episode);
|
||||
}
|
||||
|
||||
var resources = MapToResource(result, includeSeries, includeEpisodeFile, includeEpisodeImages);
|
||||
|
||||
return resources.OrderBy(e => e.AirDateUtc).ToList();
|
||||
}
|
||||
}
|
||||
}
|
||||
101
src/Sonarr.Api.V5/Calendar/CalendarFeedController.cs
Normal file
101
src/Sonarr.Api.V5/Calendar/CalendarFeedController.cs
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
using Ical.Net;
|
||||
using Ical.Net.CalendarComponents;
|
||||
using Ical.Net.DataTypes;
|
||||
using Ical.Net.Serialization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Core.Tags;
|
||||
using NzbDrone.Core.Tv;
|
||||
using Sonarr.Http;
|
||||
|
||||
namespace Sonarr.Api.V5.Calendar;
|
||||
|
||||
[V5FeedController("calendar")]
|
||||
public class CalendarFeedController : Controller
|
||||
{
|
||||
private readonly IEpisodeService _episodeService;
|
||||
private readonly ISeriesService _seriesService;
|
||||
private readonly ITagService _tagService;
|
||||
|
||||
public CalendarFeedController(IEpisodeService episodeService, ISeriesService seriesService, ITagService tagService)
|
||||
{
|
||||
_episodeService = episodeService;
|
||||
_seriesService = seriesService;
|
||||
_tagService = tagService;
|
||||
}
|
||||
|
||||
[HttpGet("Sonarr.ics")]
|
||||
public IActionResult GetCalendarFeed(int pastDays = 7, int futureDays = 28, string tags = "", bool unmonitored = false, bool premieresOnly = false, bool asAllDay = false)
|
||||
{
|
||||
var start = DateTime.Today.AddDays(-pastDays);
|
||||
var end = DateTime.Today.AddDays(futureDays);
|
||||
var parsedTags = new List<int>();
|
||||
|
||||
if (tags.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
parsedTags.AddRange(tags.Split(',').Select(_tagService.GetTag).Select(t => t.Id));
|
||||
}
|
||||
|
||||
var episodes = _episodeService.EpisodesBetweenDates(start, end, unmonitored);
|
||||
var allSeries = _seriesService.GetAllSeries();
|
||||
var calendar = new Ical.Net.Calendar
|
||||
{
|
||||
ProductId = "-//sonarr.tv//Sonarr//EN"
|
||||
};
|
||||
|
||||
var calendarName = "Sonarr TV Schedule";
|
||||
calendar.AddProperty(new CalendarProperty("NAME", calendarName));
|
||||
calendar.AddProperty(new CalendarProperty("X-WR-CALNAME", calendarName));
|
||||
|
||||
foreach (var episode in episodes.OrderBy(v => v.AirDateUtc!.Value))
|
||||
{
|
||||
var series = allSeries.SingleOrDefault(s => s.Id == episode.SeriesId);
|
||||
|
||||
if (series == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (premieresOnly && (episode.SeasonNumber == 0 || episode.EpisodeNumber != 1))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (parsedTags.Any() && parsedTags.None(series.Tags.Contains))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var occurrence = calendar.Create<CalendarEvent>();
|
||||
occurrence.Uid = "NzbDrone_episode_" + episode.Id;
|
||||
occurrence.Status = episode.HasFile ? EventStatus.Confirmed : EventStatus.Tentative;
|
||||
occurrence.Description = episode.Overview;
|
||||
occurrence.Categories = new List<string>() { series.Network };
|
||||
|
||||
if (asAllDay)
|
||||
{
|
||||
occurrence.Start = new CalDateTime(episode.AirDateUtc!.Value.ToLocalTime()) { HasTime = false };
|
||||
}
|
||||
else
|
||||
{
|
||||
occurrence.Start = new CalDateTime(episode.AirDateUtc!.Value) { HasTime = true };
|
||||
occurrence.End = new CalDateTime(episode.AirDateUtc.Value.AddMinutes(series.Runtime)) { HasTime = true };
|
||||
}
|
||||
|
||||
switch (series.SeriesType)
|
||||
{
|
||||
case SeriesTypes.Daily:
|
||||
occurrence.Summary = $"{series.Title} - {episode.Title}";
|
||||
break;
|
||||
default:
|
||||
occurrence.Summary = $"{series.Title} - {episode.SeasonNumber}x{episode.EpisodeNumber:00} - {episode.Title}";
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
var serializer = (IStringSerializer)new SerializerFactory().Build(calendar.GetType(), new SerializationContext());
|
||||
var icalendar = serializer.SerializeToString(calendar);
|
||||
|
||||
return Content(icalendar, "text/calendar");
|
||||
}
|
||||
}
|
||||
151
src/Sonarr.Api.V5/Episodes/EpisodeControllerWithSignalR.cs
Normal file
151
src/Sonarr.Api.V5/Episodes/EpisodeControllerWithSignalR.cs
Normal file
|
|
@ -0,0 +1,151 @@
|
|||
using Microsoft.AspNetCore.Mvc;
|
||||
using NzbDrone.Core.CustomFormats;
|
||||
using NzbDrone.Core.Datastore.Events;
|
||||
using NzbDrone.Core.DecisionEngine.Specifications;
|
||||
using NzbDrone.Core.Download;
|
||||
using NzbDrone.Core.MediaFiles.Events;
|
||||
using NzbDrone.Core.Messaging.Events;
|
||||
using NzbDrone.Core.Tv;
|
||||
using NzbDrone.SignalR;
|
||||
using Sonarr.Api.V5.EpisodeFiles;
|
||||
using Sonarr.Api.V5.Series;
|
||||
using Sonarr.Http.REST;
|
||||
|
||||
namespace Sonarr.Api.V5.Episodes;
|
||||
|
||||
public abstract class EpisodeControllerWithSignalR : RestControllerWithSignalR<EpisodeResource, Episode>,
|
||||
IHandle<EpisodeGrabbedEvent>,
|
||||
IHandle<EpisodeImportedEvent>,
|
||||
IHandle<EpisodeFileDeletedEvent>
|
||||
{
|
||||
protected readonly IEpisodeService _episodeService;
|
||||
protected readonly ISeriesService _seriesService;
|
||||
protected readonly IUpgradableSpecification _upgradableSpecification;
|
||||
protected readonly ICustomFormatCalculationService _formatCalculator;
|
||||
|
||||
protected EpisodeControllerWithSignalR(IEpisodeService episodeService,
|
||||
ISeriesService seriesService,
|
||||
IUpgradableSpecification upgradableSpecification,
|
||||
ICustomFormatCalculationService formatCalculator,
|
||||
IBroadcastSignalRMessage signalRBroadcaster)
|
||||
: base(signalRBroadcaster)
|
||||
{
|
||||
_episodeService = episodeService;
|
||||
_seriesService = seriesService;
|
||||
_upgradableSpecification = upgradableSpecification;
|
||||
_formatCalculator = formatCalculator;
|
||||
}
|
||||
|
||||
protected EpisodeControllerWithSignalR(IEpisodeService episodeService,
|
||||
ISeriesService seriesService,
|
||||
IUpgradableSpecification upgradableSpecification,
|
||||
ICustomFormatCalculationService formatCalculator,
|
||||
IBroadcastSignalRMessage signalRBroadcaster,
|
||||
string resource)
|
||||
: base(signalRBroadcaster)
|
||||
{
|
||||
_episodeService = episodeService;
|
||||
_seriesService = seriesService;
|
||||
_upgradableSpecification = upgradableSpecification;
|
||||
_formatCalculator = formatCalculator;
|
||||
}
|
||||
|
||||
protected override EpisodeResource GetResourceById(int id)
|
||||
{
|
||||
var episode = _episodeService.GetEpisode(id);
|
||||
var resource = MapToResource(episode, true, true, true);
|
||||
return resource;
|
||||
}
|
||||
|
||||
protected EpisodeResource MapToResource(Episode episode, bool includeSeries, bool includeEpisodeFile, bool includeImages)
|
||||
{
|
||||
var resource = episode.ToResource();
|
||||
|
||||
if (includeSeries || includeEpisodeFile || includeImages)
|
||||
{
|
||||
var series = episode.Series ?? _seriesService.GetSeries(episode.SeriesId);
|
||||
|
||||
if (includeSeries)
|
||||
{
|
||||
resource.Series = series.ToResource();
|
||||
}
|
||||
|
||||
if (includeEpisodeFile && episode.EpisodeFileId != 0)
|
||||
{
|
||||
resource.EpisodeFile = episode.EpisodeFile.Value.ToResource(series, _upgradableSpecification, _formatCalculator);
|
||||
}
|
||||
|
||||
if (includeImages)
|
||||
{
|
||||
resource.Images = episode.Images;
|
||||
}
|
||||
}
|
||||
|
||||
return resource;
|
||||
}
|
||||
|
||||
protected List<EpisodeResource> MapToResource(List<Episode> episodes, bool includeSeries, bool includeEpisodeFile, bool includeImages)
|
||||
{
|
||||
var result = episodes.ToResource();
|
||||
|
||||
if (includeSeries || includeEpisodeFile || includeImages)
|
||||
{
|
||||
var seriesDict = new Dictionary<int, NzbDrone.Core.Tv.Series>();
|
||||
for (var i = 0; i < episodes.Count; i++)
|
||||
{
|
||||
var episode = episodes[i];
|
||||
var resource = result[i];
|
||||
|
||||
var series = episode.Series ?? seriesDict.GetValueOrDefault(episodes[i].SeriesId) ?? _seriesService.GetSeries(episodes[i].SeriesId);
|
||||
seriesDict[series.Id] = series;
|
||||
|
||||
if (includeSeries)
|
||||
{
|
||||
resource.Series = series.ToResource();
|
||||
}
|
||||
|
||||
if (includeEpisodeFile && episode.EpisodeFileId != 0)
|
||||
{
|
||||
resource.EpisodeFile = episode.EpisodeFile.Value.ToResource(series, _upgradableSpecification, _formatCalculator);
|
||||
}
|
||||
|
||||
if (includeImages)
|
||||
{
|
||||
resource.Images = episode.Images;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
[NonAction]
|
||||
public void Handle(EpisodeGrabbedEvent message)
|
||||
{
|
||||
foreach (var episode in message.Episode.Episodes)
|
||||
{
|
||||
var resource = episode.ToResource();
|
||||
resource.Grabbed = true;
|
||||
|
||||
BroadcastResourceChange(ModelAction.Updated, resource);
|
||||
}
|
||||
}
|
||||
|
||||
[NonAction]
|
||||
public void Handle(EpisodeImportedEvent message)
|
||||
{
|
||||
foreach (var episode in message.EpisodeInfo.Episodes)
|
||||
{
|
||||
BroadcastResourceChange(ModelAction.Updated, episode.Id);
|
||||
}
|
||||
}
|
||||
|
||||
[NonAction]
|
||||
public void Handle(EpisodeFileDeletedEvent message)
|
||||
{
|
||||
foreach (var episode in message.EpisodeFile.Episodes.Value)
|
||||
{
|
||||
BroadcastResourceChange(ModelAction.Updated, episode.Id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -15,7 +15,7 @@ public class EpisodeResource : RestResource
|
|||
public int EpisodeFileId { get; set; }
|
||||
public int SeasonNumber { get; set; }
|
||||
public int EpisodeNumber { get; set; }
|
||||
public required string Title { get; set; }
|
||||
public string? Title { get; set; }
|
||||
public string? AirDate { get; set; }
|
||||
public DateTime? AirDateUtc { get; set; }
|
||||
public DateTime? LastSearchTime { get; set; }
|
||||
|
|
|
|||
Loading…
Reference in a new issue