Compare commits

..
Author SHA1 Message Date
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
10 changed files with 151 additions and 69 deletions
+1
View File
@@ -61,6 +61,7 @@ jobs:
- name: Publish
uses: softprops/action-gh-release@v1
with:
prerelease: true
files: |
ErsatzTV*.zip
ErsatzTV*.tar.gz
@@ -25,11 +25,20 @@ namespace ErsatzTV.Core.Tests.Metadata
[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)]
[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)
{
var metadata = FallbackMetadataProvider.GetFallbackMetadata(path);
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);
+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"
};
}
}
+4 -3
View File
@@ -23,8 +23,8 @@ namespace ErsatzTV.Core.Iptv
public string ToXml()
{
var sb = new StringBuilder();
using var xml = XmlWriter.Create(sb);
using var ms = new MemoryStream();
using var xml = XmlWriter.Create(ms);
xml.WriteStartDocument();
xml.WriteStartElement("tv");
@@ -113,7 +113,8 @@ namespace ErsatzTV.Core.Iptv
xml.WriteEndElement(); // tv
xml.WriteEndDocument();
return sb.ToString();
xml.Flush();
return Encoding.UTF8.GetString(ms.ToArray());
}
}
}
@@ -1,4 +1,5 @@
using System.IO;
using System;
using System.IO;
using System.Text.RegularExpressions;
using ErsatzTV.Core.Domain;
@@ -6,12 +7,32 @@ namespace ErsatzTV.Core.Metadata
{
public static class FallbackMetadataProvider
{
public static MediaMetadata GetFallbackMetadata(string path)
public static MediaMetadata GetFallbackMetadata(MediaItem mediaItem)
{
string fileName = Path.GetFileName(path);
var metadata = new MediaMetadata { Title = fileName ?? path };
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);
@@ -23,6 +44,31 @@ namespace ErsatzTV.Core.Metadata
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;
}
+1 -1
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)
+71 -46
View File
@@ -1,28 +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(() => FallbackMetadataProvider.GetFallbackMetadata(mediaItem.Path));
maybeMetadata.IfNone(() => FallbackMetadataProvider.GetFallbackMetadata(mediaItem));
await ApplyMetadataUpdate(mediaItem, metadata);
}
@@ -50,54 +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));
if (!(mediaItem.Source is LocalMediaSource localMediaSource))
{
_logger.LogDebug("Media source {Name} is not a local media source", mediaItem.Source.Name);
return None;
}
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)
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
{
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);
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(
+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>
+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