Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
88b645af2d | ||
|
|
941f1a59ee | ||
|
|
a3e20826a5 |
@@ -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);
|
||||
|
||||
@@ -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"
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,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(
|
||||
|
||||
Reference in New Issue
Block a user