Compare commits
9
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
88b645af2d | ||
|
|
941f1a59ee | ||
|
|
a3e20826a5 | ||
|
|
ebff29d6cd | ||
|
|
5a29fc1cbb | ||
|
|
f4487eb422 | ||
|
|
d04f3d574b | ||
|
|
a2e85655d2 | ||
|
|
4d49289dbd |
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -6,6 +6,7 @@ using NUnit.Framework;
|
||||
|
||||
namespace ErsatzTV.Core.Tests.FFmpeg
|
||||
{
|
||||
[TestFixture]
|
||||
public class FFmpegPlaybackSettingsCalculatorTests
|
||||
{
|
||||
public class CalculateSettings
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
|
||||
xml.WriteStartElement("display-name");
|
||||
xml.WriteAttributeString("lang", "en");
|
||||
xml.WriteString(channel.Name);
|
||||
xml.WriteEndElement(); // display-name
|
||||
|
||||
sb.AppendLine("</channel>");
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -82,7 +82,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(
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
@@ -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>
|
||||
|
||||
+1
-1
@@ -5,7 +5,7 @@ services:
|
||||
build:
|
||||
context: .
|
||||
ports:
|
||||
- "8409:80"
|
||||
- "8409:8409"
|
||||
volumes:
|
||||
- ersatztv:/root/.local/share/ersatztv
|
||||
#- /media/shared:/media/shared:ro
|
||||
|
||||
Reference in New Issue
Block a user