Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6cc61f3212 | ||
|
|
4cf44616a8 | ||
|
|
33aaadae68 | ||
|
|
fe3f8e391e | ||
|
|
1a68dd040a | ||
|
|
67761c1a14 |
+18
-1
@@ -4,8 +4,24 @@ All notable changes to this project will be documented in this file.
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [0.1.5-alpha] - 2021-10-18
|
||||
### Fixed
|
||||
- Fix double scheduling; this could happen if the app was shutdown during a playout build
|
||||
- Fix updating Jellyfin and Emby TV seasons
|
||||
- Fix updating Jellyfin and Emby artwork
|
||||
- Fix Plex, Jellyfin, Emby worker crash attempting to sync library that no longer exists
|
||||
- Fix bug with `Duration` mode scheduling when media items are too long to fit in the requested duration
|
||||
- Fix bug with `Duration` mode scheduling with `Filler` tail mode where other duration items in the schedule would be skipped
|
||||
|
||||
### Added
|
||||
- Include music video thumbnails in channel guide (xmltv)
|
||||
|
||||
### Changed
|
||||
- Automatically find working Plex address on startup
|
||||
- Change default log level from `Debug` to `Information`
|
||||
- The `Debug` log level can be enabled in the `appsettings.json` file for non-docker installs
|
||||
- The `Debug` log level can be enabled by setting the environment variable `Serilog:MinimumLevel=Debug` for docker installs
|
||||
|
||||
## [0.1.4-alpha] - 2021-10-14
|
||||
### Fixed
|
||||
@@ -714,7 +730,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||
- Initial release to facilitate testing outside of Docker.
|
||||
|
||||
|
||||
[Unreleased]: https://github.com/jasongdove/ErsatzTV/compare/v0.1.4-alpha...HEAD
|
||||
[Unreleased]: https://github.com/jasongdove/ErsatzTV/compare/v0.1.5-alpha...HEAD
|
||||
[0.1.5-alpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.1.4-alpha...v0.1.5-alpha
|
||||
[0.1.4-alpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.1.3-alpha...v0.1.4-alpha
|
||||
[0.1.3-alpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.1.2-alpha...v0.1.3-alpha
|
||||
[0.1.2-alpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.1.1-alpha...v0.1.2-alpha
|
||||
|
||||
@@ -8,6 +8,7 @@ using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Interfaces.Locking;
|
||||
using ErsatzTV.Core.Interfaces.Plex;
|
||||
using ErsatzTV.Core.Interfaces.Repositories;
|
||||
using ErsatzTV.Core.Plex;
|
||||
using LanguageExt;
|
||||
using MediatR;
|
||||
using Microsoft.Extensions.Logging;
|
||||
@@ -23,16 +24,22 @@ namespace ErsatzTV.Application.Plex.Commands
|
||||
private readonly ILogger<SynchronizePlexMediaSourcesHandler> _logger;
|
||||
private readonly IMediaSourceRepository _mediaSourceRepository;
|
||||
private readonly IPlexTvApiClient _plexTvApiClient;
|
||||
private readonly IPlexServerApiClient _plexServerApiClient;
|
||||
private readonly IPlexSecretStore _plexSecretStore;
|
||||
|
||||
public SynchronizePlexMediaSourcesHandler(
|
||||
IMediaSourceRepository mediaSourceRepository,
|
||||
IPlexTvApiClient plexTvApiClient,
|
||||
IPlexServerApiClient plexServerApiClient,
|
||||
IPlexSecretStore plexSecretStore,
|
||||
ChannelWriter<IPlexBackgroundServiceRequest> channel,
|
||||
IEntityLocker entityLocker,
|
||||
ILogger<SynchronizePlexMediaSourcesHandler> logger)
|
||||
{
|
||||
_mediaSourceRepository = mediaSourceRepository;
|
||||
_plexTvApiClient = plexTvApiClient;
|
||||
_plexServerApiClient = plexServerApiClient;
|
||||
_plexSecretStore = plexSecretStore;
|
||||
_channel = channel;
|
||||
_entityLocker = entityLocker;
|
||||
_logger = logger;
|
||||
@@ -69,32 +76,76 @@ namespace ErsatzTV.Application.Plex.Commands
|
||||
return allExisting;
|
||||
}
|
||||
|
||||
private Task SynchronizeServer(List<PlexMediaSource> allExisting, PlexMediaSource server)
|
||||
private async Task SynchronizeServer(List<PlexMediaSource> allExisting, PlexMediaSource server)
|
||||
{
|
||||
Option<PlexMediaSource> maybeExisting =
|
||||
allExisting.Find(s => s.ClientIdentifier == server.ClientIdentifier);
|
||||
return maybeExisting.Match(
|
||||
existing =>
|
||||
{
|
||||
existing.Platform = server.Platform;
|
||||
existing.PlatformVersion = server.PlatformVersion;
|
||||
existing.ProductVersion = server.ProductVersion;
|
||||
existing.ServerName = server.ServerName;
|
||||
var toAdd = server.Connections
|
||||
.Filter(connection => existing.Connections.All(c => c.Uri != connection.Uri)).ToList();
|
||||
var toRemove = existing.Connections
|
||||
.Filter(connection => server.Connections.All(c => c.Uri != connection.Uri)).ToList();
|
||||
return _mediaSourceRepository.Update(existing, server.Connections, toAdd, toRemove);
|
||||
},
|
||||
async () =>
|
||||
{
|
||||
if (server.Connections.Any())
|
||||
{
|
||||
server.Connections.Head().IsActive = true;
|
||||
}
|
||||
|
||||
await _mediaSourceRepository.Add(server);
|
||||
});
|
||||
foreach (PlexMediaSource existing in maybeExisting)
|
||||
{
|
||||
existing.Platform = server.Platform;
|
||||
existing.PlatformVersion = server.PlatformVersion;
|
||||
existing.ProductVersion = server.ProductVersion;
|
||||
existing.ServerName = server.ServerName;
|
||||
var toAdd = server.Connections
|
||||
.Filter(connection => existing.Connections.All(c => c.Uri != connection.Uri)).ToList();
|
||||
var toRemove = existing.Connections
|
||||
.Filter(connection => server.Connections.All(c => c.Uri != connection.Uri)).ToList();
|
||||
await _mediaSourceRepository.Update(existing, toAdd, toRemove);
|
||||
await FindConnectionToActivate(existing);
|
||||
}
|
||||
|
||||
if (maybeExisting.IsNone)
|
||||
{
|
||||
await _mediaSourceRepository.Add(server);
|
||||
await FindConnectionToActivate(server);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task FindConnectionToActivate(PlexMediaSource server)
|
||||
{
|
||||
var prioritized = server.Connections.OrderBy(pc => pc.IsActive ? 0 : 1).ToList();
|
||||
foreach (PlexConnection connection in server.Connections)
|
||||
{
|
||||
connection.IsActive = false;
|
||||
}
|
||||
|
||||
Option<PlexServerAuthToken> maybeToken = await _plexSecretStore.GetServerAuthToken(server.ClientIdentifier);
|
||||
foreach (PlexServerAuthToken token in maybeToken)
|
||||
{
|
||||
foreach (PlexConnection connection in prioritized)
|
||||
{
|
||||
try
|
||||
{
|
||||
_logger.LogDebug("Attempting to locate to Plex at {Uri}", connection.Uri);
|
||||
if (await _plexServerApiClient.Ping(connection, token))
|
||||
{
|
||||
_logger.LogInformation("Located Plex at {Uri}", connection.Uri);
|
||||
connection.IsActive = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// do nothing
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (maybeToken.IsNone)
|
||||
{
|
||||
_logger.LogError(
|
||||
"Unable to activate Plex connection for server {Server} without auth token",
|
||||
server.ServerName);
|
||||
}
|
||||
|
||||
if (server.Connections.All(c => !c.IsActive))
|
||||
{
|
||||
_logger.LogError("Unable to locate Plex");
|
||||
server.Connections.Head().IsActive = true;
|
||||
}
|
||||
|
||||
await _mediaSourceRepository.Update(server, new List<PlexConnection>(), new List<PlexConnection>());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1151,6 +1151,212 @@ namespace ErsatzTV.Core.Tests.Scheduling
|
||||
result.Anchor.NextScheduleItem.Should().Be(items[1]);
|
||||
result.Anchor.DurationFinish.Should().Be(HoursAfterMidnight(6).UtcDateTime);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Alternating_Duration_With_Filler_Should_Alternate_Schedule_Items()
|
||||
{
|
||||
var collectionOne = new Collection
|
||||
{
|
||||
Id = 1,
|
||||
Name = "Duration Items 1",
|
||||
MediaItems = new List<MediaItem>
|
||||
{
|
||||
TestMovie(1, TimeSpan.FromMinutes(55), new DateTime(2020, 1, 1))
|
||||
}
|
||||
};
|
||||
|
||||
var collectionTwo = new Collection
|
||||
{
|
||||
Id = 2,
|
||||
Name = "Duration Items 2",
|
||||
MediaItems = new List<MediaItem>
|
||||
{
|
||||
TestMovie(2, TimeSpan.FromMinutes(55), new DateTime(2020, 1, 1))
|
||||
}
|
||||
};
|
||||
|
||||
var collectionThree = new Collection
|
||||
{
|
||||
Id = 3,
|
||||
Name = "Filler Items",
|
||||
MediaItems = new List<MediaItem>
|
||||
{
|
||||
TestMovie(3, TimeSpan.FromMinutes(5), new DateTime(2020, 1, 1))
|
||||
}
|
||||
};
|
||||
|
||||
var fakeRepository = new FakeMediaCollectionRepository(
|
||||
Map(
|
||||
(collectionOne.Id, collectionOne.MediaItems.ToList()),
|
||||
(collectionTwo.Id, collectionTwo.MediaItems.ToList()),
|
||||
(collectionThree.Id, collectionThree.MediaItems.ToList())));
|
||||
|
||||
var items = new List<ProgramScheduleItem>
|
||||
{
|
||||
new ProgramScheduleItemDuration
|
||||
{
|
||||
Id = 1,
|
||||
Index = 1,
|
||||
Collection = collectionOne,
|
||||
CollectionId = collectionOne.Id,
|
||||
StartTime = null,
|
||||
PlayoutDuration = TimeSpan.FromHours(3),
|
||||
PlaybackOrder = PlaybackOrder.Chronological,
|
||||
TailMode = TailMode.Filler,
|
||||
TailCollectionType = ProgramScheduleItemCollectionType.Collection,
|
||||
TailCollection = collectionThree,
|
||||
TailCollectionId = collectionThree.Id
|
||||
},
|
||||
new ProgramScheduleItemDuration
|
||||
{
|
||||
Id = 2,
|
||||
Index = 2,
|
||||
Collection = collectionTwo,
|
||||
CollectionId = collectionTwo.Id,
|
||||
StartTime = null,
|
||||
PlayoutDuration = TimeSpan.FromHours(3),
|
||||
PlaybackOrder = PlaybackOrder.Chronological,
|
||||
TailMode = TailMode.Filler,
|
||||
TailCollectionType = ProgramScheduleItemCollectionType.Collection,
|
||||
TailCollection = collectionThree,
|
||||
TailCollectionId = collectionThree.Id
|
||||
}
|
||||
};
|
||||
|
||||
var playout = new Playout
|
||||
{
|
||||
ProgramSchedule = new ProgramSchedule
|
||||
{
|
||||
Items = items
|
||||
},
|
||||
Channel = new Channel(Guid.Empty) { Id = 1, Name = "Test Channel" },
|
||||
};
|
||||
|
||||
var configRepo = new Mock<IConfigElementRepository>();
|
||||
var televisionRepo = new FakeTelevisionRepository();
|
||||
var artistRepo = new Mock<IArtistRepository>();
|
||||
var builder = new PlayoutBuilder(
|
||||
configRepo.Object,
|
||||
fakeRepository,
|
||||
televisionRepo,
|
||||
artistRepo.Object,
|
||||
_logger);
|
||||
|
||||
DateTimeOffset start = HoursAfterMidnight(0);
|
||||
DateTimeOffset finish = start + TimeSpan.FromHours(6);
|
||||
|
||||
Playout result = await builder.BuildPlayoutItems(playout, start, finish);
|
||||
|
||||
result.Items.Count.Should().Be(12);
|
||||
|
||||
result.Items[0].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromMinutes(0));
|
||||
result.Items[0].MediaItemId.Should().Be(1);
|
||||
result.Items[1].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromMinutes(55));
|
||||
result.Items[1].MediaItemId.Should().Be(1);
|
||||
result.Items[2].StartOffset.TimeOfDay.Should().Be(new TimeSpan(1, 50, 0));
|
||||
result.Items[2].MediaItemId.Should().Be(1);
|
||||
|
||||
result.Items[3].StartOffset.TimeOfDay.Should().Be(new TimeSpan(2, 45, 0));
|
||||
result.Items[3].MediaItemId.Should().Be(3);
|
||||
result.Items[4].StartOffset.TimeOfDay.Should().Be(new TimeSpan(2, 50, 0));
|
||||
result.Items[4].MediaItemId.Should().Be(3);
|
||||
result.Items[5].StartOffset.TimeOfDay.Should().Be(new TimeSpan(2, 55, 0));
|
||||
result.Items[5].MediaItemId.Should().Be(3);
|
||||
|
||||
result.Items[6].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(3));
|
||||
result.Items[6].MediaItemId.Should().Be(2);
|
||||
result.Items[7].StartOffset.TimeOfDay.Should().Be(new TimeSpan(3, 55, 0));
|
||||
result.Items[7].MediaItemId.Should().Be(2);
|
||||
result.Items[8].StartOffset.TimeOfDay.Should().Be(new TimeSpan(4, 50, 0));
|
||||
result.Items[8].MediaItemId.Should().Be(2);
|
||||
|
||||
result.Items[9].StartOffset.TimeOfDay.Should().Be(new TimeSpan(5, 45, 0));
|
||||
result.Items[9].MediaItemId.Should().Be(3);
|
||||
result.Items[10].StartOffset.TimeOfDay.Should().Be(new TimeSpan(5, 50, 0));
|
||||
result.Items[10].MediaItemId.Should().Be(3);
|
||||
result.Items[11].StartOffset.TimeOfDay.Should().Be(new TimeSpan(5, 55, 0));
|
||||
result.Items[11].MediaItemId.Should().Be(3);
|
||||
|
||||
result.Anchor.NextScheduleItem.Should().Be(items[0]);
|
||||
result.Anchor.DurationFinish.Should().BeNull();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Duration_Should_Skip_Items_That_Are_Too_Long()
|
||||
{
|
||||
var collectionOne = new Collection
|
||||
{
|
||||
Id = 1,
|
||||
Name = "Duration Items 1",
|
||||
MediaItems = new List<MediaItem>
|
||||
{
|
||||
TestMovie(1, TimeSpan.FromHours(2), new DateTime(2020, 1, 1)),
|
||||
TestMovie(2, TimeSpan.FromHours(1), new DateTime(2020, 1, 1)),
|
||||
TestMovie(3, TimeSpan.FromHours(2), new DateTime(2020, 1, 1)),
|
||||
TestMovie(4, TimeSpan.FromHours(1), new DateTime(2020, 1, 1))
|
||||
}
|
||||
};
|
||||
|
||||
var fakeRepository =
|
||||
new FakeMediaCollectionRepository(Map((collectionOne.Id, collectionOne.MediaItems.ToList())));
|
||||
|
||||
var items = new List<ProgramScheduleItem>
|
||||
{
|
||||
new ProgramScheduleItemDuration
|
||||
{
|
||||
Id = 1,
|
||||
Index = 1,
|
||||
Collection = collectionOne,
|
||||
CollectionId = collectionOne.Id,
|
||||
StartTime = null,
|
||||
PlayoutDuration = TimeSpan.FromHours(1),
|
||||
PlaybackOrder = PlaybackOrder.Chronological,
|
||||
TailMode = TailMode.None,
|
||||
}
|
||||
};
|
||||
|
||||
var playout = new Playout
|
||||
{
|
||||
ProgramSchedule = new ProgramSchedule
|
||||
{
|
||||
Items = items
|
||||
},
|
||||
Channel = new Channel(Guid.Empty) { Id = 1, Name = "Test Channel" },
|
||||
};
|
||||
|
||||
var configRepo = new Mock<IConfigElementRepository>();
|
||||
var televisionRepo = new FakeTelevisionRepository();
|
||||
var artistRepo = new Mock<IArtistRepository>();
|
||||
var builder = new PlayoutBuilder(
|
||||
configRepo.Object,
|
||||
fakeRepository,
|
||||
televisionRepo,
|
||||
artistRepo.Object,
|
||||
_logger);
|
||||
|
||||
DateTimeOffset start = HoursAfterMidnight(0);
|
||||
DateTimeOffset finish = start + TimeSpan.FromHours(6);
|
||||
|
||||
Playout result = await builder.BuildPlayoutItems(playout, start, finish);
|
||||
|
||||
result.Items.Count.Should().Be(6);
|
||||
|
||||
result.Items[0].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(0));
|
||||
result.Items[0].MediaItemId.Should().Be(2);
|
||||
result.Items[1].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(1));
|
||||
result.Items[1].MediaItemId.Should().Be(4);
|
||||
result.Items[2].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(2));
|
||||
result.Items[2].MediaItemId.Should().Be(2);
|
||||
result.Items[3].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(3));
|
||||
result.Items[3].MediaItemId.Should().Be(4);
|
||||
result.Items[4].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(4));
|
||||
result.Items[4].MediaItemId.Should().Be(2);
|
||||
result.Items[5].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(5));
|
||||
result.Items[5].MediaItemId.Should().Be(4);
|
||||
|
||||
result.Anchor.NextScheduleItem.Should().Be(items[0]);
|
||||
result.Anchor.DurationFinish.Should().BeNull();
|
||||
}
|
||||
|
||||
private static DateTimeOffset HoursAfterMidnight(int hours)
|
||||
{
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace ErsatzTV.Core.Domain
|
||||
{
|
||||
[DebuggerDisplay("{MediaItemId} - {Start} - {Finish}")]
|
||||
public class PlayoutItem
|
||||
{
|
||||
public int Id { get; set; }
|
||||
|
||||
@@ -230,7 +230,13 @@ namespace ErsatzTV.Core.Emby
|
||||
foreach (EmbySeason updated in await _televisionRepository.Update(incoming))
|
||||
{
|
||||
incoming.Show = show;
|
||||
await _searchIndex.UpdateItems(_searchRepository, new List<MediaItem> { updated });
|
||||
|
||||
foreach (MediaItem toIndex in await _searchRepository.GetItemToIndex(updated.Id))
|
||||
{
|
||||
await _searchIndex.UpdateItems(
|
||||
_searchRepository,
|
||||
new List<MediaItem> { toIndex });
|
||||
}
|
||||
}
|
||||
},
|
||||
async () =>
|
||||
|
||||
@@ -9,6 +9,10 @@ namespace ErsatzTV.Core.Interfaces.Plex
|
||||
{
|
||||
public interface IPlexServerApiClient
|
||||
{
|
||||
Task<bool> Ping(
|
||||
PlexConnection connection,
|
||||
PlexServerAuthToken token);
|
||||
|
||||
Task<Either<BaseError, List<PlexLibrary>>> GetLibraries(
|
||||
PlexConnection connection,
|
||||
PlexServerAuthToken token);
|
||||
|
||||
@@ -18,7 +18,6 @@ namespace ErsatzTV.Core.Interfaces.Repositories
|
||||
|
||||
Task Update(
|
||||
PlexMediaSource plexMediaSource,
|
||||
List<PlexConnection> prioritizedConnections,
|
||||
List<PlexConnection> toAdd,
|
||||
List<PlexConnection> toDelete);
|
||||
|
||||
|
||||
@@ -175,6 +175,39 @@ namespace ErsatzTV.Core.Iptv
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasCustomTitle && startItem.MediaItem is MusicVideo musicVideo)
|
||||
{
|
||||
foreach (MusicVideoMetadata metadata in musicVideo.MusicVideoMetadata.HeadOrNone())
|
||||
{
|
||||
if (metadata.Year.HasValue)
|
||||
{
|
||||
xml.WriteStartElement("date");
|
||||
xml.WriteString(metadata.Year.Value.ToString());
|
||||
xml.WriteEndElement(); // date
|
||||
}
|
||||
}
|
||||
|
||||
xml.WriteStartElement("category");
|
||||
xml.WriteAttributeString("lang", "en");
|
||||
xml.WriteString("Music");
|
||||
xml.WriteEndElement(); // category
|
||||
|
||||
foreach (MusicVideoMetadata metadata in musicVideo.MusicVideoMetadata.HeadOrNone())
|
||||
{
|
||||
string thumbnail = Optional(metadata.Artwork).Flatten()
|
||||
.Filter(a => a.ArtworkKind == ArtworkKind.Thumbnail)
|
||||
.HeadOrNone()
|
||||
.Match(a => GetArtworkUrl(a, ArtworkKind.Thumbnail), () => string.Empty);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(thumbnail))
|
||||
{
|
||||
xml.WriteStartElement("icon");
|
||||
xml.WriteAttributeString("src", thumbnail);
|
||||
xml.WriteEndElement(); // icon
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (startItem.MediaItem is Episode episode && (!hasCustomTitle || isSameCustomShow))
|
||||
{
|
||||
Option<ShowMetadata> maybeMetadata =
|
||||
|
||||
@@ -230,7 +230,13 @@ namespace ErsatzTV.Core.Jellyfin
|
||||
foreach (JellyfinSeason updated in await _televisionRepository.Update(incoming))
|
||||
{
|
||||
incoming.Show = show;
|
||||
await _searchIndex.UpdateItems(_searchRepository, new List<MediaItem> { updated });
|
||||
|
||||
foreach (MediaItem toIndex in await _searchRepository.GetItemToIndex(updated.Id))
|
||||
{
|
||||
await _searchIndex.UpdateItems(
|
||||
_searchRepository,
|
||||
new List<MediaItem> { toIndex });
|
||||
}
|
||||
}
|
||||
},
|
||||
async () =>
|
||||
|
||||
@@ -302,9 +302,20 @@ namespace ErsatzTV.Core.Scheduling
|
||||
playoutItem.CustomTitle = scheduleItem.CustomTitle;
|
||||
}
|
||||
|
||||
currentTime = itemStartTime + version.Duration;
|
||||
enumerator.MoveNext();
|
||||
|
||||
if (scheduleItem is ProgramScheduleItemDuration d &&
|
||||
version.Duration > d.PlayoutDuration)
|
||||
{
|
||||
_logger.LogWarning(
|
||||
"Skipping playout item {Title} with duration {Duration} that is longer than schedule item duration {PlayoutDuration}",
|
||||
DisplayTitle(mediaItem),
|
||||
version.Duration,
|
||||
d.PlayoutDuration);
|
||||
return;
|
||||
}
|
||||
|
||||
currentTime = itemStartTime + version.Duration;
|
||||
playout.Items.Add(playoutItem);
|
||||
|
||||
switch (scheduleItem)
|
||||
@@ -442,6 +453,10 @@ namespace ErsatzTV.Core.Scheduling
|
||||
durationFinish,
|
||||
collectionEnumerators))
|
||||
{
|
||||
// if we're starting filler, we don't actually need to move
|
||||
// to the next schedule item yet
|
||||
index--;
|
||||
|
||||
inDurationFiller = true;
|
||||
durationFinish.Do(
|
||||
f => playoutItem.GuideFinish = f.UtcDateTime);
|
||||
|
||||
@@ -233,7 +233,9 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
|
||||
}
|
||||
|
||||
var paths = incomingMetadata.Artwork.Map(a => a.Path).ToList();
|
||||
foreach (Artwork artworkToRemove in metadata.Artwork.Filter(a => !paths.Contains(a.Path)))
|
||||
foreach (Artwork artworkToRemove in metadata.Artwork
|
||||
.Filter(a => !paths.Contains(a.Path))
|
||||
.ToList())
|
||||
{
|
||||
metadata.Artwork.Remove(artworkToRemove);
|
||||
}
|
||||
@@ -370,7 +372,9 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
|
||||
}
|
||||
|
||||
var paths = incomingMetadata.Artwork.Map(a => a.Path).ToList();
|
||||
foreach (Artwork artworkToRemove in metadata.Artwork.Filter(a => !paths.Contains(a.Path)))
|
||||
foreach (Artwork artworkToRemove in metadata.Artwork
|
||||
.Filter(a => !paths.Contains(a.Path))
|
||||
.ToList())
|
||||
{
|
||||
metadata.Artwork.Remove(artworkToRemove);
|
||||
}
|
||||
@@ -526,7 +530,9 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
|
||||
}
|
||||
|
||||
var paths = incomingMetadata.Artwork.Map(a => a.Path).ToList();
|
||||
foreach (Artwork artworkToRemove in metadata.Artwork.Filter(a => !paths.Contains(a.Path)))
|
||||
foreach (Artwork artworkToRemove in metadata.Artwork
|
||||
.Filter(a => !paths.Contains(a.Path))
|
||||
.ToList())
|
||||
{
|
||||
metadata.Artwork.Remove(artworkToRemove);
|
||||
}
|
||||
|
||||
@@ -250,7 +250,9 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
|
||||
}
|
||||
|
||||
var paths = incomingMetadata.Artwork.Map(a => a.Path).ToList();
|
||||
foreach (Artwork artworkToRemove in metadata.Artwork.Filter(a => !paths.Contains(a.Path)))
|
||||
foreach (Artwork artworkToRemove in metadata.Artwork
|
||||
.Filter(a => !paths.Contains(a.Path))
|
||||
.ToList())
|
||||
{
|
||||
metadata.Artwork.Remove(artworkToRemove);
|
||||
}
|
||||
@@ -370,7 +372,9 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
|
||||
}
|
||||
|
||||
var paths = incomingMetadata.Artwork.Map(a => a.Path).ToList();
|
||||
foreach (Artwork artworkToRemove in metadata.Artwork.Filter(a => !paths.Contains(a.Path)))
|
||||
foreach (Artwork artworkToRemove in metadata.Artwork
|
||||
.Filter(a => !paths.Contains(a.Path))
|
||||
.ToList())
|
||||
{
|
||||
metadata.Artwork.Remove(artworkToRemove);
|
||||
}
|
||||
@@ -527,7 +531,9 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
|
||||
}
|
||||
|
||||
var paths = incomingMetadata.Artwork.Map(a => a.Path).ToList();
|
||||
foreach (Artwork artworkToRemove in metadata.Artwork.Filter(a => !paths.Contains(a.Path)))
|
||||
foreach (Artwork artworkToRemove in metadata.Artwork
|
||||
.Filter(a => !paths.Contains(a.Path))
|
||||
.ToList())
|
||||
{
|
||||
metadata.Artwork.Remove(artworkToRemove);
|
||||
}
|
||||
|
||||
@@ -81,7 +81,7 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
|
||||
|
||||
public async Task<Option<PlexMediaSource>> GetPlexByLibraryId(int plexLibraryId)
|
||||
{
|
||||
int? id = await _dbConnection.QuerySingleAsync<int?>(
|
||||
int? id = await _dbConnection.QuerySingleOrDefaultAsync<int?>(
|
||||
@"SELECT L.MediaSourceId FROM Library L
|
||||
INNER JOIN PlexLibrary PL on L.Id = PL.Id
|
||||
WHERE L.Id = @PlexLibraryId",
|
||||
@@ -113,55 +113,30 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
|
||||
|
||||
public async Task Update(
|
||||
PlexMediaSource plexMediaSource,
|
||||
List<PlexConnection> sortedConnections,
|
||||
List<PlexConnection> toAdd,
|
||||
List<PlexConnection> toDelete)
|
||||
{
|
||||
await _dbConnection.ExecuteAsync(
|
||||
@"UPDATE PlexMediaSource SET
|
||||
ProductVersion = @ProductVersion,
|
||||
Platform = @Platform,
|
||||
PlatformVersion = @PlatformVersion,
|
||||
ServerName = @ServerName
|
||||
WHERE Id = @Id",
|
||||
new
|
||||
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
|
||||
|
||||
dbContext.Entry(plexMediaSource).State = EntityState.Modified;
|
||||
|
||||
if (toAdd.Any() || toDelete.Any())
|
||||
{
|
||||
plexMediaSource.Connections.Clear();
|
||||
await dbContext.Entry(plexMediaSource).Collection(pms => pms.Connections).LoadAsync();
|
||||
|
||||
plexMediaSource.Connections.AddRange(toAdd);
|
||||
plexMediaSource.Connections.RemoveAll(toDelete.Contains);
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (PlexConnection connection in plexMediaSource.Connections)
|
||||
{
|
||||
plexMediaSource.ProductVersion,
|
||||
plexMediaSource.Platform,
|
||||
plexMediaSource.PlatformVersion,
|
||||
plexMediaSource.ServerName,
|
||||
plexMediaSource.Id
|
||||
});
|
||||
|
||||
foreach (PlexConnection add in toAdd)
|
||||
{
|
||||
await _dbConnection.ExecuteAsync(
|
||||
@"INSERT INTO PlexConnection (IsActive, Uri, PlexMediaSourceId)
|
||||
VALUES (0, @Uri, @PlexMediaSourceId)",
|
||||
new { add.Uri, PlexMediaSourceId = plexMediaSource.Id });
|
||||
dbContext.Entry(connection).State = EntityState.Modified;
|
||||
}
|
||||
}
|
||||
|
||||
foreach (PlexConnection delete in toDelete)
|
||||
{
|
||||
await _dbConnection.ExecuteAsync(
|
||||
@"DELETE FROM PlexConnection WHERE Id = @Id",
|
||||
new { delete.Id });
|
||||
}
|
||||
|
||||
int activeCount = await _dbConnection.QuerySingleAsync<int>(
|
||||
@"SELECT COUNT(*) FROM PlexConnection WHERE IsActive = 1 AND PlexMediaSourceId = @PlexMediaSourceId",
|
||||
new { PlexMediaSourceId = plexMediaSource.Id });
|
||||
if (activeCount == 0)
|
||||
{
|
||||
Option<PlexConnection> toActivate =
|
||||
sortedConnections.FirstOrDefault(c => toDelete.All(d => d.Id != c.Id));
|
||||
|
||||
// update on uri because connections from Plex API don't have our local ids
|
||||
await toActivate.IfSomeAsync(
|
||||
async c => await _dbConnection.ExecuteAsync(
|
||||
@"UPDATE PlexConnection SET IsActive = 1 WHERE Uri = @Uri",
|
||||
new { c.Uri }));
|
||||
}
|
||||
await dbContext.SaveChangesAsync();
|
||||
}
|
||||
|
||||
public async Task<List<int>> UpdateLibraries(
|
||||
@@ -587,7 +562,7 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
|
||||
|
||||
public async Task<Option<JellyfinMediaSource>> GetJellyfinByLibraryId(int jellyfinLibraryId)
|
||||
{
|
||||
int? id = await _dbConnection.QuerySingleAsync<int?>(
|
||||
int? id = await _dbConnection.QuerySingleOrDefaultAsync<int?>(
|
||||
@"SELECT L.MediaSourceId FROM Library L
|
||||
INNER JOIN JellyfinLibrary PL on L.Id = PL.Id
|
||||
WHERE L.Id = @JellyfinLibraryId",
|
||||
@@ -770,7 +745,7 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
|
||||
|
||||
public async Task<Option<EmbyMediaSource>> GetEmbyByLibraryId(int embyLibraryId)
|
||||
{
|
||||
int? id = await _dbConnection.QuerySingleAsync<int?>(
|
||||
int? id = await _dbConnection.QuerySingleOrDefaultAsync<int?>(
|
||||
@"SELECT L.MediaSourceId FROM Library L
|
||||
INNER JOIN EmbyLibrary PL on L.Id = PL.Id
|
||||
WHERE L.Id = @EmbyLibraryId",
|
||||
|
||||
@@ -6,6 +6,12 @@ namespace ErsatzTV.Infrastructure.Plex
|
||||
{
|
||||
public interface IPlexServerApi
|
||||
{
|
||||
[Get("/")]
|
||||
[Headers("Accept: application/json")]
|
||||
public Task Ping(
|
||||
[Query] [AliasAs("X-Plex-Token")]
|
||||
string token);
|
||||
|
||||
[Get("/library/sections")]
|
||||
[Headers("Accept: application/json")]
|
||||
public Task<PlexMediaContainerResponse<PlexMediaContainerDirectoryContent<PlexLibraryResponse>>> GetLibraries(
|
||||
|
||||
@@ -30,6 +30,28 @@ namespace ErsatzTV.Infrastructure.Plex
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task<bool> Ping(
|
||||
PlexConnection connection,
|
||||
PlexServerAuthToken token)
|
||||
{
|
||||
try
|
||||
{
|
||||
IPlexServerApi service = RestService.For<IPlexServerApi>(
|
||||
new HttpClient
|
||||
{
|
||||
BaseAddress = new Uri(connection.Uri),
|
||||
Timeout = TimeSpan.FromSeconds(5)
|
||||
});
|
||||
|
||||
await service.Ping(token.AuthToken);
|
||||
return true;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<Either<BaseError, List<PlexLibrary>>> GetLibraries(
|
||||
PlexConnection connection,
|
||||
PlexServerAuthToken token)
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"Serilog.Sinks.Console"
|
||||
],
|
||||
"MinimumLevel": {
|
||||
"Default": "Debug",
|
||||
"Default": "Information",
|
||||
"Override": {
|
||||
"Microsoft": "Warning",
|
||||
"System.Net.Http.HttpClient": "Warning"
|
||||
|
||||
Reference in New Issue
Block a user