Compare commits

...
Author SHA1 Message Date
Jason DoveandGitHub db054ece24 Database migrations (#13)
* remove last use of dbcontextfactory

* add initial migration
2021-02-12 12:50:04 +00:00
Jason DoveandGitHub c2d8a54a47 catch ffprobe errors parsing statistics (#11) 2021-02-12 03:09:52 +00:00
Jason Dove 88b645af2d keep releases as prerelease 2021-02-11 15:54:00 -06:00
Jason DoveandGitHub 941f1a59ee fix HDHR channel routes (#10) 2021-02-11 20:47:33 +00:00
Jason DoveandGitHub a3e20826a5 Movie metadata fixes (#9)
* reorganize metadata parsing; only attempt to parse appropriate media type based on media source configuration

* add fallback metadata for movie sources

* only request read access for nfo metadata

* fix tests
2021-02-11 19:33:59 +00:00
Jason DoveandGitHub ebff29d6cd Xml and scanner fixes (#7)
* flush xml, use utf8

* scan ts files

* use links instead of icons for m3u, xmltv, api
2021-02-11 15:35:32 +00:00
Jason DoveandGitHub 5a29fc1cbb fix docker-compose port mapping (#6) 2021-02-11 13:11:30 +00:00
Jason DoveandGitHub f4487eb422 use xml writer so strings are properly escaped (#4) 2021-02-11 12:18:20 +00:00
Jason DoveandGitHub d04f3d574b Fix fallback metadata parsing for TV (#3)
* fix yml formatting

* test build action

* move ci workflow

* fix unit tests from dotnet test command

* add release workflow

* try another matrix syntax

* fix path to csproj

* fix target framework

* more release fixes

* port fixes for running outside of docker

* more search fixes

* improve fallback metadata parsing, add tests

* fix odd behavior when searching collections

* more fallback metadata fixes

* refresh "other" metadata on startup
2021-02-11 11:57:32 +00:00
Jason Dove a2e85655d2 include cli in release builds 2021-02-10 21:37:31 -06:00
Jason Dove 4d49289dbd relax case sensitivity in searches 2021-02-10 21:29:54 -06:00
48 changed files with 3174 additions and 211 deletions
+3
View File
@@ -21,6 +21,9 @@ jobs:
with:
dotnet-version: 5.0.x
- name: Clean
run: dotnet clean --configuration Release && dotnet nuget locals all --clear
- name: Install dependencies
run: dotnet restore
+6
View File
@@ -39,23 +39,29 @@ jobs:
# Define some variables for things we need
tag=$(git describe --tags --abbrev=0)
release_name="ErsatzTV-$tag-${{ matrix.target }}"
release_name_cli="ErsatzTV.CommandLine-$tag-${{ matrix.target }}"
# Build everything
dotnet publish ErsatzTV/ErsatzTV.csproj --framework net5.0 --runtime "${{ matrix.target }}" -c Release -o "$release_name"
dotnet publish ErsatzTV.CommandLine/ErsatzTV.CommandLine.csproj --framework net5.0 --runtime "${{ matrix.target }}" -c Release -o "$release_name_cli"
# Pack files
if [ "${{ matrix.target }}" == "win-x64" ]; then
7z a -tzip "${release_name}.zip" "./${release_name}/*"
7z a -tzip "${release_name_cli}.zip" "./${release_name_cli}/*"
else
tar czvf "${release_name}.tar.gz" "$release_name"
tar czvf "${release_name_cli}.tar.gz" "$release_name_cli"
fi
# Delete output directory
rm -r "$release_name"
rm -r "$release_name_cli"
- name: Publish
uses: softprops/action-gh-release@v1
with:
prerelease: true
files: |
ErsatzTV*.zip
ErsatzTV*.tar.gz
@@ -0,0 +1,9 @@
using System.Collections.Generic;
using ErsatzTV.Core;
using LanguageExt;
namespace ErsatzTV.Application.MediaCollections.Commands
{
public record AddItemsToSimpleMediaCollection
(int MediaCollectionId, List<int> ItemIds) : MediatR.IRequest<Either<BaseError, Unit>>;
}
@@ -0,0 +1,79 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Repositories;
using LanguageExt;
using static LanguageExt.Prelude;
namespace ErsatzTV.Application.MediaCollections.Commands
{
public class
AddItemsToSimpleMediaCollectionHandler : MediatR.IRequestHandler<AddItemsToSimpleMediaCollection,
Either<BaseError, Unit>>
{
private readonly IMediaCollectionRepository _mediaCollectionRepository;
private readonly IMediaItemRepository _mediaItemRepository;
public AddItemsToSimpleMediaCollectionHandler(
IMediaCollectionRepository mediaCollectionRepository,
IMediaItemRepository mediaItemRepository)
{
_mediaCollectionRepository = mediaCollectionRepository;
_mediaItemRepository = mediaItemRepository;
}
public Task<Either<BaseError, Unit>> Handle(
AddItemsToSimpleMediaCollection request,
CancellationToken cancellationToken) =>
Validate(request)
.MapT(ApplyAddItemsRequest)
.Bind(v => v.ToEitherAsync());
private async Task<Unit> ApplyAddItemsRequest(RequestParameters parameters)
{
foreach (MediaItem item in parameters.ItemsToAdd.Where(
item => parameters.Collection.Items.All(i => i.Id != item.Id)))
{
parameters.Collection.Items.Add(item);
}
await _mediaCollectionRepository.Update(parameters.Collection);
return Unit.Default;
}
private async Task<Validation<BaseError, RequestParameters>>
Validate(AddItemsToSimpleMediaCollection request) =>
(await SimpleMediaCollectionMustExist(request), await ValidateItems(request))
.Apply(
(simpleMediaCollectionToUpdate, itemsToAdd) =>
new RequestParameters(simpleMediaCollectionToUpdate, itemsToAdd));
private Task<Validation<BaseError, SimpleMediaCollection>> SimpleMediaCollectionMustExist(
AddItemsToSimpleMediaCollection updateSimpleMediaCollection) =>
_mediaCollectionRepository.GetSimpleMediaCollection(updateSimpleMediaCollection.MediaCollectionId)
.Map(v => v.ToValidation<BaseError>("SimpleMediaCollection does not exist."));
private Task<Validation<BaseError, List<MediaItem>>> ValidateItems(
AddItemsToSimpleMediaCollection request) =>
LoadAllMediaItems(request)
.Map(v => v.ToValidation<BaseError>("MediaItem does not exist"));
private async Task<Option<List<MediaItem>>> LoadAllMediaItems(AddItemsToSimpleMediaCollection request)
{
var items = (await request.ItemIds.Map(async id => await _mediaItemRepository.Get(id)).Sequence())
.ToList();
if (items.Any(i => i.IsNone))
{
return None;
}
return items.Somes().ToList();
}
private record RequestParameters(SimpleMediaCollection Collection, List<MediaItem> ItemsToAdd);
}
}
@@ -0,0 +1,11 @@
using System;
using System.Collections.Generic;
using ErsatzTV.Application.MediaItems;
using LanguageExt;
using MediatR;
namespace ErsatzTV.Application.MediaCollections.Queries
{
public record GetSimpleMediaCollectionWithItemsById
(int Id) : IRequest<Option<Tuple<MediaCollectionViewModel, List<MediaItemSearchResultViewModel>>>>;
}
@@ -0,0 +1,37 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using ErsatzTV.Application.MediaItems;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Repositories;
using LanguageExt;
using MediatR;
using static LanguageExt.Prelude;
using static ErsatzTV.Application.MediaCollections.Mapper;
using static ErsatzTV.Application.MediaItems.Mapper;
namespace ErsatzTV.Application.MediaCollections.Queries
{
public class GetSimpleMediaCollectionWithItemsByIdHandler : IRequestHandler<GetSimpleMediaCollectionWithItemsById,
Option<Tuple<MediaCollectionViewModel, List<MediaItemSearchResultViewModel>>>>
{
private readonly IMediaCollectionRepository _mediaCollectionRepository;
public GetSimpleMediaCollectionWithItemsByIdHandler(IMediaCollectionRepository mediaCollectionRepository) =>
_mediaCollectionRepository = mediaCollectionRepository;
public async Task<Option<Tuple<MediaCollectionViewModel, List<MediaItemSearchResultViewModel>>>> Handle(
GetSimpleMediaCollectionWithItemsById request,
CancellationToken cancellationToken)
{
Option<SimpleMediaCollection> maybeCollection =
await _mediaCollectionRepository.GetSimpleMediaCollectionWithItems(request.Id);
return maybeCollection.Match<Option<Tuple<MediaCollectionViewModel, List<MediaItemSearchResultViewModel>>>>(
c => Tuple(ProjectToViewModel(c), c.Items.Map(ProjectToSearchViewModel).ToList()),
None);
}
}
}
+22
View File
@@ -1,4 +1,5 @@
using ErsatzTV.Core.Domain;
using static LanguageExt.Prelude;
namespace ErsatzTV.Application.MediaItems
{
@@ -9,5 +10,26 @@ namespace ErsatzTV.Application.MediaItems
mediaItem.Id,
mediaItem.MediaSourceId,
mediaItem.Path);
internal static MediaItemSearchResultViewModel ProjectToSearchViewModel(MediaItem mediaItem) =>
new(
mediaItem.Id,
mediaItem.Source.Name,
mediaItem.Metadata.MediaType.ToString(),
GetDisplayTitle(mediaItem),
GetDisplayDuration(mediaItem));
private static string GetDisplayTitle(this MediaItem mediaItem) =>
mediaItem.Metadata.MediaType == MediaType.TvShow &&
Optional(mediaItem.Metadata.SeasonNumber).IsSome &&
Optional(mediaItem.Metadata.EpisodeNumber).IsSome
? $"{mediaItem.Metadata.Title} s{mediaItem.Metadata.SeasonNumber:00}e{mediaItem.Metadata.EpisodeNumber:00}"
: mediaItem.Metadata.Title;
private static string GetDisplayDuration(MediaItem mediaItem) =>
string.Format(
mediaItem.Metadata.Duration.TotalHours >= 1 ? @"{0:h\:mm\:ss}" : @"{0:mm\:ss}",
mediaItem.Metadata.Duration);
}
}
@@ -0,0 +1,9 @@
namespace ErsatzTV.Application.MediaItems
{
public record MediaItemSearchResultViewModel(
int Id,
string Source,
string MediaType,
string Title,
string Duration);
}
@@ -1,4 +1,5 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
@@ -24,7 +25,9 @@ namespace ErsatzTV.Application.MediaItems.Queries
if (!string.IsNullOrEmpty(request.SearchString))
{
allItems = allItems.Filter(i => i.Metadata?.Title.Contains(request.SearchString) == true);
allItems = allItems.Filter(
i => i.Metadata?.Title.Contains(request.SearchString, StringComparison.OrdinalIgnoreCase) ==
true);
}
return allItems.GroupBy(c => new { c.Source.Name, c.Metadata.Title }).Map(
@@ -3,6 +3,7 @@ using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using ErsatzTV.Core.Interfaces.Repositories;
using LanguageExt;
using MediatR;
using static ErsatzTV.Application.MediaItems.Mapper;
@@ -15,9 +16,9 @@ namespace ErsatzTV.Application.MediaItems.Queries
public GetAllMediaItemsHandler(IMediaItemRepository mediaItemRepository) =>
_mediaItemRepository = mediaItemRepository;
public async Task<List<MediaItemViewModel>> Handle(
public Task<List<MediaItemViewModel>> Handle(
GetAllMediaItems request,
CancellationToken cancellationToken) =>
(await _mediaItemRepository.GetAll()).Map(ProjectToViewModel).ToList();
_mediaItemRepository.GetAll().Map(list => list.Map(ProjectToViewModel).ToList());
}
}
@@ -0,0 +1,7 @@
using System.Collections.Generic;
using MediatR;
namespace ErsatzTV.Application.MediaItems.Queries
{
public record SearchAllMediaItems(string SearchString) : IRequest<List<MediaItemSearchResultViewModel>>;
}
@@ -0,0 +1,23 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using ErsatzTV.Core.Interfaces.Repositories;
using LanguageExt;
using MediatR;
using static ErsatzTV.Application.MediaItems.Mapper;
namespace ErsatzTV.Application.MediaItems.Queries
{
public class SearchAllMediaItemsHandler : IRequestHandler<SearchAllMediaItems, List<MediaItemSearchResultViewModel>>
{
private readonly IMediaItemRepository _mediaItemRepository;
public SearchAllMediaItemsHandler(IMediaItemRepository mediaItemRepository) =>
_mediaItemRepository = mediaItemRepository;
public Task<List<MediaItemSearchResultViewModel>>
Handle(SearchAllMediaItems request, CancellationToken cancellationToken) =>
_mediaItemRepository.Search(request.SearchString).Map(list => list.Map(ProjectToSearchViewModel).ToList());
}
}
+1 -1
View File
@@ -28,7 +28,7 @@ namespace ErsatzTV.Application.Playouts
? $"{mediaItem.Metadata.Title} s{mediaItem.Metadata.SeasonNumber:00}e{mediaItem.Metadata.EpisodeNumber:00}"
: mediaItem.Metadata.Title;
public static string GetDisplayDuration(MediaItem mediaItem) =>
private static string GetDisplayDuration(MediaItem mediaItem) =>
string.Format(
mediaItem.Metadata.Duration.TotalHours >= 1 ? @"{0:h\:mm\:ss}" : @"{0:mm\:ss}",
mediaItem.Metadata.Duration);
@@ -6,6 +6,7 @@ using NUnit.Framework;
namespace ErsatzTV.Core.Tests.FFmpeg
{
[TestFixture]
public class FFmpegPlaybackSettingsCalculatorTests
{
public class CalculateSettings
@@ -22,6 +22,9 @@ namespace ErsatzTV.Core.Tests.Fakes
public Task<Option<SimpleMediaCollection>> GetSimpleMediaCollection(int id) =>
throw new NotSupportedException();
public Task<Option<SimpleMediaCollection>> GetSimpleMediaCollectionWithItems(int id) =>
throw new NotSupportedException();
public Task<Option<TelevisionMediaCollection>> GetTelevisionMediaCollection(int id) =>
throw new NotSupportedException();
@@ -0,0 +1,49 @@
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Metadata;
using FluentAssertions;
using NUnit.Framework;
namespace ErsatzTV.Core.Tests.Metadata
{
[TestFixture]
public class FallbackMetadataProviderTests
{
[Test]
[TestCase("Awesome Show - s01e02.mkv", "Awesome Show", 1, 2)]
[TestCase("Awesome Show - S01E02.mkv", "Awesome Show", 1, 2)]
[TestCase("Awesome Show - s1e2.mkv", "Awesome Show", 1, 2)]
[TestCase("Awesome Show - S1E2.mkv", "Awesome Show", 1, 2)]
[TestCase("Awesome Show - s01e02 - Episode Title.mkv", "Awesome Show", 1, 2)]
[TestCase("Awesome Show - S01E02 - Episode Title.mkv", "Awesome Show", 1, 2)]
[TestCase("Awesome Show - s1e2 - Episode Title.mkv", "Awesome Show", 1, 2)]
[TestCase("Awesome Show - S1E2 - Episode Title.mkv", "Awesome Show", 1, 2)]
[TestCase("Awesome Show (2021) - s01e02 - Episode Title.mkv", "Awesome Show (2021)", 1, 2)]
[TestCase("Awesome Show (2021) - S01E02 - Episode Title.mkv", "Awesome Show (2021)", 1, 2)]
[TestCase("Awesome Show (2021) - s1e2 - Episode Title.mkv", "Awesome Show (2021)", 1, 2)]
[TestCase("Awesome Show (2021) - S1E2 - Episode Title.mkv", "Awesome Show (2021)", 1, 2)]
[TestCase("Awesome Show - s01e02 - Episode Title-720p.mkv", "Awesome Show", 1, 2)]
[TestCase("Awesome Show - S01E02 - Episode Title-720p.mkv", "Awesome Show", 1, 2)]
[TestCase("Awesome Show - s1e2 - Episode Title-720p.mkv", "Awesome Show", 1, 2)]
[TestCase("Awesome Show - S1E2 - Episode Title-720p.mkv", "Awesome Show", 1, 2)]
[TestCase(
"Awesome Show (2021) - S01E02 - Description; More Description (1080p QUALITY codec GROUP).mkv",
"Awesome Show (2021)",
1,
2)]
[TestCase(
"Awesome.Show.S01E02.Description.more.Description.QUAlity.codec.CODEC-GROUP.mkv",
"Awesome.Show",
1,
2)]
public void GetFallbackMetadata_ShouldHandleVariousFormats(string path, string title, int season, int episode)
{
MediaMetadata metadata = FallbackMetadataProvider.GetFallbackMetadata(
new MediaItem { Path = path, Source = new LocalMediaSource { MediaType = MediaType.TvShow } });
metadata.MediaType.Should().Be(MediaType.TvShow);
metadata.Title.Should().Be(title);
metadata.SeasonNumber.Should().Be(season);
metadata.EpisodeNumber.Should().Be(episode);
}
}
}
@@ -9,6 +9,7 @@ using static LanguageExt.Prelude;
namespace ErsatzTV.Core.Tests.Scheduling
{
[TestFixture]
public class ChronologicalContentTests
{
[Test]
@@ -14,6 +14,7 @@ using static LanguageExt.Prelude;
namespace ErsatzTV.Core.Tests.Scheduling
{
[TestFixture]
public class PlayoutBuilderTests
{
private readonly ILogger<PlayoutBuilder> _logger;
@@ -9,6 +9,7 @@ using static LanguageExt.Prelude;
namespace ErsatzTV.Core.Tests.Scheduling
{
[TestFixture]
public class RandomizedContentTests
{
private const int KnownSeed = 22295;
@@ -9,6 +9,7 @@ using static LanguageExt.Prelude;
namespace ErsatzTV.Core.Tests.Scheduling
{
[TestFixture]
public class ShuffledContentTests
{
// this seed will produce (shuffle) 1-10 in order
+6 -1
View File
@@ -19,6 +19,11 @@ namespace ErsatzTV.Core.Hdhr
public string GuideNumber => _channel.Number.ToString();
public string GuideName => _channel.Name;
public string URL => $"{_scheme}://{_host}/iptv/channel/{_channel.Number}";
public string URL => _channel.StreamingMode switch
{
StreamingMode.HttpLiveStreaming => $"{_scheme}://{_host}/iptv/channel/{_channel.Number}.m3u8",
_ => $"{_scheme}://{_host}/iptv/channel/{_channel.Number}.ts"
};
}
}
@@ -11,6 +11,7 @@ namespace ErsatzTV.Core.Interfaces.Repositories
public Task<SimpleMediaCollection> Add(SimpleMediaCollection collection);
public Task<Option<MediaCollection>> Get(int id);
public Task<Option<SimpleMediaCollection>> GetSimpleMediaCollection(int id);
public Task<Option<SimpleMediaCollection>> GetSimpleMediaCollectionWithItems(int id);
public Task<Option<TelevisionMediaCollection>> GetTelevisionMediaCollection(int id);
public Task<List<SimpleMediaCollection>> GetSimpleMediaCollections();
public Task<List<MediaCollection>> GetAll();
@@ -10,6 +10,7 @@ namespace ErsatzTV.Core.Interfaces.Repositories
public Task<int> Add(MediaItem mediaItem);
public Task<Option<MediaItem>> Get(int id);
public Task<List<MediaItem>> GetAll();
public Task<List<MediaItem>> Search(string searchString);
public Task<List<MediaItem>> GetAll(MediaType mediaType);
public Task<List<MediaItem>> GetAllByMediaSourceId(int mediaSourceId);
public Task Update(MediaItem mediaItem);
+58 -24
View File
@@ -2,6 +2,7 @@
using System.IO;
using System.Linq;
using System.Text;
using System.Xml;
using ErsatzTV.Core.Domain;
using static LanguageExt.Prelude;
@@ -22,20 +23,32 @@ namespace ErsatzTV.Core.Iptv
public string ToXml()
{
var sb = new StringBuilder();
sb.AppendLine("<?xml version=\"1.0\"?>");
sb.AppendLine("<tv generator-info-name=\"ersatztv\">");
using var ms = new MemoryStream();
using var xml = XmlWriter.Create(ms);
xml.WriteStartDocument();
xml.WriteStartElement("tv");
xml.WriteAttributeString("generator-info-name", "ersatztv");
foreach (Channel channel in _channels)
{
sb.AppendLine($"<channel id=\"{channel.Number}\">");
sb.AppendLine($"<display-name lang=\"en\">{channel.Name}</display-name>");
sb.AppendLine(
!string.IsNullOrWhiteSpace(channel.Logo)
? $"<icon src=\"{_scheme}://{_host}/iptv/images/{channel.Logo}\"/>"
: $"<icon src=\"{_scheme}://{_host}/images/ersatztv-500.png\"/>");
xml.WriteStartElement("channel");
xml.WriteAttributeString("id", channel.Number.ToString());
sb.AppendLine("</channel>");
xml.WriteStartElement("display-name");
xml.WriteAttributeString("lang", "en");
xml.WriteString(channel.Name);
xml.WriteEndElement(); // display-name
xml.WriteStartElement("icon");
xml.WriteAttributeString(
"src",
!string.IsNullOrWhiteSpace(channel.Logo)
? $"{_scheme}://{_host}/iptv/images/{channel.Logo}"
: $"{_scheme}://{_host}/images/ersatztv-500.png");
xml.WriteEndElement(); // icon
xml.WriteEndElement(); // channel
}
foreach (Channel channel in _channels)
@@ -50,37 +63,58 @@ namespace ErsatzTV.Core.Iptv
Title = Path.GetFileName(playoutItem.MediaItem.Path)
});
sb.AppendLine(
$"<programme start=\"{start}\" stop=\"{stop}\" channel=\"{channel.Number}\">");
sb.AppendLine($"<title lang=\"en\">{metadata.Title}</title>");
sb.AppendLine("<previously-shown/>");
sb.AppendLine("<sub-title lang=\"en\"></sub-title>");
xml.WriteStartElement("programme");
xml.WriteAttributeString("start", start);
xml.WriteAttributeString("stop", stop);
xml.WriteAttributeString("channel", channel.Number.ToString());
xml.WriteStartElement("title");
xml.WriteAttributeString("lang", "en");
xml.WriteString(metadata.Title);
xml.WriteEndElement(); // title
xml.WriteStartElement("previously-shown");
xml.WriteEndElement(); // previously-shown
xml.WriteStartElement("sub-title");
xml.WriteAttributeString("lang", "en");
xml.WriteEndElement(); // sub-title
int season = Optional(metadata.SeasonNumber).IfNone(0);
int episode = Optional(metadata.EpisodeNumber).IfNone(0);
if (season > 0 && episode > 0)
{
sb.AppendLine($"<episode-num system=\"xmltv_ns\">{season - 1}.{episode - 1}.0/1</episode-num>");
xml.WriteStartElement("episode-num");
xml.WriteAttributeString("system", "xmltv_ns");
xml.WriteString($"{season - 1}.{episode - 1}.0/1");
xml.WriteEndElement(); // episode-num
}
// sb.AppendLine("<icon src=\"\"/>");
sb.AppendLine($"<desc lang=\"en\">{metadata.Description}</desc>");
xml.WriteStartElement("desc");
xml.WriteAttributeString("lang", "en");
xml.WriteString(metadata.Description);
xml.WriteEndElement(); // desc
if (!string.IsNullOrWhiteSpace(metadata.ContentRating))
{
sb.AppendLine("<rating system=\"MPAA\">");
sb.AppendLine($"<value>{metadata.ContentRating}</value>");
sb.AppendLine("</rating>");
xml.WriteStartElement("rating");
xml.WriteAttributeString("system", "MPAA");
xml.WriteStartElement("value");
xml.WriteString(metadata.ContentRating);
xml.WriteEndElement(); // value
xml.WriteEndElement(); // rating
}
sb.AppendLine("</programme>");
xml.WriteEndElement(); // programme
}
}
sb.AppendLine("</tv>");
xml.WriteEndElement(); // tv
xml.WriteEndDocument();
return sb.ToString();
xml.Flush();
return Encoding.UTF8.GetString(ms.ToArray());
}
}
}
@@ -0,0 +1,76 @@
using System;
using System.IO;
using System.Text.RegularExpressions;
using ErsatzTV.Core.Domain;
namespace ErsatzTV.Core.Metadata
{
public static class FallbackMetadataProvider
{
public static MediaMetadata GetFallbackMetadata(MediaItem mediaItem)
{
string fileName = Path.GetFileName(mediaItem.Path);
var metadata = new MediaMetadata { Title = fileName ?? mediaItem.Path };
if (fileName != null)
{
if (!(mediaItem.Source is LocalMediaSource localMediaSource))
{
return metadata;
}
return localMediaSource.MediaType switch
{
MediaType.TvShow => GetTvShowMetadata(fileName, metadata),
MediaType.Movie => GetMovieMetadata(fileName, metadata),
_ => metadata
};
}
return metadata;
}
private static MediaMetadata GetTvShowMetadata(string fileName, MediaMetadata metadata)
{
try
{
const string PATTERN = @"^(.*?)[.\s-]+[sS](\d+)[eE](\d+).*\.\w+$";
Match match = Regex.Match(fileName, PATTERN);
if (match.Success)
{
metadata.MediaType = MediaType.TvShow;
metadata.Title = match.Groups[1].Value;
metadata.SeasonNumber = int.Parse(match.Groups[2].Value);
metadata.EpisodeNumber = int.Parse(match.Groups[3].Value);
}
}
catch (Exception)
{
// ignored
}
return metadata;
}
private static MediaMetadata GetMovieMetadata(string fileName, MediaMetadata metadata)
{
try
{
const string PATTERN = @"^(.*?)[.\(](\d{4})[.\)].*\.\w+$";
Match match = Regex.Match(fileName, PATTERN);
if (match.Success)
{
metadata.MediaType = MediaType.Movie;
metadata.Title = match.Groups[1].Value;
metadata.Aired = new DateTime(int.Parse(match.Groups[2].Value), 1, 1);
}
}
catch (Exception)
{
// ignored
}
return metadata;
}
}
}
+3 -2
View File
@@ -60,7 +60,7 @@ namespace ErsatzTV.Core.Metadata
var knownExtensions = new List<string>
{
".mpg", ".mp2", ".mpeg", ".mpe", ".mpv", ".ogg", ".mp4", ".m4p", ".m4v",
".avi", ".wmv", ".mov", ".mkv"
".avi", ".wmv", ".mov", ".mkv", ".ts"
};
var allFiles = Directory.GetFiles(localMediaSource.Folder, "*", SearchOption.AllDirectories)
@@ -82,12 +82,13 @@ namespace ErsatzTV.Core.Metadata
}
// if exists, check if the file was modified
// also, try to re-categorize "other" by refreshing metadata
Seq<MediaItem> modifiedMediaItems = existingMediaItems.Filter(
mediaItem =>
{
DateTime lastWrite = File.GetLastWriteTimeUtc(mediaItem.Path);
bool modified = lastWrite > mediaItem.LastWriteTime.IfNone(DateTime.MinValue);
return modified || mediaItem.Metadata == null;
return modified || mediaItem.Metadata == null || mediaItem.Metadata.MediaType == MediaType.Other;
});
modifiedPlayoutIds.AddRange(await _playoutRepository.GetPlayoutIdsForMediaItems(modifiedMediaItems));
foreach (MediaItem mediaItem in modifiedMediaItems)
+71 -66
View File
@@ -1,27 +1,35 @@
using System;
using System.IO;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using System.Xml.Serialization;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Metadata;
using ErsatzTV.Core.Interfaces.Repositories;
using LanguageExt;
using Microsoft.Extensions.Logging;
using static LanguageExt.Prelude;
namespace ErsatzTV.Core.Metadata
{
public class LocalMetadataProvider : ILocalMetadataProvider
{
private static readonly XmlSerializer MovieSerializer = new(typeof(MovieNfo));
private static readonly XmlSerializer TvShowSerializer = new(typeof(TvShowEpisodeNfo));
private readonly ILogger<LocalMetadataProvider> _logger;
private readonly IMediaItemRepository _mediaItemRepository;
public LocalMetadataProvider(IMediaItemRepository mediaItemRepository) =>
public LocalMetadataProvider(IMediaItemRepository mediaItemRepository, ILogger<LocalMetadataProvider> logger)
{
_mediaItemRepository = mediaItemRepository;
_logger = logger;
}
public async Task RefreshMetadata(MediaItem mediaItem)
{
Option<MediaMetadata> maybeMetadata = await LoadMetadata(mediaItem);
MediaMetadata metadata = maybeMetadata.IfNone(() => GetFallbackMetadata(mediaItem));
MediaMetadata metadata =
maybeMetadata.IfNone(() => FallbackMetadataProvider.GetFallbackMetadata(mediaItem));
await ApplyMetadataUpdate(mediaItem, metadata);
}
@@ -49,75 +57,72 @@ namespace ErsatzTV.Core.Metadata
string nfoFileName = Path.ChangeExtension(mediaItem.Path, "nfo");
if (nfoFileName == null || !File.Exists(nfoFileName))
{
_logger.LogDebug("NFO file does not exist at {Path}", nfoFileName);
return None;
}
var tvShowSerializer = new XmlSerializer(typeof(TvShowEpisodeNfo));
var movieSerializer = new XmlSerializer(typeof(MovieNfo));
TryAsync<object> tvShowAttempt = TryAsync(
async () =>
{
await using FileStream fileStream = File.Open(nfoFileName, FileMode.Open);
return tvShowSerializer.Deserialize(fileStream);
});
TryAsync<object> movieAttempt = TryAsync(
async () =>
{
await using FileStream fileStream = File.Open(nfoFileName, FileMode.Open);
return movieSerializer.Deserialize(fileStream);
});
return await choice(tvShowAttempt, movieAttempt).Match<object, Option<MediaMetadata>>(
result =>
{
switch (result)
{
case TvShowEpisodeNfo nfo:
return new MediaMetadata
{
MediaType = MediaType.TvShow,
Title = nfo.ShowTitle,
Subtitle = nfo.Title,
Description = nfo.Outline,
EpisodeNumber = nfo.Episode,
SeasonNumber = nfo.Season,
Aired = GetAired(nfo.Aired)
};
case MovieNfo nfo:
return new MediaMetadata
{
MediaType = MediaType.Movie,
Title = nfo.Title,
Description = nfo.Outline,
ContentRating = nfo.ContentRating,
Aired = GetAired(nfo.Premiered)
};
default:
return None;
}
},
None);
}
private MediaMetadata GetFallbackMetadata(MediaItem mediaItem)
{
string fileName = Path.GetFileName(mediaItem.Path);
var metadata = new MediaMetadata { Title = fileName ?? mediaItem.Path };
if (fileName != null)
if (!(mediaItem.Source is LocalMediaSource localMediaSource))
{
const string PATTERN = @"^(.*?)[\s-]+[sS](\d+)[eE](\d+)\.\w+$";
Match match = Regex.Match(fileName, PATTERN);
if (match.Success)
{
metadata.MediaType = MediaType.TvShow;
metadata.Title = match.Groups[1].Value;
metadata.SeasonNumber = int.Parse(match.Groups[2].Value);
metadata.EpisodeNumber = int.Parse(match.Groups[3].Value);
}
_logger.LogDebug("Media source {Name} is not a local media source", mediaItem.Source.Name);
return None;
}
return metadata;
return localMediaSource.MediaType switch
{
MediaType.Movie => await LoadMovieMetadata(nfoFileName),
MediaType.TvShow => await LoadTvShowMetadata(nfoFileName),
_ => None
};
}
private async Task<Option<MediaMetadata>> LoadTvShowMetadata(string nfoFileName)
{
try
{
await using FileStream fileStream = File.Open(nfoFileName, FileMode.Open, FileAccess.Read);
Option<TvShowEpisodeNfo> maybeNfo = TvShowSerializer.Deserialize(fileStream) as TvShowEpisodeNfo;
return maybeNfo.Match<Option<MediaMetadata>>(
nfo => new MediaMetadata
{
MediaType = MediaType.TvShow,
Title = nfo.ShowTitle,
Subtitle = nfo.Title,
Description = nfo.Outline,
EpisodeNumber = nfo.Episode,
SeasonNumber = nfo.Season,
Aired = GetAired(nfo.Aired)
},
None);
}
catch (Exception ex)
{
_logger.LogDebug(ex, "Failed to read TV nfo metadata from {Path}", nfoFileName);
return None;
}
}
private async Task<Option<MediaMetadata>> LoadMovieMetadata(string nfoFileName)
{
try
{
await using FileStream fileStream = File.Open(nfoFileName, FileMode.Open, FileAccess.Read);
Option<MovieNfo> maybeNfo = MovieSerializer.Deserialize(fileStream) as MovieNfo;
return maybeNfo.Match<Option<MediaMetadata>>(
nfo => new MediaMetadata
{
MediaType = MediaType.Movie,
Title = nfo.Title,
Description = nfo.Outline,
ContentRating = nfo.ContentRating,
Aired = GetAired(nfo.Premiered)
},
None);
}
catch (Exception ex)
{
_logger.LogDebug(ex, "Failed to read Movie nfo metadata from {Path}", nfoFileName);
return None;
}
}
private static DateTime? GetAired(string aired)
@@ -7,6 +7,7 @@ using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Metadata;
using ErsatzTV.Core.Interfaces.Repositories;
using LanguageExt;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using static LanguageExt.Prelude;
@@ -14,16 +15,29 @@ namespace ErsatzTV.Core.Metadata
{
public class LocalStatisticsProvider : ILocalStatisticsProvider
{
private readonly ILogger<LocalStatisticsProvider> _logger;
private readonly IMediaItemRepository _mediaItemRepository;
public LocalStatisticsProvider(IMediaItemRepository mediaItemRepository) =>
public LocalStatisticsProvider(
IMediaItemRepository mediaItemRepository,
ILogger<LocalStatisticsProvider> logger)
{
_mediaItemRepository = mediaItemRepository;
_logger = logger;
}
public async Task RefreshStatistics(string ffprobePath, MediaItem mediaItem)
{
FFprobe ffprobe = await GetProbeOutput(ffprobePath, mediaItem);
MediaMetadata metadata = ProjectToMediaMetadata(ffprobe);
await ApplyStatisticsUpdate(mediaItem, metadata);
try
{
FFprobe ffprobe = await GetProbeOutput(ffprobePath, mediaItem);
MediaMetadata metadata = ProjectToMediaMetadata(ffprobe);
await ApplyStatisticsUpdate(mediaItem, metadata);
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Failed to refresh statistics for media item at {Path}", mediaItem.Path);
}
}
private async Task ApplyStatisticsUpdate(
@@ -82,7 +96,7 @@ namespace ErsatzTV.Core.Metadata
private MediaMetadata ProjectToMediaMetadata(FFprobe probeOutput) =>
Optional(probeOutput)
.Filter(json => json != null)
.Filter(json => json?.format != null && json.streams != null)
.ToValidation<BaseError>("Unable to parse ffprobe output")
.ToEither<FFprobe>()
.Match(
@@ -29,6 +29,13 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
public Task<Option<SimpleMediaCollection>> GetSimpleMediaCollection(int id) =>
Get(id).Map(c => c.OfType<SimpleMediaCollection>().HeadOrNone());
public Task<Option<SimpleMediaCollection>> GetSimpleMediaCollectionWithItems(int id) =>
_dbContext.SimpleMediaCollections
.Include(s => s.Items)
.ThenInclude(i => i.Source)
.SingleOrDefaultAsync(c => c.Id == id)
.Map(Optional);
public Task<Option<TelevisionMediaCollection>> GetTelevisionMediaCollection(int id) =>
Get(id).Map(c => c.OfType<TelevisionMediaCollection>().HeadOrNone());
@@ -1,4 +1,5 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Repositories;
@@ -26,6 +27,19 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
public Task<List<MediaItem>> GetAll() => _dbContext.MediaItems.ToListAsync();
public Task<List<MediaItem>> Search(string searchString)
{
IQueryable<MediaItem> data = from c in _dbContext.MediaItems.Include(c => c.Source) select c;
if (!string.IsNullOrEmpty(searchString))
{
data = data.Where(c => EF.Functions.Like(c.Metadata.Title, $"%{searchString}%"));
}
return data.ToListAsync();
}
public Task<List<MediaItem>> GetAll(MediaType mediaType) =>
_dbContext.MediaItems
.Include(i => i.Source)
@@ -2,11 +2,16 @@
<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
<GenerateRuntimeConfigurationFiles>true</GenerateRuntimeConfigurationFiles>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="5.0.2" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="5.0.2" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="5.0.3" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="5.0.3">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="5.0.3" />
<PackageReference Include="Refit" Version="6.0.1" />
</ItemGroup>
@@ -0,0 +1,876 @@
// <auto-generated />
using System;
using ErsatzTV.Infrastructure.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
namespace ErsatzTV.Infrastructure.Migrations
{
[DbContext(typeof(TvContext))]
[Migration("20210212105010_Initial")]
partial class Initial
{
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "5.0.3");
modelBuilder.Entity("ErsatzTV.Core.AggregateModels.GenericIntegerId", b =>
{
b.Property<int>("Id")
.HasColumnType("INTEGER");
b.ToTable("GenericIntegerIds");
});
modelBuilder.Entity("ErsatzTV.Core.AggregateModels.MediaCollectionSummary", b =>
{
b.Property<int>("Id")
.HasColumnType("INTEGER");
b.Property<bool>("IsSimple")
.HasColumnType("INTEGER");
b.Property<int>("ItemCount")
.HasColumnType("INTEGER");
b.Property<string>("Name")
.HasColumnType("TEXT");
b.ToTable("MediaCollectionSummaries");
});
modelBuilder.Entity("ErsatzTV.Core.Domain.Channel", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int>("FFmpegProfileId")
.HasColumnType("INTEGER");
b.Property<string>("Logo")
.HasColumnType("TEXT");
b.Property<string>("Name")
.HasColumnType("TEXT");
b.Property<int>("Number")
.HasColumnType("INTEGER");
b.Property<int>("StreamingMode")
.HasColumnType("INTEGER");
b.Property<Guid>("UniqueId")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("FFmpegProfileId");
b.HasIndex("Number")
.IsUnique();
b.ToTable("Channels");
});
modelBuilder.Entity("ErsatzTV.Core.Domain.ConfigElement", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("Key")
.HasColumnType("TEXT");
b.Property<string>("Value")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("Key")
.IsUnique();
b.ToTable("ConfigElements");
});
modelBuilder.Entity("ErsatzTV.Core.Domain.FFmpegProfile", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int>("AudioBitrate")
.HasColumnType("INTEGER");
b.Property<int>("AudioBufferSize")
.HasColumnType("INTEGER");
b.Property<int>("AudioChannels")
.HasColumnType("INTEGER");
b.Property<string>("AudioCodec")
.HasColumnType("TEXT");
b.Property<int>("AudioSampleRate")
.HasColumnType("INTEGER");
b.Property<int>("AudioVolume")
.HasColumnType("INTEGER");
b.Property<string>("Name")
.HasColumnType("TEXT");
b.Property<bool>("NormalizeAudio")
.HasColumnType("INTEGER");
b.Property<bool>("NormalizeAudioCodec")
.HasColumnType("INTEGER");
b.Property<bool>("NormalizeResolution")
.HasColumnType("INTEGER");
b.Property<bool>("NormalizeVideoCodec")
.HasColumnType("INTEGER");
b.Property<int>("ResolutionId")
.HasColumnType("INTEGER");
b.Property<int>("ThreadCount")
.HasColumnType("INTEGER");
b.Property<bool>("Transcode")
.HasColumnType("INTEGER");
b.Property<int>("VideoBitrate")
.HasColumnType("INTEGER");
b.Property<int>("VideoBufferSize")
.HasColumnType("INTEGER");
b.Property<string>("VideoCodec")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("ResolutionId");
b.ToTable("FFmpegProfiles");
});
modelBuilder.Entity("ErsatzTV.Core.Domain.MediaCollection", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("Name")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("Name")
.IsUnique();
b.ToTable("MediaCollections");
});
modelBuilder.Entity("ErsatzTV.Core.Domain.MediaItem", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<DateTime?>("LastWriteTime")
.HasColumnType("TEXT");
b.Property<int>("MediaSourceId")
.HasColumnType("INTEGER");
b.Property<string>("Path")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("MediaSourceId");
b.ToTable("MediaItems");
});
modelBuilder.Entity("ErsatzTV.Core.Domain.MediaSource", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("Name")
.HasColumnType("TEXT");
b.Property<int>("SourceType")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("Name")
.IsUnique();
b.ToTable("MediaSources");
});
modelBuilder.Entity("ErsatzTV.Core.Domain.Playout", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int>("ChannelId")
.HasColumnType("INTEGER");
b.Property<int>("ProgramScheduleId")
.HasColumnType("INTEGER");
b.Property<int>("ProgramSchedulePlayoutType")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("ChannelId");
b.HasIndex("ProgramScheduleId");
b.ToTable("Playouts");
});
modelBuilder.Entity("ErsatzTV.Core.Domain.PlayoutItem", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<DateTimeOffset>("Finish")
.HasColumnType("TEXT");
b.Property<int>("MediaItemId")
.HasColumnType("INTEGER");
b.Property<int>("PlayoutId")
.HasColumnType("INTEGER");
b.Property<DateTimeOffset>("Start")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("MediaItemId");
b.HasIndex("PlayoutId");
b.ToTable("PlayoutItems");
});
modelBuilder.Entity("ErsatzTV.Core.Domain.PlayoutProgramScheduleAnchor", b =>
{
b.Property<int>("PlayoutId")
.HasColumnType("INTEGER");
b.Property<int>("ProgramScheduleId")
.HasColumnType("INTEGER");
b.Property<int>("MediaCollectionId")
.HasColumnType("INTEGER");
b.HasKey("PlayoutId", "ProgramScheduleId", "MediaCollectionId");
b.HasIndex("MediaCollectionId");
b.HasIndex("ProgramScheduleId");
b.ToTable("PlayoutProgramScheduleItemAnchors");
});
modelBuilder.Entity("ErsatzTV.Core.Domain.PlexMediaSourceConnection", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<bool>("IsActive")
.HasColumnType("INTEGER");
b.Property<int?>("PlexMediaSourceId")
.HasColumnType("INTEGER");
b.Property<string>("Uri")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("PlexMediaSourceId");
b.ToTable("PlexMediaSourceConnections");
});
modelBuilder.Entity("ErsatzTV.Core.Domain.PlexMediaSourceLibrary", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("Key")
.HasColumnType("TEXT");
b.Property<int>("MediaType")
.HasColumnType("INTEGER");
b.Property<string>("Name")
.HasColumnType("TEXT");
b.Property<int?>("PlexMediaSourceId")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("PlexMediaSourceId");
b.ToTable("PlexMediaSourceLibraries");
});
modelBuilder.Entity("ErsatzTV.Core.Domain.ProgramSchedule", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int>("MediaCollectionPlaybackOrder")
.HasColumnType("INTEGER");
b.Property<string>("Name")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("Name")
.IsUnique();
b.ToTable("ProgramSchedules");
});
modelBuilder.Entity("ErsatzTV.Core.Domain.ProgramScheduleItem", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int>("Index")
.HasColumnType("INTEGER");
b.Property<int>("MediaCollectionId")
.HasColumnType("INTEGER");
b.Property<int>("ProgramScheduleId")
.HasColumnType("INTEGER");
b.Property<TimeSpan?>("StartTime")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("MediaCollectionId");
b.HasIndex("ProgramScheduleId");
b.ToTable("ProgramScheduleItems");
});
modelBuilder.Entity("ErsatzTV.Core.Domain.Resolution", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int>("Height")
.HasColumnType("INTEGER");
b.Property<string>("Name")
.HasColumnType("TEXT");
b.Property<int>("Width")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.ToTable("Resolutions");
});
modelBuilder.Entity("MediaItemSimpleMediaCollection", b =>
{
b.Property<int>("ItemsId")
.HasColumnType("INTEGER");
b.Property<int>("SimpleMediaCollectionsId")
.HasColumnType("INTEGER");
b.HasKey("ItemsId", "SimpleMediaCollectionsId");
b.HasIndex("SimpleMediaCollectionsId");
b.ToTable("MediaItemSimpleMediaCollection");
});
modelBuilder.Entity("ErsatzTV.Core.Domain.SimpleMediaCollection", b =>
{
b.HasBaseType("ErsatzTV.Core.Domain.MediaCollection");
b.ToTable("SimpleMediaCollections");
});
modelBuilder.Entity("ErsatzTV.Core.Domain.TelevisionMediaCollection", b =>
{
b.HasBaseType("ErsatzTV.Core.Domain.MediaCollection");
b.Property<int?>("SeasonNumber")
.HasColumnType("INTEGER");
b.Property<string>("ShowTitle")
.HasColumnType("TEXT");
b.HasIndex("ShowTitle", "SeasonNumber")
.IsUnique();
b.ToTable("TelevisionMediaCollections");
});
modelBuilder.Entity("ErsatzTV.Core.Domain.LocalMediaSource", b =>
{
b.HasBaseType("ErsatzTV.Core.Domain.MediaSource");
b.Property<string>("Folder")
.HasColumnType("TEXT");
b.Property<int>("MediaType")
.HasColumnType("INTEGER");
b.ToTable("LocalMediaSources");
});
modelBuilder.Entity("ErsatzTV.Core.Domain.PlexMediaSource", b =>
{
b.HasBaseType("ErsatzTV.Core.Domain.MediaSource");
b.Property<string>("ClientIdentifier")
.HasColumnType("TEXT");
b.Property<string>("ProductVersion")
.HasColumnType("TEXT");
b.ToTable("PlexMediaSources");
});
modelBuilder.Entity("ErsatzTV.Core.Domain.ProgramScheduleItemDuration", b =>
{
b.HasBaseType("ErsatzTV.Core.Domain.ProgramScheduleItem");
b.Property<bool>("OfflineTail")
.HasColumnType("INTEGER");
b.Property<TimeSpan>("PlayoutDuration")
.HasColumnType("TEXT");
b.ToTable("ProgramScheduleDurationItems");
});
modelBuilder.Entity("ErsatzTV.Core.Domain.ProgramScheduleItemFlood", b =>
{
b.HasBaseType("ErsatzTV.Core.Domain.ProgramScheduleItem");
b.ToTable("ProgramScheduleFloodItems");
});
modelBuilder.Entity("ErsatzTV.Core.Domain.ProgramScheduleItemMultiple", b =>
{
b.HasBaseType("ErsatzTV.Core.Domain.ProgramScheduleItem");
b.Property<int>("Count")
.HasColumnType("INTEGER");
b.ToTable("ProgramScheduleMultipleItems");
});
modelBuilder.Entity("ErsatzTV.Core.Domain.ProgramScheduleItemOne", b =>
{
b.HasBaseType("ErsatzTV.Core.Domain.ProgramScheduleItem");
b.ToTable("ProgramScheduleOneItems");
});
modelBuilder.Entity("ErsatzTV.Core.Domain.Channel", b =>
{
b.HasOne("ErsatzTV.Core.Domain.FFmpegProfile", "FFmpegProfile")
.WithMany()
.HasForeignKey("FFmpegProfileId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("FFmpegProfile");
});
modelBuilder.Entity("ErsatzTV.Core.Domain.FFmpegProfile", b =>
{
b.HasOne("ErsatzTV.Core.Domain.Resolution", "Resolution")
.WithMany()
.HasForeignKey("ResolutionId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Resolution");
});
modelBuilder.Entity("ErsatzTV.Core.Domain.MediaItem", b =>
{
b.HasOne("ErsatzTV.Core.Domain.MediaSource", "Source")
.WithMany()
.HasForeignKey("MediaSourceId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.OwnsOne("ErsatzTV.Core.Domain.MediaMetadata", "Metadata", b1 =>
{
b1.Property<int>("MediaItemId")
.HasColumnType("INTEGER");
b1.Property<DateTime?>("Aired")
.HasColumnType("TEXT");
b1.Property<string>("AudioCodec")
.HasColumnType("TEXT");
b1.Property<string>("ContentRating")
.HasColumnType("TEXT");
b1.Property<string>("Description")
.HasColumnType("TEXT");
b1.Property<string>("DisplayAspectRatio")
.HasColumnType("TEXT");
b1.Property<TimeSpan>("Duration")
.HasColumnType("TEXT");
b1.Property<int?>("EpisodeNumber")
.HasColumnType("INTEGER");
b1.Property<int>("Height")
.HasColumnType("INTEGER");
b1.Property<int>("MediaType")
.HasColumnType("INTEGER");
b1.Property<string>("SampleAspectRatio")
.HasColumnType("TEXT");
b1.Property<int?>("SeasonNumber")
.HasColumnType("INTEGER");
b1.Property<string>("Subtitle")
.HasColumnType("TEXT");
b1.Property<string>("Title")
.HasColumnType("TEXT");
b1.Property<string>("VideoCodec")
.HasColumnType("TEXT");
b1.Property<int>("VideoScanType")
.HasColumnType("INTEGER");
b1.Property<int>("Width")
.HasColumnType("INTEGER");
b1.HasKey("MediaItemId");
b1.ToTable("MediaItems");
b1.WithOwner()
.HasForeignKey("MediaItemId");
});
b.Navigation("Metadata");
b.Navigation("Source");
});
modelBuilder.Entity("ErsatzTV.Core.Domain.Playout", b =>
{
b.HasOne("ErsatzTV.Core.Domain.Channel", "Channel")
.WithMany("Playouts")
.HasForeignKey("ChannelId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("ErsatzTV.Core.Domain.ProgramSchedule", "ProgramSchedule")
.WithMany("Playouts")
.HasForeignKey("ProgramScheduleId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.OwnsOne("ErsatzTV.Core.Domain.PlayoutAnchor", "Anchor", b1 =>
{
b1.Property<int>("PlayoutId")
.HasColumnType("INTEGER");
b1.Property<int>("NextScheduleItemId")
.HasColumnType("INTEGER");
b1.Property<DateTimeOffset>("NextStart")
.HasColumnType("TEXT");
b1.HasKey("PlayoutId");
b1.HasIndex("NextScheduleItemId");
b1.ToTable("Playouts");
b1.HasOne("ErsatzTV.Core.Domain.ProgramScheduleItem", "NextScheduleItem")
.WithMany()
.HasForeignKey("NextScheduleItemId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b1.WithOwner()
.HasForeignKey("PlayoutId");
b1.Navigation("NextScheduleItem");
});
b.Navigation("Anchor");
b.Navigation("Channel");
b.Navigation("ProgramSchedule");
});
modelBuilder.Entity("ErsatzTV.Core.Domain.PlayoutItem", b =>
{
b.HasOne("ErsatzTV.Core.Domain.MediaItem", "MediaItem")
.WithMany()
.HasForeignKey("MediaItemId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("ErsatzTV.Core.Domain.Playout", "Playout")
.WithMany("Items")
.HasForeignKey("PlayoutId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("MediaItem");
b.Navigation("Playout");
});
modelBuilder.Entity("ErsatzTV.Core.Domain.PlayoutProgramScheduleAnchor", b =>
{
b.HasOne("ErsatzTV.Core.Domain.MediaCollection", "MediaCollection")
.WithMany()
.HasForeignKey("MediaCollectionId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("ErsatzTV.Core.Domain.Playout", "Playout")
.WithMany("ProgramScheduleAnchors")
.HasForeignKey("PlayoutId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("ErsatzTV.Core.Domain.ProgramSchedule", "ProgramSchedule")
.WithMany()
.HasForeignKey("ProgramScheduleId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.OwnsOne("ErsatzTV.Core.Domain.MediaCollectionEnumeratorState", "EnumeratorState", b1 =>
{
b1.Property<int>("PlayoutProgramScheduleAnchorPlayoutId")
.HasColumnType("INTEGER");
b1.Property<int>("PlayoutProgramScheduleAnchorProgramScheduleId")
.HasColumnType("INTEGER");
b1.Property<int>("PlayoutProgramScheduleAnchorMediaCollectionId")
.HasColumnType("INTEGER");
b1.Property<int>("Index")
.HasColumnType("INTEGER");
b1.Property<int>("Seed")
.HasColumnType("INTEGER");
b1.HasKey("PlayoutProgramScheduleAnchorPlayoutId", "PlayoutProgramScheduleAnchorProgramScheduleId", "PlayoutProgramScheduleAnchorMediaCollectionId");
b1.ToTable("PlayoutProgramScheduleItemAnchors");
b1.WithOwner()
.HasForeignKey("PlayoutProgramScheduleAnchorPlayoutId", "PlayoutProgramScheduleAnchorProgramScheduleId", "PlayoutProgramScheduleAnchorMediaCollectionId");
});
b.Navigation("EnumeratorState");
b.Navigation("MediaCollection");
b.Navigation("Playout");
b.Navigation("ProgramSchedule");
});
modelBuilder.Entity("ErsatzTV.Core.Domain.PlexMediaSourceConnection", b =>
{
b.HasOne("ErsatzTV.Core.Domain.PlexMediaSource", null)
.WithMany("Connections")
.HasForeignKey("PlexMediaSourceId");
});
modelBuilder.Entity("ErsatzTV.Core.Domain.PlexMediaSourceLibrary", b =>
{
b.HasOne("ErsatzTV.Core.Domain.PlexMediaSource", null)
.WithMany("Libraries")
.HasForeignKey("PlexMediaSourceId");
});
modelBuilder.Entity("ErsatzTV.Core.Domain.ProgramScheduleItem", b =>
{
b.HasOne("ErsatzTV.Core.Domain.MediaCollection", "MediaCollection")
.WithMany()
.HasForeignKey("MediaCollectionId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("ErsatzTV.Core.Domain.ProgramSchedule", "ProgramSchedule")
.WithMany("Items")
.HasForeignKey("ProgramScheduleId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("MediaCollection");
b.Navigation("ProgramSchedule");
});
modelBuilder.Entity("MediaItemSimpleMediaCollection", b =>
{
b.HasOne("ErsatzTV.Core.Domain.MediaItem", null)
.WithMany()
.HasForeignKey("ItemsId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("ErsatzTV.Core.Domain.SimpleMediaCollection", null)
.WithMany()
.HasForeignKey("SimpleMediaCollectionsId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("ErsatzTV.Core.Domain.SimpleMediaCollection", b =>
{
b.HasOne("ErsatzTV.Core.Domain.MediaCollection", null)
.WithOne()
.HasForeignKey("ErsatzTV.Core.Domain.SimpleMediaCollection", "Id")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("ErsatzTV.Core.Domain.TelevisionMediaCollection", b =>
{
b.HasOne("ErsatzTV.Core.Domain.MediaCollection", null)
.WithOne()
.HasForeignKey("ErsatzTV.Core.Domain.TelevisionMediaCollection", "Id")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("ErsatzTV.Core.Domain.LocalMediaSource", b =>
{
b.HasOne("ErsatzTV.Core.Domain.MediaSource", null)
.WithOne()
.HasForeignKey("ErsatzTV.Core.Domain.LocalMediaSource", "Id")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("ErsatzTV.Core.Domain.PlexMediaSource", b =>
{
b.HasOne("ErsatzTV.Core.Domain.MediaSource", null)
.WithOne()
.HasForeignKey("ErsatzTV.Core.Domain.PlexMediaSource", "Id")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("ErsatzTV.Core.Domain.ProgramScheduleItemDuration", b =>
{
b.HasOne("ErsatzTV.Core.Domain.ProgramScheduleItem", null)
.WithOne()
.HasForeignKey("ErsatzTV.Core.Domain.ProgramScheduleItemDuration", "Id")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("ErsatzTV.Core.Domain.ProgramScheduleItemFlood", b =>
{
b.HasOne("ErsatzTV.Core.Domain.ProgramScheduleItem", null)
.WithOne()
.HasForeignKey("ErsatzTV.Core.Domain.ProgramScheduleItemFlood", "Id")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("ErsatzTV.Core.Domain.ProgramScheduleItemMultiple", b =>
{
b.HasOne("ErsatzTV.Core.Domain.ProgramScheduleItem", null)
.WithOne()
.HasForeignKey("ErsatzTV.Core.Domain.ProgramScheduleItemMultiple", "Id")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("ErsatzTV.Core.Domain.ProgramScheduleItemOne", b =>
{
b.HasOne("ErsatzTV.Core.Domain.ProgramScheduleItem", null)
.WithOne()
.HasForeignKey("ErsatzTV.Core.Domain.ProgramScheduleItemOne", "Id")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("ErsatzTV.Core.Domain.Channel", b =>
{
b.Navigation("Playouts");
});
modelBuilder.Entity("ErsatzTV.Core.Domain.Playout", b =>
{
b.Navigation("Items");
b.Navigation("ProgramScheduleAnchors");
});
modelBuilder.Entity("ErsatzTV.Core.Domain.ProgramSchedule", b =>
{
b.Navigation("Items");
b.Navigation("Playouts");
});
modelBuilder.Entity("ErsatzTV.Core.Domain.PlexMediaSource", b =>
{
b.Navigation("Connections");
b.Navigation("Libraries");
});
#pragma warning restore 612, 618
}
}
}
@@ -0,0 +1,718 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
namespace ErsatzTV.Infrastructure.Migrations
{
public partial class Initial : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
"ConfigElements",
table => new
{
Id = table.Column<int>("INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
Key = table.Column<string>("TEXT", nullable: true),
Value = table.Column<string>("TEXT", nullable: true)
},
constraints: table => { table.PrimaryKey("PK_ConfigElements", x => x.Id); });
migrationBuilder.CreateTable(
"GenericIntegerIds",
table => new
{
Id = table.Column<int>("INTEGER", nullable: false)
},
constraints: table => { });
migrationBuilder.CreateTable(
"MediaCollections",
table => new
{
Id = table.Column<int>("INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
Name = table.Column<string>("TEXT", nullable: true)
},
constraints: table => { table.PrimaryKey("PK_MediaCollections", x => x.Id); });
migrationBuilder.CreateTable(
"MediaCollectionSummaries",
table => new
{
Id = table.Column<int>("INTEGER", nullable: false),
Name = table.Column<string>("TEXT", nullable: true),
ItemCount = table.Column<int>("INTEGER", nullable: false),
IsSimple = table.Column<bool>("INTEGER", nullable: false)
},
constraints: table => { });
migrationBuilder.CreateTable(
"MediaSources",
table => new
{
Id = table.Column<int>("INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
SourceType = table.Column<int>("INTEGER", nullable: false),
Name = table.Column<string>("TEXT", nullable: true)
},
constraints: table => { table.PrimaryKey("PK_MediaSources", x => x.Id); });
migrationBuilder.CreateTable(
"ProgramSchedules",
table => new
{
Id = table.Column<int>("INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
Name = table.Column<string>("TEXT", nullable: true),
MediaCollectionPlaybackOrder = table.Column<int>("INTEGER", nullable: false)
},
constraints: table => { table.PrimaryKey("PK_ProgramSchedules", x => x.Id); });
migrationBuilder.CreateTable(
"Resolutions",
table => new
{
Id = table.Column<int>("INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
Name = table.Column<string>("TEXT", nullable: true),
Height = table.Column<int>("INTEGER", nullable: false),
Width = table.Column<int>("INTEGER", nullable: false)
},
constraints: table => { table.PrimaryKey("PK_Resolutions", x => x.Id); });
migrationBuilder.CreateTable(
"SimpleMediaCollections",
table => new
{
Id = table.Column<int>("INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true)
},
constraints: table =>
{
table.PrimaryKey("PK_SimpleMediaCollections", x => x.Id);
table.ForeignKey(
"FK_SimpleMediaCollections_MediaCollections_Id",
x => x.Id,
"MediaCollections",
"Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
"TelevisionMediaCollections",
table => new
{
Id = table.Column<int>("INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
ShowTitle = table.Column<string>("TEXT", nullable: true),
SeasonNumber = table.Column<int>("INTEGER", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_TelevisionMediaCollections", x => x.Id);
table.ForeignKey(
"FK_TelevisionMediaCollections_MediaCollections_Id",
x => x.Id,
"MediaCollections",
"Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
"LocalMediaSources",
table => new
{
Id = table.Column<int>("INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
MediaType = table.Column<int>("INTEGER", nullable: false),
Folder = table.Column<string>("TEXT", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_LocalMediaSources", x => x.Id);
table.ForeignKey(
"FK_LocalMediaSources_MediaSources_Id",
x => x.Id,
"MediaSources",
"Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
"MediaItems",
table => new
{
Id = table.Column<int>("INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
MediaSourceId = table.Column<int>("INTEGER", nullable: false),
Path = table.Column<string>("TEXT", nullable: true),
Metadata_Duration = table.Column<TimeSpan>("TEXT", nullable: true),
Metadata_SampleAspectRatio = table.Column<string>("TEXT", nullable: true),
Metadata_DisplayAspectRatio = table.Column<string>("TEXT", nullable: true),
Metadata_VideoCodec = table.Column<string>("TEXT", nullable: true),
Metadata_AudioCodec = table.Column<string>("TEXT", nullable: true),
Metadata_MediaType = table.Column<int>("INTEGER", nullable: true),
Metadata_Title = table.Column<string>("TEXT", nullable: true),
Metadata_Subtitle = table.Column<string>("TEXT", nullable: true),
Metadata_Description = table.Column<string>("TEXT", nullable: true),
Metadata_SeasonNumber = table.Column<int>("INTEGER", nullable: true),
Metadata_EpisodeNumber = table.Column<int>("INTEGER", nullable: true),
Metadata_ContentRating = table.Column<string>("TEXT", nullable: true),
Metadata_Aired = table.Column<DateTime>("TEXT", nullable: true),
Metadata_VideoScanType = table.Column<int>("INTEGER", nullable: true),
Metadata_Width = table.Column<int>("INTEGER", nullable: true),
Metadata_Height = table.Column<int>("INTEGER", nullable: true),
LastWriteTime = table.Column<DateTime>("TEXT", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_MediaItems", x => x.Id);
table.ForeignKey(
"FK_MediaItems_MediaSources_MediaSourceId",
x => x.MediaSourceId,
"MediaSources",
"Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
"PlexMediaSources",
table => new
{
Id = table.Column<int>("INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
ProductVersion = table.Column<string>("TEXT", nullable: true),
ClientIdentifier = table.Column<string>("TEXT", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_PlexMediaSources", x => x.Id);
table.ForeignKey(
"FK_PlexMediaSources_MediaSources_Id",
x => x.Id,
"MediaSources",
"Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
"ProgramScheduleItems",
table => new
{
Id = table.Column<int>("INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
Index = table.Column<int>("INTEGER", nullable: false),
StartTime = table.Column<TimeSpan>("TEXT", nullable: true),
MediaCollectionId = table.Column<int>("INTEGER", nullable: false),
ProgramScheduleId = table.Column<int>("INTEGER", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_ProgramScheduleItems", x => x.Id);
table.ForeignKey(
"FK_ProgramScheduleItems_MediaCollections_MediaCollectionId",
x => x.MediaCollectionId,
"MediaCollections",
"Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
"FK_ProgramScheduleItems_ProgramSchedules_ProgramScheduleId",
x => x.ProgramScheduleId,
"ProgramSchedules",
"Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
"FFmpegProfiles",
table => new
{
Id = table.Column<int>("INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
Name = table.Column<string>("TEXT", nullable: true),
ThreadCount = table.Column<int>("INTEGER", nullable: false),
Transcode = table.Column<bool>("INTEGER", nullable: false),
ResolutionId = table.Column<int>("INTEGER", nullable: false),
NormalizeResolution = table.Column<bool>("INTEGER", nullable: false),
VideoCodec = table.Column<string>("TEXT", nullable: true),
NormalizeVideoCodec = table.Column<bool>("INTEGER", nullable: false),
VideoBitrate = table.Column<int>("INTEGER", nullable: false),
VideoBufferSize = table.Column<int>("INTEGER", nullable: false),
AudioCodec = table.Column<string>("TEXT", nullable: true),
NormalizeAudioCodec = table.Column<bool>("INTEGER", nullable: false),
AudioBitrate = table.Column<int>("INTEGER", nullable: false),
AudioBufferSize = table.Column<int>("INTEGER", nullable: false),
AudioVolume = table.Column<int>("INTEGER", nullable: false),
AudioChannels = table.Column<int>("INTEGER", nullable: false),
AudioSampleRate = table.Column<int>("INTEGER", nullable: false),
NormalizeAudio = table.Column<bool>("INTEGER", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_FFmpegProfiles", x => x.Id);
table.ForeignKey(
"FK_FFmpegProfiles_Resolutions_ResolutionId",
x => x.ResolutionId,
"Resolutions",
"Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
"MediaItemSimpleMediaCollection",
table => new
{
ItemsId = table.Column<int>("INTEGER", nullable: false),
SimpleMediaCollectionsId = table.Column<int>("INTEGER", nullable: false)
},
constraints: table =>
{
table.PrimaryKey(
"PK_MediaItemSimpleMediaCollection",
x => new { x.ItemsId, x.SimpleMediaCollectionsId });
table.ForeignKey(
"FK_MediaItemSimpleMediaCollection_MediaItems_ItemsId",
x => x.ItemsId,
"MediaItems",
"Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
"FK_MediaItemSimpleMediaCollection_SimpleMediaCollections_SimpleMediaCollectionsId",
x => x.SimpleMediaCollectionsId,
"SimpleMediaCollections",
"Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
"PlexMediaSourceConnections",
table => new
{
Id = table.Column<int>("INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
IsActive = table.Column<bool>("INTEGER", nullable: false),
Uri = table.Column<string>("TEXT", nullable: true),
PlexMediaSourceId = table.Column<int>("INTEGER", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_PlexMediaSourceConnections", x => x.Id);
table.ForeignKey(
"FK_PlexMediaSourceConnections_PlexMediaSources_PlexMediaSourceId",
x => x.PlexMediaSourceId,
"PlexMediaSources",
"Id",
onDelete: ReferentialAction.Restrict);
});
migrationBuilder.CreateTable(
"PlexMediaSourceLibraries",
table => new
{
Id = table.Column<int>("INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
Key = table.Column<string>("TEXT", nullable: true),
Name = table.Column<string>("TEXT", nullable: true),
MediaType = table.Column<int>("INTEGER", nullable: false),
PlexMediaSourceId = table.Column<int>("INTEGER", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_PlexMediaSourceLibraries", x => x.Id);
table.ForeignKey(
"FK_PlexMediaSourceLibraries_PlexMediaSources_PlexMediaSourceId",
x => x.PlexMediaSourceId,
"PlexMediaSources",
"Id",
onDelete: ReferentialAction.Restrict);
});
migrationBuilder.CreateTable(
"ProgramScheduleDurationItems",
table => new
{
Id = table.Column<int>("INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
PlayoutDuration = table.Column<TimeSpan>("TEXT", nullable: false),
OfflineTail = table.Column<bool>("INTEGER", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_ProgramScheduleDurationItems", x => x.Id);
table.ForeignKey(
"FK_ProgramScheduleDurationItems_ProgramScheduleItems_Id",
x => x.Id,
"ProgramScheduleItems",
"Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
"ProgramScheduleFloodItems",
table => new
{
Id = table.Column<int>("INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true)
},
constraints: table =>
{
table.PrimaryKey("PK_ProgramScheduleFloodItems", x => x.Id);
table.ForeignKey(
"FK_ProgramScheduleFloodItems_ProgramScheduleItems_Id",
x => x.Id,
"ProgramScheduleItems",
"Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
"ProgramScheduleMultipleItems",
table => new
{
Id = table.Column<int>("INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
Count = table.Column<int>("INTEGER", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_ProgramScheduleMultipleItems", x => x.Id);
table.ForeignKey(
"FK_ProgramScheduleMultipleItems_ProgramScheduleItems_Id",
x => x.Id,
"ProgramScheduleItems",
"Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
"ProgramScheduleOneItems",
table => new
{
Id = table.Column<int>("INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true)
},
constraints: table =>
{
table.PrimaryKey("PK_ProgramScheduleOneItems", x => x.Id);
table.ForeignKey(
"FK_ProgramScheduleOneItems_ProgramScheduleItems_Id",
x => x.Id,
"ProgramScheduleItems",
"Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
"Channels",
table => new
{
Id = table.Column<int>("INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
UniqueId = table.Column<Guid>("TEXT", nullable: false),
Number = table.Column<int>("INTEGER", nullable: false),
Name = table.Column<string>("TEXT", nullable: true),
Logo = table.Column<string>("TEXT", nullable: true),
FFmpegProfileId = table.Column<int>("INTEGER", nullable: false),
StreamingMode = table.Column<int>("INTEGER", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Channels", x => x.Id);
table.ForeignKey(
"FK_Channels_FFmpegProfiles_FFmpegProfileId",
x => x.FFmpegProfileId,
"FFmpegProfiles",
"Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
"Playouts",
table => new
{
Id = table.Column<int>("INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
ChannelId = table.Column<int>("INTEGER", nullable: false),
ProgramScheduleId = table.Column<int>("INTEGER", nullable: false),
ProgramSchedulePlayoutType = table.Column<int>("INTEGER", nullable: false),
Anchor_NextScheduleItemId = table.Column<int>("INTEGER", nullable: true),
Anchor_NextStart = table.Column<DateTimeOffset>("TEXT", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_Playouts", x => x.Id);
table.ForeignKey(
"FK_Playouts_Channels_ChannelId",
x => x.ChannelId,
"Channels",
"Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
"FK_Playouts_ProgramScheduleItems_Anchor_NextScheduleItemId",
x => x.Anchor_NextScheduleItemId,
"ProgramScheduleItems",
"Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
"FK_Playouts_ProgramSchedules_ProgramScheduleId",
x => x.ProgramScheduleId,
"ProgramSchedules",
"Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
"PlayoutItems",
table => new
{
Id = table.Column<int>("INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
MediaItemId = table.Column<int>("INTEGER", nullable: false),
Start = table.Column<DateTimeOffset>("TEXT", nullable: false),
Finish = table.Column<DateTimeOffset>("TEXT", nullable: false),
PlayoutId = table.Column<int>("INTEGER", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_PlayoutItems", x => x.Id);
table.ForeignKey(
"FK_PlayoutItems_MediaItems_MediaItemId",
x => x.MediaItemId,
"MediaItems",
"Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
"FK_PlayoutItems_Playouts_PlayoutId",
x => x.PlayoutId,
"Playouts",
"Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
"PlayoutProgramScheduleItemAnchors",
table => new
{
PlayoutId = table.Column<int>("INTEGER", nullable: false),
ProgramScheduleId = table.Column<int>("INTEGER", nullable: false),
MediaCollectionId = table.Column<int>("INTEGER", nullable: false),
EnumeratorState_Seed = table.Column<int>("INTEGER", nullable: true),
EnumeratorState_Index = table.Column<int>("INTEGER", nullable: true)
},
constraints: table =>
{
table.PrimaryKey(
"PK_PlayoutProgramScheduleItemAnchors",
x => new { x.PlayoutId, x.ProgramScheduleId, x.MediaCollectionId });
table.ForeignKey(
"FK_PlayoutProgramScheduleItemAnchors_MediaCollections_MediaCollectionId",
x => x.MediaCollectionId,
"MediaCollections",
"Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
"FK_PlayoutProgramScheduleItemAnchors_Playouts_PlayoutId",
x => x.PlayoutId,
"Playouts",
"Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
"FK_PlayoutProgramScheduleItemAnchors_ProgramSchedules_ProgramScheduleId",
x => x.ProgramScheduleId,
"ProgramSchedules",
"Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
"IX_Channels_FFmpegProfileId",
"Channels",
"FFmpegProfileId");
migrationBuilder.CreateIndex(
"IX_Channels_Number",
"Channels",
"Number",
unique: true);
migrationBuilder.CreateIndex(
"IX_ConfigElements_Key",
"ConfigElements",
"Key",
unique: true);
migrationBuilder.CreateIndex(
"IX_FFmpegProfiles_ResolutionId",
"FFmpegProfiles",
"ResolutionId");
migrationBuilder.CreateIndex(
"IX_MediaCollections_Name",
"MediaCollections",
"Name",
unique: true);
migrationBuilder.CreateIndex(
"IX_MediaItems_MediaSourceId",
"MediaItems",
"MediaSourceId");
migrationBuilder.CreateIndex(
"IX_MediaItemSimpleMediaCollection_SimpleMediaCollectionsId",
"MediaItemSimpleMediaCollection",
"SimpleMediaCollectionsId");
migrationBuilder.CreateIndex(
"IX_MediaSources_Name",
"MediaSources",
"Name",
unique: true);
migrationBuilder.CreateIndex(
"IX_PlayoutItems_MediaItemId",
"PlayoutItems",
"MediaItemId");
migrationBuilder.CreateIndex(
"IX_PlayoutItems_PlayoutId",
"PlayoutItems",
"PlayoutId");
migrationBuilder.CreateIndex(
"IX_PlayoutProgramScheduleItemAnchors_MediaCollectionId",
"PlayoutProgramScheduleItemAnchors",
"MediaCollectionId");
migrationBuilder.CreateIndex(
"IX_PlayoutProgramScheduleItemAnchors_ProgramScheduleId",
"PlayoutProgramScheduleItemAnchors",
"ProgramScheduleId");
migrationBuilder.CreateIndex(
"IX_Playouts_Anchor_NextScheduleItemId",
"Playouts",
"Anchor_NextScheduleItemId");
migrationBuilder.CreateIndex(
"IX_Playouts_ChannelId",
"Playouts",
"ChannelId");
migrationBuilder.CreateIndex(
"IX_Playouts_ProgramScheduleId",
"Playouts",
"ProgramScheduleId");
migrationBuilder.CreateIndex(
"IX_PlexMediaSourceConnections_PlexMediaSourceId",
"PlexMediaSourceConnections",
"PlexMediaSourceId");
migrationBuilder.CreateIndex(
"IX_PlexMediaSourceLibraries_PlexMediaSourceId",
"PlexMediaSourceLibraries",
"PlexMediaSourceId");
migrationBuilder.CreateIndex(
"IX_ProgramScheduleItems_MediaCollectionId",
"ProgramScheduleItems",
"MediaCollectionId");
migrationBuilder.CreateIndex(
"IX_ProgramScheduleItems_ProgramScheduleId",
"ProgramScheduleItems",
"ProgramScheduleId");
migrationBuilder.CreateIndex(
"IX_ProgramSchedules_Name",
"ProgramSchedules",
"Name",
unique: true);
migrationBuilder.CreateIndex(
"IX_TelevisionMediaCollections_ShowTitle_SeasonNumber",
"TelevisionMediaCollections",
new[] { "ShowTitle", "SeasonNumber" },
unique: true);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
"ConfigElements");
migrationBuilder.DropTable(
"GenericIntegerIds");
migrationBuilder.DropTable(
"LocalMediaSources");
migrationBuilder.DropTable(
"MediaCollectionSummaries");
migrationBuilder.DropTable(
"MediaItemSimpleMediaCollection");
migrationBuilder.DropTable(
"PlayoutItems");
migrationBuilder.DropTable(
"PlayoutProgramScheduleItemAnchors");
migrationBuilder.DropTable(
"PlexMediaSourceConnections");
migrationBuilder.DropTable(
"PlexMediaSourceLibraries");
migrationBuilder.DropTable(
"ProgramScheduleDurationItems");
migrationBuilder.DropTable(
"ProgramScheduleFloodItems");
migrationBuilder.DropTable(
"ProgramScheduleMultipleItems");
migrationBuilder.DropTable(
"ProgramScheduleOneItems");
migrationBuilder.DropTable(
"TelevisionMediaCollections");
migrationBuilder.DropTable(
"SimpleMediaCollections");
migrationBuilder.DropTable(
"MediaItems");
migrationBuilder.DropTable(
"Playouts");
migrationBuilder.DropTable(
"PlexMediaSources");
migrationBuilder.DropTable(
"Channels");
migrationBuilder.DropTable(
"ProgramScheduleItems");
migrationBuilder.DropTable(
"MediaSources");
migrationBuilder.DropTable(
"FFmpegProfiles");
migrationBuilder.DropTable(
"MediaCollections");
migrationBuilder.DropTable(
"ProgramSchedules");
migrationBuilder.DropTable(
"Resolutions");
}
}
}
@@ -0,0 +1,978 @@
// <auto-generated />
using System;
using ErsatzTV.Infrastructure.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
namespace ErsatzTV.Infrastructure.Migrations
{
[DbContext(typeof(TvContext))]
internal class TvContextModelSnapshot : ModelSnapshot
{
protected override void BuildModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "5.0.3");
modelBuilder.Entity(
"ErsatzTV.Core.AggregateModels.GenericIntegerId",
b =>
{
b.Property<int>("Id")
.HasColumnType("INTEGER");
b.ToTable("GenericIntegerIds");
});
modelBuilder.Entity(
"ErsatzTV.Core.AggregateModels.MediaCollectionSummary",
b =>
{
b.Property<int>("Id")
.HasColumnType("INTEGER");
b.Property<bool>("IsSimple")
.HasColumnType("INTEGER");
b.Property<int>("ItemCount")
.HasColumnType("INTEGER");
b.Property<string>("Name")
.HasColumnType("TEXT");
b.ToTable("MediaCollectionSummaries");
});
modelBuilder.Entity(
"ErsatzTV.Core.Domain.Channel",
b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int>("FFmpegProfileId")
.HasColumnType("INTEGER");
b.Property<string>("Logo")
.HasColumnType("TEXT");
b.Property<string>("Name")
.HasColumnType("TEXT");
b.Property<int>("Number")
.HasColumnType("INTEGER");
b.Property<int>("StreamingMode")
.HasColumnType("INTEGER");
b.Property<Guid>("UniqueId")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("FFmpegProfileId");
b.HasIndex("Number")
.IsUnique();
b.ToTable("Channels");
});
modelBuilder.Entity(
"ErsatzTV.Core.Domain.ConfigElement",
b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("Key")
.HasColumnType("TEXT");
b.Property<string>("Value")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("Key")
.IsUnique();
b.ToTable("ConfigElements");
});
modelBuilder.Entity(
"ErsatzTV.Core.Domain.FFmpegProfile",
b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int>("AudioBitrate")
.HasColumnType("INTEGER");
b.Property<int>("AudioBufferSize")
.HasColumnType("INTEGER");
b.Property<int>("AudioChannels")
.HasColumnType("INTEGER");
b.Property<string>("AudioCodec")
.HasColumnType("TEXT");
b.Property<int>("AudioSampleRate")
.HasColumnType("INTEGER");
b.Property<int>("AudioVolume")
.HasColumnType("INTEGER");
b.Property<string>("Name")
.HasColumnType("TEXT");
b.Property<bool>("NormalizeAudio")
.HasColumnType("INTEGER");
b.Property<bool>("NormalizeAudioCodec")
.HasColumnType("INTEGER");
b.Property<bool>("NormalizeResolution")
.HasColumnType("INTEGER");
b.Property<bool>("NormalizeVideoCodec")
.HasColumnType("INTEGER");
b.Property<int>("ResolutionId")
.HasColumnType("INTEGER");
b.Property<int>("ThreadCount")
.HasColumnType("INTEGER");
b.Property<bool>("Transcode")
.HasColumnType("INTEGER");
b.Property<int>("VideoBitrate")
.HasColumnType("INTEGER");
b.Property<int>("VideoBufferSize")
.HasColumnType("INTEGER");
b.Property<string>("VideoCodec")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("ResolutionId");
b.ToTable("FFmpegProfiles");
});
modelBuilder.Entity(
"ErsatzTV.Core.Domain.MediaCollection",
b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("Name")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("Name")
.IsUnique();
b.ToTable("MediaCollections");
});
modelBuilder.Entity(
"ErsatzTV.Core.Domain.MediaItem",
b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<DateTime?>("LastWriteTime")
.HasColumnType("TEXT");
b.Property<int>("MediaSourceId")
.HasColumnType("INTEGER");
b.Property<string>("Path")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("MediaSourceId");
b.ToTable("MediaItems");
});
modelBuilder.Entity(
"ErsatzTV.Core.Domain.MediaSource",
b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("Name")
.HasColumnType("TEXT");
b.Property<int>("SourceType")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("Name")
.IsUnique();
b.ToTable("MediaSources");
});
modelBuilder.Entity(
"ErsatzTV.Core.Domain.Playout",
b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int>("ChannelId")
.HasColumnType("INTEGER");
b.Property<int>("ProgramScheduleId")
.HasColumnType("INTEGER");
b.Property<int>("ProgramSchedulePlayoutType")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("ChannelId");
b.HasIndex("ProgramScheduleId");
b.ToTable("Playouts");
});
modelBuilder.Entity(
"ErsatzTV.Core.Domain.PlayoutItem",
b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<DateTimeOffset>("Finish")
.HasColumnType("TEXT");
b.Property<int>("MediaItemId")
.HasColumnType("INTEGER");
b.Property<int>("PlayoutId")
.HasColumnType("INTEGER");
b.Property<DateTimeOffset>("Start")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("MediaItemId");
b.HasIndex("PlayoutId");
b.ToTable("PlayoutItems");
});
modelBuilder.Entity(
"ErsatzTV.Core.Domain.PlayoutProgramScheduleAnchor",
b =>
{
b.Property<int>("PlayoutId")
.HasColumnType("INTEGER");
b.Property<int>("ProgramScheduleId")
.HasColumnType("INTEGER");
b.Property<int>("MediaCollectionId")
.HasColumnType("INTEGER");
b.HasKey("PlayoutId", "ProgramScheduleId", "MediaCollectionId");
b.HasIndex("MediaCollectionId");
b.HasIndex("ProgramScheduleId");
b.ToTable("PlayoutProgramScheduleItemAnchors");
});
modelBuilder.Entity(
"ErsatzTV.Core.Domain.PlexMediaSourceConnection",
b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<bool>("IsActive")
.HasColumnType("INTEGER");
b.Property<int?>("PlexMediaSourceId")
.HasColumnType("INTEGER");
b.Property<string>("Uri")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("PlexMediaSourceId");
b.ToTable("PlexMediaSourceConnections");
});
modelBuilder.Entity(
"ErsatzTV.Core.Domain.PlexMediaSourceLibrary",
b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("Key")
.HasColumnType("TEXT");
b.Property<int>("MediaType")
.HasColumnType("INTEGER");
b.Property<string>("Name")
.HasColumnType("TEXT");
b.Property<int?>("PlexMediaSourceId")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("PlexMediaSourceId");
b.ToTable("PlexMediaSourceLibraries");
});
modelBuilder.Entity(
"ErsatzTV.Core.Domain.ProgramSchedule",
b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int>("MediaCollectionPlaybackOrder")
.HasColumnType("INTEGER");
b.Property<string>("Name")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("Name")
.IsUnique();
b.ToTable("ProgramSchedules");
});
modelBuilder.Entity(
"ErsatzTV.Core.Domain.ProgramScheduleItem",
b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int>("Index")
.HasColumnType("INTEGER");
b.Property<int>("MediaCollectionId")
.HasColumnType("INTEGER");
b.Property<int>("ProgramScheduleId")
.HasColumnType("INTEGER");
b.Property<TimeSpan?>("StartTime")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("MediaCollectionId");
b.HasIndex("ProgramScheduleId");
b.ToTable("ProgramScheduleItems");
});
modelBuilder.Entity(
"ErsatzTV.Core.Domain.Resolution",
b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int>("Height")
.HasColumnType("INTEGER");
b.Property<string>("Name")
.HasColumnType("TEXT");
b.Property<int>("Width")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.ToTable("Resolutions");
});
modelBuilder.Entity(
"MediaItemSimpleMediaCollection",
b =>
{
b.Property<int>("ItemsId")
.HasColumnType("INTEGER");
b.Property<int>("SimpleMediaCollectionsId")
.HasColumnType("INTEGER");
b.HasKey("ItemsId", "SimpleMediaCollectionsId");
b.HasIndex("SimpleMediaCollectionsId");
b.ToTable("MediaItemSimpleMediaCollection");
});
modelBuilder.Entity(
"ErsatzTV.Core.Domain.SimpleMediaCollection",
b =>
{
b.HasBaseType("ErsatzTV.Core.Domain.MediaCollection");
b.ToTable("SimpleMediaCollections");
});
modelBuilder.Entity(
"ErsatzTV.Core.Domain.TelevisionMediaCollection",
b =>
{
b.HasBaseType("ErsatzTV.Core.Domain.MediaCollection");
b.Property<int?>("SeasonNumber")
.HasColumnType("INTEGER");
b.Property<string>("ShowTitle")
.HasColumnType("TEXT");
b.HasIndex("ShowTitle", "SeasonNumber")
.IsUnique();
b.ToTable("TelevisionMediaCollections");
});
modelBuilder.Entity(
"ErsatzTV.Core.Domain.LocalMediaSource",
b =>
{
b.HasBaseType("ErsatzTV.Core.Domain.MediaSource");
b.Property<string>("Folder")
.HasColumnType("TEXT");
b.Property<int>("MediaType")
.HasColumnType("INTEGER");
b.ToTable("LocalMediaSources");
});
modelBuilder.Entity(
"ErsatzTV.Core.Domain.PlexMediaSource",
b =>
{
b.HasBaseType("ErsatzTV.Core.Domain.MediaSource");
b.Property<string>("ClientIdentifier")
.HasColumnType("TEXT");
b.Property<string>("ProductVersion")
.HasColumnType("TEXT");
b.ToTable("PlexMediaSources");
});
modelBuilder.Entity(
"ErsatzTV.Core.Domain.ProgramScheduleItemDuration",
b =>
{
b.HasBaseType("ErsatzTV.Core.Domain.ProgramScheduleItem");
b.Property<bool>("OfflineTail")
.HasColumnType("INTEGER");
b.Property<TimeSpan>("PlayoutDuration")
.HasColumnType("TEXT");
b.ToTable("ProgramScheduleDurationItems");
});
modelBuilder.Entity(
"ErsatzTV.Core.Domain.ProgramScheduleItemFlood",
b =>
{
b.HasBaseType("ErsatzTV.Core.Domain.ProgramScheduleItem");
b.ToTable("ProgramScheduleFloodItems");
});
modelBuilder.Entity(
"ErsatzTV.Core.Domain.ProgramScheduleItemMultiple",
b =>
{
b.HasBaseType("ErsatzTV.Core.Domain.ProgramScheduleItem");
b.Property<int>("Count")
.HasColumnType("INTEGER");
b.ToTable("ProgramScheduleMultipleItems");
});
modelBuilder.Entity(
"ErsatzTV.Core.Domain.ProgramScheduleItemOne",
b =>
{
b.HasBaseType("ErsatzTV.Core.Domain.ProgramScheduleItem");
b.ToTable("ProgramScheduleOneItems");
});
modelBuilder.Entity(
"ErsatzTV.Core.Domain.Channel",
b =>
{
b.HasOne("ErsatzTV.Core.Domain.FFmpegProfile", "FFmpegProfile")
.WithMany()
.HasForeignKey("FFmpegProfileId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("FFmpegProfile");
});
modelBuilder.Entity(
"ErsatzTV.Core.Domain.FFmpegProfile",
b =>
{
b.HasOne("ErsatzTV.Core.Domain.Resolution", "Resolution")
.WithMany()
.HasForeignKey("ResolutionId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Resolution");
});
modelBuilder.Entity(
"ErsatzTV.Core.Domain.MediaItem",
b =>
{
b.HasOne("ErsatzTV.Core.Domain.MediaSource", "Source")
.WithMany()
.HasForeignKey("MediaSourceId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.OwnsOne(
"ErsatzTV.Core.Domain.MediaMetadata",
"Metadata",
b1 =>
{
b1.Property<int>("MediaItemId")
.HasColumnType("INTEGER");
b1.Property<DateTime?>("Aired")
.HasColumnType("TEXT");
b1.Property<string>("AudioCodec")
.HasColumnType("TEXT");
b1.Property<string>("ContentRating")
.HasColumnType("TEXT");
b1.Property<string>("Description")
.HasColumnType("TEXT");
b1.Property<string>("DisplayAspectRatio")
.HasColumnType("TEXT");
b1.Property<TimeSpan>("Duration")
.HasColumnType("TEXT");
b1.Property<int?>("EpisodeNumber")
.HasColumnType("INTEGER");
b1.Property<int>("Height")
.HasColumnType("INTEGER");
b1.Property<int>("MediaType")
.HasColumnType("INTEGER");
b1.Property<string>("SampleAspectRatio")
.HasColumnType("TEXT");
b1.Property<int?>("SeasonNumber")
.HasColumnType("INTEGER");
b1.Property<string>("Subtitle")
.HasColumnType("TEXT");
b1.Property<string>("Title")
.HasColumnType("TEXT");
b1.Property<string>("VideoCodec")
.HasColumnType("TEXT");
b1.Property<int>("VideoScanType")
.HasColumnType("INTEGER");
b1.Property<int>("Width")
.HasColumnType("INTEGER");
b1.HasKey("MediaItemId");
b1.ToTable("MediaItems");
b1.WithOwner()
.HasForeignKey("MediaItemId");
});
b.Navigation("Metadata");
b.Navigation("Source");
});
modelBuilder.Entity(
"ErsatzTV.Core.Domain.Playout",
b =>
{
b.HasOne("ErsatzTV.Core.Domain.Channel", "Channel")
.WithMany("Playouts")
.HasForeignKey("ChannelId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("ErsatzTV.Core.Domain.ProgramSchedule", "ProgramSchedule")
.WithMany("Playouts")
.HasForeignKey("ProgramScheduleId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.OwnsOne(
"ErsatzTV.Core.Domain.PlayoutAnchor",
"Anchor",
b1 =>
{
b1.Property<int>("PlayoutId")
.HasColumnType("INTEGER");
b1.Property<int>("NextScheduleItemId")
.HasColumnType("INTEGER");
b1.Property<DateTimeOffset>("NextStart")
.HasColumnType("TEXT");
b1.HasKey("PlayoutId");
b1.HasIndex("NextScheduleItemId");
b1.ToTable("Playouts");
b1.HasOne("ErsatzTV.Core.Domain.ProgramScheduleItem", "NextScheduleItem")
.WithMany()
.HasForeignKey("NextScheduleItemId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b1.WithOwner()
.HasForeignKey("PlayoutId");
b1.Navigation("NextScheduleItem");
});
b.Navigation("Anchor");
b.Navigation("Channel");
b.Navigation("ProgramSchedule");
});
modelBuilder.Entity(
"ErsatzTV.Core.Domain.PlayoutItem",
b =>
{
b.HasOne("ErsatzTV.Core.Domain.MediaItem", "MediaItem")
.WithMany()
.HasForeignKey("MediaItemId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("ErsatzTV.Core.Domain.Playout", "Playout")
.WithMany("Items")
.HasForeignKey("PlayoutId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("MediaItem");
b.Navigation("Playout");
});
modelBuilder.Entity(
"ErsatzTV.Core.Domain.PlayoutProgramScheduleAnchor",
b =>
{
b.HasOne("ErsatzTV.Core.Domain.MediaCollection", "MediaCollection")
.WithMany()
.HasForeignKey("MediaCollectionId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("ErsatzTV.Core.Domain.Playout", "Playout")
.WithMany("ProgramScheduleAnchors")
.HasForeignKey("PlayoutId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("ErsatzTV.Core.Domain.ProgramSchedule", "ProgramSchedule")
.WithMany()
.HasForeignKey("ProgramScheduleId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.OwnsOne(
"ErsatzTV.Core.Domain.MediaCollectionEnumeratorState",
"EnumeratorState",
b1 =>
{
b1.Property<int>("PlayoutProgramScheduleAnchorPlayoutId")
.HasColumnType("INTEGER");
b1.Property<int>("PlayoutProgramScheduleAnchorProgramScheduleId")
.HasColumnType("INTEGER");
b1.Property<int>("PlayoutProgramScheduleAnchorMediaCollectionId")
.HasColumnType("INTEGER");
b1.Property<int>("Index")
.HasColumnType("INTEGER");
b1.Property<int>("Seed")
.HasColumnType("INTEGER");
b1.HasKey(
"PlayoutProgramScheduleAnchorPlayoutId",
"PlayoutProgramScheduleAnchorProgramScheduleId",
"PlayoutProgramScheduleAnchorMediaCollectionId");
b1.ToTable("PlayoutProgramScheduleItemAnchors");
b1.WithOwner()
.HasForeignKey(
"PlayoutProgramScheduleAnchorPlayoutId",
"PlayoutProgramScheduleAnchorProgramScheduleId",
"PlayoutProgramScheduleAnchorMediaCollectionId");
});
b.Navigation("EnumeratorState");
b.Navigation("MediaCollection");
b.Navigation("Playout");
b.Navigation("ProgramSchedule");
});
modelBuilder.Entity(
"ErsatzTV.Core.Domain.PlexMediaSourceConnection",
b =>
{
b.HasOne("ErsatzTV.Core.Domain.PlexMediaSource", null)
.WithMany("Connections")
.HasForeignKey("PlexMediaSourceId");
});
modelBuilder.Entity(
"ErsatzTV.Core.Domain.PlexMediaSourceLibrary",
b =>
{
b.HasOne("ErsatzTV.Core.Domain.PlexMediaSource", null)
.WithMany("Libraries")
.HasForeignKey("PlexMediaSourceId");
});
modelBuilder.Entity(
"ErsatzTV.Core.Domain.ProgramScheduleItem",
b =>
{
b.HasOne("ErsatzTV.Core.Domain.MediaCollection", "MediaCollection")
.WithMany()
.HasForeignKey("MediaCollectionId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("ErsatzTV.Core.Domain.ProgramSchedule", "ProgramSchedule")
.WithMany("Items")
.HasForeignKey("ProgramScheduleId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("MediaCollection");
b.Navigation("ProgramSchedule");
});
modelBuilder.Entity(
"MediaItemSimpleMediaCollection",
b =>
{
b.HasOne("ErsatzTV.Core.Domain.MediaItem", null)
.WithMany()
.HasForeignKey("ItemsId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("ErsatzTV.Core.Domain.SimpleMediaCollection", null)
.WithMany()
.HasForeignKey("SimpleMediaCollectionsId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity(
"ErsatzTV.Core.Domain.SimpleMediaCollection",
b =>
{
b.HasOne("ErsatzTV.Core.Domain.MediaCollection", null)
.WithOne()
.HasForeignKey("ErsatzTV.Core.Domain.SimpleMediaCollection", "Id")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity(
"ErsatzTV.Core.Domain.TelevisionMediaCollection",
b =>
{
b.HasOne("ErsatzTV.Core.Domain.MediaCollection", null)
.WithOne()
.HasForeignKey("ErsatzTV.Core.Domain.TelevisionMediaCollection", "Id")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity(
"ErsatzTV.Core.Domain.LocalMediaSource",
b =>
{
b.HasOne("ErsatzTV.Core.Domain.MediaSource", null)
.WithOne()
.HasForeignKey("ErsatzTV.Core.Domain.LocalMediaSource", "Id")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity(
"ErsatzTV.Core.Domain.PlexMediaSource",
b =>
{
b.HasOne("ErsatzTV.Core.Domain.MediaSource", null)
.WithOne()
.HasForeignKey("ErsatzTV.Core.Domain.PlexMediaSource", "Id")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity(
"ErsatzTV.Core.Domain.ProgramScheduleItemDuration",
b =>
{
b.HasOne("ErsatzTV.Core.Domain.ProgramScheduleItem", null)
.WithOne()
.HasForeignKey("ErsatzTV.Core.Domain.ProgramScheduleItemDuration", "Id")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity(
"ErsatzTV.Core.Domain.ProgramScheduleItemFlood",
b =>
{
b.HasOne("ErsatzTV.Core.Domain.ProgramScheduleItem", null)
.WithOne()
.HasForeignKey("ErsatzTV.Core.Domain.ProgramScheduleItemFlood", "Id")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity(
"ErsatzTV.Core.Domain.ProgramScheduleItemMultiple",
b =>
{
b.HasOne("ErsatzTV.Core.Domain.ProgramScheduleItem", null)
.WithOne()
.HasForeignKey("ErsatzTV.Core.Domain.ProgramScheduleItemMultiple", "Id")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity(
"ErsatzTV.Core.Domain.ProgramScheduleItemOne",
b =>
{
b.HasOne("ErsatzTV.Core.Domain.ProgramScheduleItem", null)
.WithOne()
.HasForeignKey("ErsatzTV.Core.Domain.ProgramScheduleItemOne", "Id")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("ErsatzTV.Core.Domain.Channel", b => { b.Navigation("Playouts"); });
modelBuilder.Entity(
"ErsatzTV.Core.Domain.Playout",
b =>
{
b.Navigation("Items");
b.Navigation("ProgramScheduleAnchors");
});
modelBuilder.Entity(
"ErsatzTV.Core.Domain.ProgramSchedule",
b =>
{
b.Navigation("Items");
b.Navigation("Playouts");
});
modelBuilder.Entity(
"ErsatzTV.Core.Domain.PlexMediaSource",
b =>
{
b.Navigation("Connections");
b.Navigation("Libraries");
});
#pragma warning restore 612, 618
}
}
}
+4
View File
@@ -16,6 +16,10 @@
<PackageReference Include="LanguageExt.Core" Version="3.4.15" />
<PackageReference Include="MediatR.Extensions.Microsoft.DependencyInjection" Version="9.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="5.0.2" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="5.0.3">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="MudBlazor" Version="5.0.0" />
<PackageReference Include="Refit.HttpClientFactory" Version="6.0.1" />
<PackageReference Include="Serilog" Version="2.10.0" />
+2 -1
View File
@@ -1,6 +1,7 @@
using System;
using ErsatzTV.Infrastructure.Data;
using LanguageExt;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
@@ -28,7 +29,7 @@ namespace ErsatzTV.Extensions
private static TvContext Migrate(TvContext context)
{
context.Database.EnsureCreated();
context.Database.Migrate();
return context;
}
-25
View File
@@ -1,25 +0,0 @@
using ErsatzTV.Core.Domain;
using static LanguageExt.Prelude;
namespace ErsatzTV.Models.UI
{
public static class MediaItemExtensions
{
public static string GetDisplayDuration(this MediaItem mediaItem) =>
string.Format(
mediaItem.Metadata.Duration.TotalHours >= 1 ? @"{0:h\:mm\:ss}" : @"{0:mm\:ss}",
mediaItem.Metadata.Duration);
public static string GetDisplayTitle(this MediaItem mediaItem) =>
mediaItem.Metadata.MediaType == MediaType.TvShow &&
Optional(mediaItem.Metadata.SeasonNumber).IsSome &&
Optional(mediaItem.Metadata.EpisodeNumber).IsSome
? $"{mediaItem.Metadata.Title} s{mediaItem.Metadata.SeasonNumber:00}e{mediaItem.Metadata.EpisodeNumber:00}"
: mediaItem.Metadata.Title;
public static string GetDisplayMediaType(this MediaItem mediaItem) =>
mediaItem.Metadata.MediaType == MediaType.TvShow
? "TV Show"
: mediaItem.Metadata.MediaType.ToString();
}
}
+42 -47
View File
@@ -1,8 +1,16 @@
@page "/media/collections/{Id:int}/items"
@inject IDbContextFactory<TvContext> DbFactory
@using ErsatzTV.Application.MediaCollections
@using ErsatzTV.Application.MediaCollections.Commands
@using ErsatzTV.Application.MediaCollections.Queries
@using ErsatzTV.Application.MediaItems
@using ErsatzTV.Application.MediaItems.Queries
@using Unit = LanguageExt.Unit
@inject NavigationManager NavigationManager
@inject IMediator Mediator
@inject ILogger<MediaCollectionItemsEditor> Logger
@inject ISnackbar Snackbar
<MudTable Hover="true" Items="_mediaCollection.Items">
<MudTable Hover="true" Items="_collectionItems">
<ToolBarContent>
<MudText Typo="Typo.h6">@_mediaCollection.Name Media Items</MudText>
</ToolBarContent>
@@ -13,20 +21,20 @@
<MudTh>Duration</MudTh>
</HeaderContent>
<RowTemplate>
<MudTd DataLabel="Source">@context.Source.Name</MudTd>
<MudTd DataLabel="Type">@context.GetDisplayMediaType()</MudTd>
<MudTd DataLabel="Title">@context.GetDisplayTitle()</MudTd>
<MudTd DataLabel="Duration">@context.Metadata.Duration.ToString(@"hh\:mm\:ss\.fff")</MudTd>
<MudTd DataLabel="Source">@context.Source</MudTd>
<MudTd DataLabel="Type">@context.MediaType</MudTd>
<MudTd DataLabel="Title">@context.Title</MudTd>
<MudTd DataLabel="Duration">@context.Duration</MudTd>
</RowTemplate>
<PagerContent>
@if (_mediaCollection.Items.Count > 0)
@if (_collectionItems.Any())
{
<MudTablePager/>
}
</PagerContent>
</MudTable>
<MudTable @ref="_table" Hover="true" ServerData="@(new Func<TableState, Task<TableData<MediaItem>>>(ServerReload))" Class="mt-8">
<MudTable @ref="_table" Hover="true" ServerData="@(new Func<TableState, Task<TableData<MediaItemSearchResultViewModel>>>(ServerReload))" Class="mt-8">
<ToolBarContent>
<MudText Typo="Typo.h6">All Media Items</MudText>
<MudToolBarSpacer/>
@@ -41,10 +49,10 @@
<MudTh>Duration</MudTh>
</HeaderContent>
<RowTemplate>
<MudTd DataLabel="Source">@context.Source.Name</MudTd>
<MudTd DataLabel="Type">@context.GetDisplayMediaType()</MudTd>
<MudTd DataLabel="Title">@context.GetDisplayTitle()</MudTd>
<MudTd DataLabel="Duration">@context.GetDisplayDuration()</MudTd>
<MudTd DataLabel="Source">@context.Source</MudTd>
<MudTd DataLabel="Type">@context.MediaType</MudTd>
<MudTd DataLabel="Title">@context.Title</MudTd>
<MudTd DataLabel="Duration">@context.Duration</MudTd>
</RowTemplate>
<PagerContent>
<MudTablePager/>
@@ -59,32 +67,27 @@
[Parameter]
public int Id { get; set; }
private SimpleMediaCollection _mediaCollection;
private MediaCollectionViewModel _mediaCollection;
private IEnumerable<MediaItemSearchResultViewModel> _collectionItems;
protected override async Task OnParametersSetAsync() => await LoadMediaCollectionAsync();
private List<int> _mediaItemIds;
private IEnumerable<MediaItem> _pagedData;
private MudTable<MediaItem> _table;
private IEnumerable<MediaItemSearchResultViewModel> _pagedData;
private MudTable<MediaItemSearchResultViewModel> _table;
private int _totalItems;
private string _searchString;
private async Task<TableData<MediaItem>> ServerReload(TableState state)
private async Task<TableData<MediaItemSearchResultViewModel>> ServerReload(TableState state)
{
await using TvContext context = DbFactory.CreateDbContext();
IQueryable<MediaItem> data = from c in context.MediaItems.Include(c => c.Source) select c;
if (!string.IsNullOrEmpty(_searchString))
{
data = data.Where(c => EF.Functions.Like(c.Metadata.Title, $"%{_searchString}%"));
}
List<MediaItemSearchResultViewModel> data = await Mediator.Send(new SearchAllMediaItems(_searchString));
_mediaItemIds = data.Map(c => c.Id).ToList();
_totalItems = data.Count();
_totalItems = data.Count;
_pagedData = data.OrderBy(c => c.Id).Skip(state.Page * state.PageSize).Take(state.PageSize).ToArray();
return new TableData<MediaItem> { TotalItems = _totalItems, Items = _pagedData };
return new TableData<MediaItemSearchResultViewModel> { TotalItems = _totalItems, Items = _pagedData };
}
private async Task OnSearch(string text)
@@ -95,32 +98,24 @@
private async Task AddResultsAsync()
{
await using TvContext context = DbFactory.CreateDbContext();
SimpleMediaCollection mediaCollection = await context.SimpleMediaCollections.FindAsync(_mediaCollection.Id);
await context.Entry(mediaCollection).Collection(cg => cg.Items).LoadAsync();
IEnumerable<int> existingMediaItems = _mediaCollection.Items.Select(c => c.Id);
IQueryable<MediaItem> mediaItemsToAdd = from c in context.MediaItems
where _mediaItemIds.Contains(c.Id) && !existingMediaItems.Contains(c.Id)
select c;
foreach (MediaItem mediaItem in mediaItemsToAdd)
{
mediaCollection.Items.Add(mediaItem);
}
context.MediaCollections.Update(mediaCollection);
await context.SaveChangesAsync();
await LoadMediaCollectionAsync();
Either<BaseError, Unit> result = await Mediator.Send(new AddItemsToSimpleMediaCollection(Id, _mediaItemIds));
await result.Match(
async _ => await LoadMediaCollectionAsync(),
error =>
{
Snackbar.Add(error.Value, Severity.Error);
Logger.LogError("Error adding items to media collection: {Error}", error.Value);
return Task.CompletedTask;
});
}
private async Task LoadMediaCollectionAsync()
{
await using TvContext context = DbFactory.CreateDbContext();
_mediaCollection = await context.SimpleMediaCollections
.AsNoTracking()
.Include(cg => cg.Items)
.ThenInclude(c => c.Source)
.FirstAsync(cg => cg.Id == Id);
Option<Tuple<MediaCollectionViewModel, List<MediaItemSearchResultViewModel>>> maybeResult =
await Mediator.Send(new GetSimpleMediaCollectionWithItemsById(Id));
maybeResult.Match(
result => (_mediaCollection, _collectionItems) = result,
() => NavigationManager.NavigateTo("404"));
}
}
+5 -1
View File
@@ -91,7 +91,11 @@
_totalItems = aggregateData.Count;
_pagedData = aggregateData.Skip(state.Page * state.PageSize).Take(state.PageSize);
_pagedData = aggregateData
.Skip(_totalItems <= state.PageSize ? 0 : state.Page * state.PageSize)
.Take(state.PageSize)
.OrderBy(c => c.Name);
return new TableData<MediaCollectionSummaryViewModel> { TotalItems = _totalItems, Items = _pagedData };
}
-1
View File
@@ -1,5 +1,4 @@
@page "/media/items"
@inject IDbContextFactory<TvContext> DbFactory
<MudTabs Elevation="1">
<MudTabPanel Text="TV Shows">
+2 -2
View File
@@ -49,10 +49,10 @@
}
private Task<IEnumerable<ChannelViewModel>> SearchChannels(string value) =>
_channels.Filter(c => $"{c.Number} - {c.Name}".ToLowerInvariant().Contains(value ?? string.Empty)).AsTask();
_channels.Filter(c => $"{c.Number} - {c.Name}".Contains(value ?? string.Empty, StringComparison.OrdinalIgnoreCase)).AsTask();
private Task<IEnumerable<ProgramScheduleViewModel>> SearchProgramSchedules(string value) =>
_programSchedules.Filter(c => c.Name.ToLowerInvariant().StartsWith(value ?? string.Empty)).AsTask();
_programSchedules.Filter(c => c.Name.Contains(value ?? string.Empty, StringComparison.OrdinalIgnoreCase)).AsTask();
private async Task HandleSubmitAsync()
+1 -1
View File
@@ -170,7 +170,7 @@
}
private Task<IEnumerable<MediaCollectionViewModel>> SearchMediaCollections(string value) =>
_mediaCollections.Filter(c => c.Name.ToLowerInvariant().StartsWith(value ?? string.Empty)).AsTask();
_mediaCollections.Filter(c => c.Name.Contains(value ?? string.Empty, StringComparison.OrdinalIgnoreCase)).AsTask();
private async Task SaveChanges()
{
+1 -1
View File
@@ -54,4 +54,4 @@ namespace ErsatzTV
.UseKestrel(options => options.AddServerHeader = false))
.UseSerilog();
}
}
}
+4 -9
View File
@@ -9,21 +9,16 @@
<MudIconButton Icon="@Icons.Material.Filled.Menu" Color="Color.Inherit" Edge="Edge.Start" OnClick="@(_ => DrawerToggle())"/>
<MudText Typo="Typo.h5" Class="ml-3">ErsatzTV</MudText>
<MudAppBarSpacer/>
<MudTooltip Text="Channels M3U">
<MudIconButton Icon="@Icons.Custom.FileFormats.FileVideo" Color="Color.Inherit" Link="/iptv/channels.m3u" Target="_blank"/>
</MudTooltip>
<MudTooltip Text="EPG XMLTV">
<MudIconButton Icon="@Icons.Custom.FileFormats.FileCode" Color="Color.Inherit" Link="/iptv/xmltv.xml" Target="_blank"/>
</MudTooltip>
<MudLink Style="@($"color:{Colors.Shades.White}")" Color="Color.Inherit" Href="/iptv/channels.m3u" Target="_blank" Underline="Underline.None">M3U</MudLink>
<MudLink Style="@($"color:{Colors.Shades.White}")" Color="Color.Inherit" Href="/iptv/xmltv.xml" Target="_blank" Class="mx-4" Underline="Underline.None">XMLTV</MudLink>
<MudLink Style="@($"color:{Colors.Shades.White}")" Color="Color.Inherit" Href="/swagger" Target="_blank" Class="mr-4" Underline="Underline.None">API</MudLink>
<MudDivider Vertical="true" FlexItem="true" DividerType="DividerType.Middle" Class="mx-4 my-5"/>
<MudTooltip Text="Discord">
<MudIconButton Icon="fab fa-discord" Color="Color.Inherit" Link="https://discord.gg/hHaJm3yGy6" Target="_blank"/>
</MudTooltip>
<MudTooltip Text="GitHub">
<MudIconButton Icon="@Icons.Custom.Brands.GitHub" Color="Color.Inherit" Link="https://github.com/jasongdove/ErsatzTV" Target="_blank"/>
</MudTooltip>
<MudTooltip Text="OpenAPI">
<MudIconButton Icon="@Icons.Material.Filled.Api" Color="Color.Inherit" Link="/swagger" Target="_blank"/>
</MudTooltip>
</MudAppBar>
<MudDrawer @bind-Open="_drawerOpen" Elevation="2">
<MudNavMenu>
+5 -15
View File
@@ -1,5 +1,4 @@
using System;
using System.Diagnostics;
using System.IO;
using System.Threading.Channels;
using ErsatzTV.Application;
@@ -100,23 +99,14 @@ namespace ErsatzTV
// string xmltvPath = Path.Combine(appDataFolder, "xmltv.xml");
// Log.Logger.Information("XMLTV is at {XmltvPath}", xmltvPath);
services.AddDbContextFactory<TvContext>(
options =>
{
options.UseSqlite(
$"Data Source={FileSystemLayout.DatabasePath}",
o => o.UseQuerySplittingBehavior(QuerySplittingBehavior.SplitQuery));
if (Debugger.IsAttached)
{
options.EnableSensitiveDataLogging();
}
});
services.AddDbContext<TvContext>(
options => options.UseSqlite(
$"Data Source={FileSystemLayout.DatabasePath}",
o => o.UseQuerySplittingBehavior(QuerySplittingBehavior.SplitQuery)));
o =>
{
o.UseQuerySplittingBehavior(QuerySplittingBehavior.SplitQuery);
o.MigrationsAssembly("ErsatzTV.Infrastructure");
}));
services.AddMediatR(typeof(GetAllChannels).Assembly);
-2
View File
@@ -20,7 +20,5 @@
@using ErsatzTV.Core
@using ErsatzTV.Core.Domain
@using ErsatzTV.Infrastructure.Data
@using ErsatzTV.Models
@using ErsatzTV.Models.UI
@using ErsatzTV.Shared
@using ErsatzTV.ViewModels
+1 -1
View File
@@ -5,7 +5,7 @@ services:
build:
context: .
ports:
- "8409:80"
- "8409:8409"
volumes:
- ersatztv:/root/.local/share/ersatztv
#- /media/shared:/media/shared:ro