mirror of https://github.com/ErsatzTV/ErsatzTV.git
Browse Source
* add some basic channel stream selector models * change windows ffmpeg url * implement basic stream selection * fixespull/2078/head
50 changed files with 35986 additions and 115 deletions
@ -0,0 +1,3 @@
@@ -0,0 +1,3 @@
|
||||
namespace ErsatzTV.Application.Channels; |
||||
|
||||
public record GetChannelStreamSelectors : IRequest<List<string>>; |
@ -0,0 +1,14 @@
@@ -0,0 +1,14 @@
|
||||
using ErsatzTV.Core; |
||||
using ErsatzTV.Core.Interfaces.Metadata; |
||||
|
||||
namespace ErsatzTV.Application.Channels; |
||||
|
||||
public class GetChannelStreamSelectorsHandler(ILocalFileSystem localFileSystem) |
||||
: IRequestHandler<GetChannelStreamSelectors, List<string>> |
||||
{ |
||||
public Task<List<string>> Handle(GetChannelStreamSelectors request, CancellationToken cancellationToken) => |
||||
localFileSystem.ListFiles(FileSystemLayout.ChannelStreamSelectorsFolder) |
||||
.Map(Path.GetFileName) |
||||
.ToList() |
||||
.AsTask(); |
||||
} |
@ -0,0 +1,390 @@
@@ -0,0 +1,390 @@
|
||||
using ErsatzTV.Core.Domain; |
||||
using ErsatzTV.Core.FFmpeg; |
||||
using ErsatzTV.Core.Interfaces.FFmpeg; |
||||
using ErsatzTV.Core.Tests.Fakes; |
||||
using Microsoft.Extensions.Logging.Abstractions; |
||||
using NUnit.Framework; |
||||
using Shouldly; |
||||
|
||||
namespace ErsatzTV.Core.Tests.FFmpeg; |
||||
|
||||
[TestFixture] |
||||
public class CustomStreamSelectorTests |
||||
{ |
||||
[TestFixture] |
||||
public class SelectStreams |
||||
{ |
||||
private static readonly string TestFileName = Path.Combine(FileSystemLayout.ChannelStreamSelectorsFolder, "test.yml"); |
||||
|
||||
private Channel _channel; |
||||
private MediaItemAudioVersion _audioVersion; |
||||
private List<Subtitle> _subtitles; |
||||
|
||||
[SetUp] |
||||
public void SetUp() |
||||
{ |
||||
_channel = new Channel(Guid.Empty) |
||||
{ |
||||
StreamSelectorMode = ChannelStreamSelectorMode.Custom, |
||||
StreamSelector = TestFileName |
||||
}; |
||||
|
||||
_audioVersion = GetTestAudioVersion("eng"); |
||||
|
||||
_subtitles = |
||||
[ |
||||
new Subtitle { Id = 1, Language = "eng", Title = "Words" } |
||||
]; |
||||
} |
||||
|
||||
[Test] |
||||
public async Task Should_Select_eng_Audio_Exact_Match() |
||||
{ |
||||
const string YAML = |
||||
"""
|
||||
--- |
||||
items: |
||||
- audio_language: |
||||
- "eng" |
||||
""";
|
||||
|
||||
var streamSelector = new CustomStreamSelector( |
||||
new FakeLocalFileSystem([new FakeFileEntry(TestFileName) { Contents = YAML }]), |
||||
new NullLogger<CustomStreamSelector>()); |
||||
|
||||
StreamSelectorResult result = await streamSelector.SelectStreams(_channel, _audioVersion, _subtitles); |
||||
|
||||
result.AudioStream.IsSome.ShouldBeTrue(); |
||||
|
||||
foreach (MediaStream audioStream in result.AudioStream) |
||||
{ |
||||
audioStream.Index.ShouldBe(1); |
||||
audioStream.Language.ShouldBe("eng"); |
||||
} |
||||
} |
||||
|
||||
[Test] |
||||
public async Task Should_Select_eng_Audio_Exact_Match_Multiple_Audio_Languages() |
||||
{ |
||||
const string YAML = |
||||
"""
|
||||
--- |
||||
items: |
||||
- audio_language: |
||||
- "en" |
||||
- "eng" |
||||
""";
|
||||
|
||||
var streamSelector = new CustomStreamSelector( |
||||
new FakeLocalFileSystem([new FakeFileEntry(TestFileName) { Contents = YAML }]), |
||||
new NullLogger<CustomStreamSelector>()); |
||||
|
||||
StreamSelectorResult result = await streamSelector.SelectStreams(_channel, _audioVersion, _subtitles); |
||||
|
||||
result.AudioStream.IsSome.ShouldBeTrue(); |
||||
|
||||
foreach (MediaStream audioStream in result.AudioStream) |
||||
{ |
||||
audioStream.Index.ShouldBe(1); |
||||
audioStream.Language.ShouldBe("eng"); |
||||
} |
||||
} |
||||
|
||||
[Test] |
||||
public async Task Should_Select_eng_Audio_Exact_Match_Multiple_Items() |
||||
{ |
||||
const string YAML = |
||||
"""
|
||||
--- |
||||
items: |
||||
- audio_language: |
||||
- "de" |
||||
subtitle_language: |
||||
- "eng" |
||||
- audio_language: |
||||
- "eng" |
||||
disable_subtitles: true |
||||
""";
|
||||
|
||||
var streamSelector = new CustomStreamSelector( |
||||
new FakeLocalFileSystem([new FakeFileEntry(TestFileName) { Contents = YAML }]), |
||||
new NullLogger<CustomStreamSelector>()); |
||||
|
||||
StreamSelectorResult result = await streamSelector.SelectStreams(_channel, _audioVersion, _subtitles); |
||||
|
||||
result.AudioStream.IsSome.ShouldBeTrue(); |
||||
|
||||
foreach (MediaStream audioStream in result.AudioStream) |
||||
{ |
||||
audioStream.Index.ShouldBe(1); |
||||
audioStream.Language.ShouldBe("eng"); |
||||
} |
||||
} |
||||
|
||||
[Test] |
||||
public async Task Should_Select_eng_Audio_Pattern_Match() |
||||
{ |
||||
const string YAML = |
||||
"""
|
||||
--- |
||||
items: |
||||
- audio_language: |
||||
- "en*" |
||||
""";
|
||||
|
||||
var streamSelector = new CustomStreamSelector( |
||||
new FakeLocalFileSystem([new FakeFileEntry(TestFileName) { Contents = YAML }]), |
||||
new NullLogger<CustomStreamSelector>()); |
||||
|
||||
StreamSelectorResult result = await streamSelector.SelectStreams(_channel, _audioVersion, _subtitles); |
||||
|
||||
result.AudioStream.IsSome.ShouldBeTrue(); |
||||
|
||||
foreach (MediaStream audioStream in result.AudioStream) |
||||
{ |
||||
audioStream.Index.ShouldBe(1); |
||||
audioStream.Language.ShouldBe("eng"); |
||||
} |
||||
} |
||||
|
||||
[Test] |
||||
public async Task Should_Select_en_Audio_Pattern_Match() |
||||
{ |
||||
const string YAML = |
||||
"""
|
||||
--- |
||||
items: |
||||
- audio_language: |
||||
- "en*" |
||||
""";
|
||||
_audioVersion = GetTestAudioVersion("en"); |
||||
|
||||
var streamSelector = new CustomStreamSelector( |
||||
new FakeLocalFileSystem([new FakeFileEntry(TestFileName) { Contents = YAML }]), |
||||
new NullLogger<CustomStreamSelector>()); |
||||
|
||||
StreamSelectorResult result = await streamSelector.SelectStreams(_channel, _audioVersion, _subtitles); |
||||
|
||||
result.AudioStream.IsSome.ShouldBeTrue(); |
||||
|
||||
foreach (MediaStream audioStream in result.AudioStream) |
||||
{ |
||||
audioStream.Index.ShouldBe(1); |
||||
audioStream.Language.ShouldBe("en"); |
||||
} |
||||
} |
||||
|
||||
[Test] |
||||
public async Task disable_subtitles_Should_Select_No_Subtitles() |
||||
{ |
||||
const string YAML = |
||||
"""
|
||||
--- |
||||
items: |
||||
- audio_language: |
||||
- "eng" |
||||
disable_subtitles: true |
||||
""";
|
||||
|
||||
var streamSelector = new CustomStreamSelector( |
||||
new FakeLocalFileSystem([new FakeFileEntry(TestFileName) { Contents = YAML }]), |
||||
new NullLogger<CustomStreamSelector>()); |
||||
|
||||
StreamSelectorResult result = await streamSelector.SelectStreams(_channel, _audioVersion, _subtitles); |
||||
|
||||
result.Subtitle.IsSome.ShouldBeFalse(); |
||||
} |
||||
|
||||
[Test] |
||||
public async Task Should_Select_eng_Subtitle_Exact_Match() |
||||
{ |
||||
const string YAML = |
||||
"""
|
||||
--- |
||||
items: |
||||
- audio_language: |
||||
- "ja" |
||||
subtitle_language: |
||||
- "eng" |
||||
""";
|
||||
|
||||
var streamSelector = new CustomStreamSelector( |
||||
new FakeLocalFileSystem([new FakeFileEntry(TestFileName) { Contents = YAML }]), |
||||
new NullLogger<CustomStreamSelector>()); |
||||
|
||||
StreamSelectorResult result = await streamSelector.SelectStreams(_channel, _audioVersion, _subtitles); |
||||
|
||||
result.Subtitle.IsSome.ShouldBeTrue(); |
||||
|
||||
foreach (Subtitle subtitle in result.Subtitle) |
||||
{ |
||||
subtitle.Id.ShouldBe(1); |
||||
subtitle.Language.ShouldBe("eng"); |
||||
} |
||||
} |
||||
|
||||
[Test] |
||||
public async Task Should_Select_eng_Subtitle_Pattern_Match() |
||||
{ |
||||
const string YAML = |
||||
"""
|
||||
--- |
||||
items: |
||||
- audio_language: |
||||
- "ja" |
||||
subtitle_language: |
||||
- "en*" |
||||
""";
|
||||
|
||||
var streamSelector = new CustomStreamSelector( |
||||
new FakeLocalFileSystem([new FakeFileEntry(TestFileName) { Contents = YAML }]), |
||||
new NullLogger<CustomStreamSelector>()); |
||||
|
||||
StreamSelectorResult result = await streamSelector.SelectStreams(_channel, _audioVersion, _subtitles); |
||||
|
||||
result.Subtitle.IsSome.ShouldBeTrue(); |
||||
|
||||
foreach (Subtitle subtitle in result.Subtitle) |
||||
{ |
||||
subtitle.Id.ShouldBe(1); |
||||
subtitle.Language.ShouldBe("eng"); |
||||
} |
||||
} |
||||
|
||||
[Test] |
||||
public async Task Should_Select_en_Subtitle_Pattern_Match() |
||||
{ |
||||
const string YAML = |
||||
"""
|
||||
--- |
||||
items: |
||||
- audio_language: |
||||
- "ja" |
||||
subtitle_language: |
||||
- "en*" |
||||
""";
|
||||
_audioVersion = GetTestAudioVersion("en"); |
||||
|
||||
_subtitles = |
||||
[ |
||||
new Subtitle { Id = 1, Language = "en", Title = "Words" } |
||||
]; |
||||
|
||||
var streamSelector = new CustomStreamSelector( |
||||
new FakeLocalFileSystem([new FakeFileEntry(TestFileName) { Contents = YAML }]), |
||||
new NullLogger<CustomStreamSelector>()); |
||||
|
||||
StreamSelectorResult result = await streamSelector.SelectStreams(_channel, _audioVersion, _subtitles); |
||||
|
||||
result.Subtitle.IsSome.ShouldBeTrue(); |
||||
|
||||
foreach (Subtitle subtitle in result.Subtitle) |
||||
{ |
||||
subtitle.Id.ShouldBe(1); |
||||
subtitle.Language.ShouldBe("en"); |
||||
} |
||||
} |
||||
|
||||
[Test] |
||||
public async Task Should_Select_no_Subtitle_Exact_Match_Multiple_Items() |
||||
{ |
||||
const string YAML = |
||||
"""
|
||||
--- |
||||
items: |
||||
- audio_language: |
||||
- "de" |
||||
subtitle_language: |
||||
- "eng" |
||||
- audio_language: |
||||
- "eng" |
||||
disable_subtitles: true |
||||
""";
|
||||
|
||||
var streamSelector = new CustomStreamSelector( |
||||
new FakeLocalFileSystem([new FakeFileEntry(TestFileName) { Contents = YAML }]), |
||||
new NullLogger<CustomStreamSelector>()); |
||||
|
||||
StreamSelectorResult result = await streamSelector.SelectStreams(_channel, _audioVersion, _subtitles); |
||||
|
||||
result.AudioStream.IsSome.ShouldBeTrue(); |
||||
|
||||
foreach (MediaStream audioStream in result.AudioStream) |
||||
{ |
||||
audioStream.Index.ShouldBe(1); |
||||
audioStream.Language.ShouldBe("eng"); |
||||
} |
||||
|
||||
result.Subtitle.IsSome.ShouldBeFalse(); |
||||
} |
||||
|
||||
[Test] |
||||
public async Task Should_Select_Foreign_Audio_And_English_Subtitle_Multiple_Items() |
||||
{ |
||||
const string YAML = |
||||
"""
|
||||
--- |
||||
items: |
||||
- audio_language: |
||||
- "ja" |
||||
subtitle_language: |
||||
- "eng" |
||||
- audio_language: |
||||
- "eng" |
||||
disable_subtitles: true |
||||
""";
|
||||
|
||||
var streamSelector = new CustomStreamSelector( |
||||
new FakeLocalFileSystem([new FakeFileEntry(TestFileName) { Contents = YAML }]), |
||||
new NullLogger<CustomStreamSelector>()); |
||||
|
||||
StreamSelectorResult result = await streamSelector.SelectStreams(_channel, _audioVersion, _subtitles); |
||||
|
||||
result.AudioStream.IsSome.ShouldBeTrue(); |
||||
|
||||
foreach (MediaStream audioStream in result.AudioStream) |
||||
{ |
||||
audioStream.Index.ShouldBe(0); |
||||
audioStream.Language.ShouldBe("ja"); |
||||
} |
||||
|
||||
result.Subtitle.IsSome.ShouldBeTrue(); |
||||
|
||||
foreach (Subtitle subtitle in result.Subtitle) |
||||
{ |
||||
subtitle.Id.ShouldBe(1); |
||||
subtitle.Language.ShouldBe("eng"); |
||||
} |
||||
} |
||||
|
||||
private static MediaItemAudioVersion GetTestAudioVersion(string englishLanguage) |
||||
{ |
||||
var mediaItem = new OtherVideo(); |
||||
var mediaVersion = new MediaVersion |
||||
{ |
||||
Streams = |
||||
[ |
||||
new MediaStream |
||||
{ |
||||
Index = 0, |
||||
MediaStreamKind = MediaStreamKind.Audio, |
||||
Channels = 2, |
||||
Language = "ja", |
||||
Title = "Some Title", |
||||
}, |
||||
new MediaStream |
||||
{ |
||||
Index = 1, |
||||
MediaStreamKind = MediaStreamKind.Audio, |
||||
Channels = 6, |
||||
Language = englishLanguage, |
||||
Title = "Another Title", |
||||
Default = true |
||||
} |
||||
] |
||||
}; |
||||
|
||||
return new MediaItemAudioVersion(mediaItem, mediaVersion); |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,7 @@
@@ -0,0 +1,7 @@
|
||||
namespace ErsatzTV.Core.Domain; |
||||
|
||||
public enum ChannelStreamSelectorMode |
||||
{ |
||||
Default = 0, |
||||
Custom = 1 |
||||
} |
@ -0,0 +1,187 @@
@@ -0,0 +1,187 @@
|
||||
using System.IO.Enumeration; |
||||
using ErsatzTV.Core.Domain; |
||||
using ErsatzTV.Core.FFmpeg.Selector; |
||||
using ErsatzTV.Core.Interfaces.FFmpeg; |
||||
using ErsatzTV.Core.Interfaces.Metadata; |
||||
using Microsoft.Extensions.Logging; |
||||
using YamlDotNet.Serialization; |
||||
using YamlDotNet.Serialization.NamingConventions; |
||||
|
||||
namespace ErsatzTV.Core.FFmpeg; |
||||
|
||||
public class CustomStreamSelector(ILocalFileSystem localFileSystem, ILogger<CustomStreamSelector> logger) : ICustomStreamSelector |
||||
{ |
||||
public async Task<StreamSelectorResult> SelectStreams(Channel channel, MediaItemAudioVersion audioVersion, List<Subtitle> allSubtitles) |
||||
{ |
||||
try |
||||
{ |
||||
string streamSelectorFile = Path.Combine( |
||||
FileSystemLayout.ChannelStreamSelectorsFolder, |
||||
channel.StreamSelector); |
||||
|
||||
if (!localFileSystem.FileExists(streamSelectorFile)) |
||||
{ |
||||
logger.LogWarning("YAML stream selector file {File} does not exist; aborting.", channel.StreamSelector); |
||||
return StreamSelectorResult.None; |
||||
} |
||||
|
||||
StreamSelector streamSelector = await LoadStreamSelector(streamSelectorFile); |
||||
|
||||
var audioStreams = audioVersion.MediaVersion.Streams |
||||
.Where(s => s.MediaStreamKind == MediaStreamKind.Audio) |
||||
.ToList(); |
||||
|
||||
foreach (StreamSelectorItem streamSelectorItem in streamSelector.Items) |
||||
{ |
||||
var candidateAudioStreams = audioStreams.ToList(); |
||||
var candidateSubtitles = allSubtitles.ToList(); |
||||
|
||||
// try to find matching audio stream
|
||||
foreach (MediaStream audioStream in audioStreams.ToList()) |
||||
{ |
||||
var matches = false; |
||||
string safeTitle = audioStream.Title ?? string.Empty; |
||||
|
||||
if (streamSelectorItem.AudioLanguages.Count > 0) |
||||
{ |
||||
// match any of the listed languages
|
||||
foreach (string audioLanguage in streamSelectorItem.AudioLanguages) |
||||
{ |
||||
// special case
|
||||
if (audioLanguage == "*") |
||||
{ |
||||
matches = true; |
||||
} |
||||
|
||||
matches = matches || FileSystemName.MatchesSimpleExpression( |
||||
audioLanguage.ToLowerInvariant(), |
||||
audioStream.Language.ToLowerInvariant()); |
||||
} |
||||
} |
||||
else |
||||
{ |
||||
matches = true; |
||||
} |
||||
|
||||
if (streamSelectorItem.AudioTitleBlocklist |
||||
.Any(block => safeTitle.Contains(block, StringComparison.OrdinalIgnoreCase))) |
||||
{ |
||||
matches = false; |
||||
} |
||||
|
||||
if (streamSelectorItem.AudioTitleAllowlist.Count > 0) |
||||
{ |
||||
int matchCount = streamSelectorItem.AudioTitleAllowlist |
||||
.Count(block => safeTitle.Contains(block, StringComparison.OrdinalIgnoreCase)); |
||||
|
||||
if (matchCount == 0) |
||||
{ |
||||
matches = false; |
||||
} |
||||
} |
||||
|
||||
if (!matches) |
||||
{ |
||||
candidateAudioStreams.Remove(audioStream); |
||||
|
||||
logger.LogDebug( |
||||
"Audio stream {@Stream} does not match selector item {@SelectorItem}", |
||||
new { audioStream.Language, audioStream.Title }, |
||||
streamSelectorItem); |
||||
} |
||||
else |
||||
{ |
||||
logger.LogDebug( |
||||
"Audio stream {@Stream} matches selector item {@SelectorItem}", |
||||
new { audioStream.Language, audioStream.Title }, |
||||
streamSelectorItem); |
||||
} |
||||
} |
||||
|
||||
// try to find matching subtitle stream
|
||||
if (streamSelectorItem.DisableSubtitles) |
||||
{ |
||||
candidateSubtitles.Clear(); |
||||
} |
||||
else |
||||
{ |
||||
foreach (Subtitle subtitle in allSubtitles.ToList()) |
||||
{ |
||||
var matches = false; |
||||
|
||||
if (streamSelectorItem.SubtitleLanguages.Count > 0) |
||||
{ |
||||
// match any of the listed languages
|
||||
foreach (string subtitleLanguage in streamSelectorItem.SubtitleLanguages) |
||||
{ |
||||
// special case
|
||||
if (subtitleLanguage == "*") |
||||
{ |
||||
matches = true; |
||||
} |
||||
|
||||
matches = matches || FileSystemName.MatchesSimpleExpression( |
||||
subtitleLanguage, |
||||
subtitle.Language); |
||||
} |
||||
} |
||||
else |
||||
{ |
||||
matches = false; |
||||
} |
||||
|
||||
if (!matches) |
||||
{ |
||||
candidateSubtitles.Remove(subtitle); |
||||
|
||||
logger.LogDebug( |
||||
"Subtitle {@Subtitle} does not match selector item {@SelectorItem}", |
||||
new { subtitle.Language }, |
||||
streamSelectorItem); |
||||
} |
||||
else |
||||
{ |
||||
logger.LogDebug( |
||||
"Subtitle {@Subtitle} matches selector item {@SelectorItem}", |
||||
new { subtitle.Language }, |
||||
streamSelectorItem); |
||||
} |
||||
} |
||||
} |
||||
|
||||
Option<MediaStream> maybeAudioStream = candidateAudioStreams.HeadOrNone(); |
||||
Option<Subtitle> maybeSubtitle = candidateSubtitles.HeadOrNone(); |
||||
|
||||
if (maybeAudioStream.IsSome) |
||||
{ |
||||
return new StreamSelectorResult(maybeAudioStream, maybeSubtitle); |
||||
} |
||||
} |
||||
} |
||||
catch (Exception ex) |
||||
{ |
||||
logger.LogError(ex, "Unexpected error selecting streams"); |
||||
} |
||||
|
||||
return StreamSelectorResult.None; |
||||
} |
||||
|
||||
private async Task<StreamSelector> LoadStreamSelector(string streamSelectorFile) |
||||
{ |
||||
try |
||||
{ |
||||
string yaml = await localFileSystem.ReadAllText(streamSelectorFile); |
||||
|
||||
IDeserializer deserializer = new DeserializerBuilder() |
||||
.WithNamingConvention(CamelCaseNamingConvention.Instance) |
||||
.Build(); |
||||
|
||||
return deserializer.Deserialize<StreamSelector>(yaml); |
||||
} |
||||
catch (Exception ex) |
||||
{ |
||||
logger.LogWarning(ex, "Error loading YAML stream selector"); |
||||
throw; |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,8 @@
@@ -0,0 +1,8 @@
|
||||
namespace ErsatzTV.Core.FFmpeg.Selector; |
||||
|
||||
public enum StreamMetadata |
||||
{ |
||||
None = 0, |
||||
Default = 1, |
||||
Forced = 2 |
||||
} |
@ -0,0 +1,6 @@
@@ -0,0 +1,6 @@
|
||||
namespace ErsatzTV.Core.FFmpeg.Selector; |
||||
|
||||
public sealed class StreamSelector |
||||
{ |
||||
public List<StreamSelectorItem> Items { get; set; } = []; |
||||
} |
@ -0,0 +1,27 @@
@@ -0,0 +1,27 @@
|
||||
using YamlDotNet.Serialization; |
||||
|
||||
namespace ErsatzTV.Core.FFmpeg.Selector; |
||||
|
||||
public class StreamSelectorItem |
||||
{ |
||||
[YamlMember(Alias = "audio_language", ApplyNamingConventions = false)] |
||||
public List<string> AudioLanguages { get; set; } = []; |
||||
|
||||
// [YamlMember(Alias = "audio_metadata", ApplyNamingConventions = false)]
|
||||
// public StreamMetadata AudioMetadata { get; set; } = StreamMetadata.None;
|
||||
|
||||
[YamlMember(Alias = "audio_title_allowlist", ApplyNamingConventions = false)] |
||||
public List<string> AudioTitleAllowlist { get; set; } = []; |
||||
|
||||
[YamlMember(Alias = "audio_title_blocklist", ApplyNamingConventions = false)] |
||||
public List<string> AudioTitleBlocklist { get; set; } = []; |
||||
|
||||
[YamlMember(Alias = "disable_subtitles", ApplyNamingConventions = false)] |
||||
public bool DisableSubtitles { get; set; } |
||||
|
||||
[YamlMember(Alias = "subtitle_language", ApplyNamingConventions = false)] |
||||
public List<string> SubtitleLanguages { get; set; } = []; |
||||
|
||||
// [YamlMember(Alias = "subtitle_metadata", ApplyNamingConventions = false)]
|
||||
// public StreamMetadata SubtitleMetadata { get; set; } = StreamMetadata.None;
|
||||
} |
@ -0,0 +1,9 @@
@@ -0,0 +1,9 @@
|
||||
using ErsatzTV.Core.Domain; |
||||
using ErsatzTV.Core.FFmpeg; |
||||
|
||||
namespace ErsatzTV.Core.Interfaces.FFmpeg; |
||||
|
||||
public interface ICustomStreamSelector |
||||
{ |
||||
Task<StreamSelectorResult> SelectStreams(Channel channel, MediaItemAudioVersion audioVersion, List<Subtitle> allSubtitles); |
||||
} |
@ -0,0 +1,8 @@
@@ -0,0 +1,8 @@
|
||||
using ErsatzTV.Core.Domain; |
||||
|
||||
namespace ErsatzTV.Core.Interfaces.FFmpeg; |
||||
|
||||
public record StreamSelectorResult(Option<MediaStream> AudioStream, Option<Subtitle> Subtitle) |
||||
{ |
||||
public static readonly StreamSelectorResult None = new(Option<MediaStream>.None, Option<Subtitle>.None); |
||||
} |
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,40 @@
@@ -0,0 +1,40 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations; |
||||
|
||||
#nullable disable |
||||
|
||||
namespace ErsatzTV.Infrastructure.MySql.Migrations |
||||
{ |
||||
/// <inheritdoc />
|
||||
public partial class Add_ChannelStreamSelectorModes : Migration |
||||
{ |
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder) |
||||
{ |
||||
migrationBuilder.AddColumn<int>( |
||||
name: "AudioStreamSelectorMode", |
||||
table: "Channel", |
||||
type: "int", |
||||
nullable: false, |
||||
defaultValue: 0); |
||||
|
||||
migrationBuilder.AddColumn<int>( |
||||
name: "SubtitleStreamSelectorMode", |
||||
table: "Channel", |
||||
type: "int", |
||||
nullable: false, |
||||
defaultValue: 0); |
||||
} |
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder) |
||||
{ |
||||
migrationBuilder.DropColumn( |
||||
name: "AudioStreamSelectorMode", |
||||
table: "Channel"); |
||||
|
||||
migrationBuilder.DropColumn( |
||||
name: "SubtitleStreamSelectorMode", |
||||
table: "Channel"); |
||||
} |
||||
} |
||||
} |
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,40 @@
@@ -0,0 +1,40 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations; |
||||
|
||||
#nullable disable |
||||
|
||||
namespace ErsatzTV.Infrastructure.MySql.Migrations |
||||
{ |
||||
/// <inheritdoc />
|
||||
public partial class Add_ChannelStreamSelectors : Migration |
||||
{ |
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder) |
||||
{ |
||||
migrationBuilder.AddColumn<string>( |
||||
name: "AudioStreamSelector", |
||||
table: "Channel", |
||||
type: "longtext", |
||||
nullable: true) |
||||
.Annotation("MySql:CharSet", "utf8mb4"); |
||||
|
||||
migrationBuilder.AddColumn<string>( |
||||
name: "SubtitleStreamSelector", |
||||
table: "Channel", |
||||
type: "longtext", |
||||
nullable: true) |
||||
.Annotation("MySql:CharSet", "utf8mb4"); |
||||
} |
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder) |
||||
{ |
||||
migrationBuilder.DropColumn( |
||||
name: "AudioStreamSelector", |
||||
table: "Channel"); |
||||
|
||||
migrationBuilder.DropColumn( |
||||
name: "SubtitleStreamSelector", |
||||
table: "Channel"); |
||||
} |
||||
} |
||||
} |
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,60 @@
@@ -0,0 +1,60 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations; |
||||
|
||||
#nullable disable |
||||
|
||||
namespace ErsatzTV.Infrastructure.MySql.Migrations |
||||
{ |
||||
/// <inheritdoc />
|
||||
public partial class Refactor_ChannelStreamSelector : Migration |
||||
{ |
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder) |
||||
{ |
||||
migrationBuilder.DropColumn( |
||||
name: "AudioStreamSelector", |
||||
table: "Channel"); |
||||
|
||||
migrationBuilder.DropColumn( |
||||
name: "AudioStreamSelectorMode", |
||||
table: "Channel"); |
||||
|
||||
migrationBuilder.RenameColumn( |
||||
name: "SubtitleStreamSelectorMode", |
||||
table: "Channel", |
||||
newName: "StreamSelectorMode"); |
||||
|
||||
migrationBuilder.RenameColumn( |
||||
name: "SubtitleStreamSelector", |
||||
table: "Channel", |
||||
newName: "StreamSelector"); |
||||
} |
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder) |
||||
{ |
||||
migrationBuilder.RenameColumn( |
||||
name: "StreamSelectorMode", |
||||
table: "Channel", |
||||
newName: "SubtitleStreamSelectorMode"); |
||||
|
||||
migrationBuilder.RenameColumn( |
||||
name: "StreamSelector", |
||||
table: "Channel", |
||||
newName: "SubtitleStreamSelector"); |
||||
|
||||
migrationBuilder.AddColumn<string>( |
||||
name: "AudioStreamSelector", |
||||
table: "Channel", |
||||
type: "longtext", |
||||
nullable: true) |
||||
.Annotation("MySql:CharSet", "utf8mb4"); |
||||
|
||||
migrationBuilder.AddColumn<int>( |
||||
name: "AudioStreamSelectorMode", |
||||
table: "Channel", |
||||
type: "int", |
||||
nullable: false, |
||||
defaultValue: 0); |
||||
} |
||||
} |
||||
} |
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,40 @@
@@ -0,0 +1,40 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations; |
||||
|
||||
#nullable disable |
||||
|
||||
namespace ErsatzTV.Infrastructure.Sqlite.Migrations |
||||
{ |
||||
/// <inheritdoc />
|
||||
public partial class Add_ChannelStreamSelectorModes : Migration |
||||
{ |
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder) |
||||
{ |
||||
migrationBuilder.AddColumn<int>( |
||||
name: "AudioStreamSelectorMode", |
||||
table: "Channel", |
||||
type: "INTEGER", |
||||
nullable: false, |
||||
defaultValue: 0); |
||||
|
||||
migrationBuilder.AddColumn<int>( |
||||
name: "SubtitleStreamSelectorMode", |
||||
table: "Channel", |
||||
type: "INTEGER", |
||||
nullable: false, |
||||
defaultValue: 0); |
||||
} |
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder) |
||||
{ |
||||
migrationBuilder.DropColumn( |
||||
name: "AudioStreamSelectorMode", |
||||
table: "Channel"); |
||||
|
||||
migrationBuilder.DropColumn( |
||||
name: "SubtitleStreamSelectorMode", |
||||
table: "Channel"); |
||||
} |
||||
} |
||||
} |
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,38 @@
@@ -0,0 +1,38 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations; |
||||
|
||||
#nullable disable |
||||
|
||||
namespace ErsatzTV.Infrastructure.Sqlite.Migrations |
||||
{ |
||||
/// <inheritdoc />
|
||||
public partial class Add_ChannelStreamSelectors : Migration |
||||
{ |
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder) |
||||
{ |
||||
migrationBuilder.AddColumn<string>( |
||||
name: "AudioStreamSelector", |
||||
table: "Channel", |
||||
type: "TEXT", |
||||
nullable: true); |
||||
|
||||
migrationBuilder.AddColumn<string>( |
||||
name: "SubtitleStreamSelector", |
||||
table: "Channel", |
||||
type: "TEXT", |
||||
nullable: true); |
||||
} |
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder) |
||||
{ |
||||
migrationBuilder.DropColumn( |
||||
name: "AudioStreamSelector", |
||||
table: "Channel"); |
||||
|
||||
migrationBuilder.DropColumn( |
||||
name: "SubtitleStreamSelector", |
||||
table: "Channel"); |
||||
} |
||||
} |
||||
} |
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,59 @@
@@ -0,0 +1,59 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations; |
||||
|
||||
#nullable disable |
||||
|
||||
namespace ErsatzTV.Infrastructure.Sqlite.Migrations |
||||
{ |
||||
/// <inheritdoc />
|
||||
public partial class Refactor_ChannelStreamSelector : Migration |
||||
{ |
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder) |
||||
{ |
||||
migrationBuilder.DropColumn( |
||||
name: "AudioStreamSelector", |
||||
table: "Channel"); |
||||
|
||||
migrationBuilder.DropColumn( |
||||
name: "AudioStreamSelectorMode", |
||||
table: "Channel"); |
||||
|
||||
migrationBuilder.RenameColumn( |
||||
name: "SubtitleStreamSelectorMode", |
||||
table: "Channel", |
||||
newName: "StreamSelectorMode"); |
||||
|
||||
migrationBuilder.RenameColumn( |
||||
name: "SubtitleStreamSelector", |
||||
table: "Channel", |
||||
newName: "StreamSelector"); |
||||
} |
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder) |
||||
{ |
||||
migrationBuilder.RenameColumn( |
||||
name: "StreamSelectorMode", |
||||
table: "Channel", |
||||
newName: "SubtitleStreamSelectorMode"); |
||||
|
||||
migrationBuilder.RenameColumn( |
||||
name: "StreamSelector", |
||||
table: "Channel", |
||||
newName: "SubtitleStreamSelector"); |
||||
|
||||
migrationBuilder.AddColumn<string>( |
||||
name: "AudioStreamSelector", |
||||
table: "Channel", |
||||
type: "TEXT", |
||||
nullable: true); |
||||
|
||||
migrationBuilder.AddColumn<int>( |
||||
name: "AudioStreamSelectorMode", |
||||
table: "Channel", |
||||
type: "INTEGER", |
||||
nullable: false, |
||||
defaultValue: 0); |
||||
} |
||||
} |
||||
} |
Loading…
Reference in new issue