Skip to content

Commit

Permalink
Add basic handling for specials
Browse files Browse the repository at this point in the history
  • Loading branch information
Smith00101010 committed May 24, 2021
1 parent 767f289 commit a3487bc
Show file tree
Hide file tree
Showing 2 changed files with 146 additions and 21 deletions.
12 changes: 3 additions & 9 deletions Jellyfin.Plugin.TvMaze/Providers/TvMazeEpisodeImageProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -64,21 +64,15 @@ public async Task<IEnumerable<RemoteImageInfo>> GetImages(BaseItem item, Cancell
return Enumerable.Empty<RemoteImageInfo>();
}

var tvMazeId = TvHelpers.GetTvMazeId(episode.Series.ProviderIds);
var tvMazeId = TvHelpers.GetTvMazeId(episode.ProviderIds);
if (tvMazeId == null)
{
// Requires series TVMaze id.
return Enumerable.Empty<RemoteImageInfo>();
}

if (episode.IndexNumber == null || episode.ParentIndexNumber == null)
{
// Missing episode or season number.
// Requires episode TVMaze id.
return Enumerable.Empty<RemoteImageInfo>();
}

var tvMazeClient = new TvMazeClient(_httpClientFactory.CreateClient(NamedClient.Default), new RetryRateLimitingStrategy());
var tvMazeEpisode = await tvMazeClient.Shows.GetEpisodeByNumberAsync(tvMazeId.Value, episode.ParentIndexNumber.Value, episode.IndexNumber.Value).ConfigureAwait(false);
var tvMazeEpisode = await tvMazeClient.Episodes.GetEpisodeMainInformationAsync(tvMazeId.Value).ConfigureAwait(false);
if (tvMazeEpisode == null)
{
return Enumerable.Empty<RemoteImageInfo>();
Expand Down
155 changes: 143 additions & 12 deletions Jellyfin.Plugin.TvMaze/Providers/TvMazeEpisodeProvider.cs
Original file line number Diff line number Diff line change
@@ -1,18 +1,23 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Providers;
using Microsoft.Extensions.Logging;
using TvMaze.Api.Client;
using TvMaze.Api.Client.Configuration;
using TvMaze.Api.Client.Models;
using Episode = MediaBrowser.Controller.Entities.TV.Episode;
using TvMazeEpisode = TvMaze.Api.Client.Models.Episode;

namespace Jellyfin.Plugin.TvMaze.Providers
{
Expand All @@ -21,6 +26,10 @@ namespace Jellyfin.Plugin.TvMaze.Providers
/// </summary>
public class TvMazeEpisodeProvider : IRemoteMetadataProvider<Episode, EpisodeInfo>
{
private static readonly char[] _normalizedEpisodeNamesIgnoredChars = { ' ', '+', '.', '-', '_' };
private static readonly Regex _filenameDateRegex = new ("[0-9]{4}-[0-9]{2}-[0-9]{2}");
private static readonly Regex _filenameIdRegex = new (@"\[([0-9]+)\]");

private readonly IHttpClientFactory _httpClientFactory;
private readonly ILogger<TvMazeEpisodeProvider> _logger;

Expand Down Expand Up @@ -117,26 +126,105 @@ public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken
return null;
}

// The search query must provide an episode number.
if (!info.IndexNumber.HasValue || !info.ParentIndexNumber.HasValue)
var tvMazeClient = new TvMazeClient(_httpClientFactory.CreateClient(NamedClient.Default), new RetryRateLimitingStrategy());
// TODO maybe we should cache the last 10 shows or so because it is reasonable to assume that the metadata for multiple episodes of the same show will be requested
var allEpisodes = (await tvMazeClient.Shows.GetShowEpisodeListAsync(tvMazeId.Value, true).ConfigureAwait(false)).ToArray();
var possibleEpisodes = allEpisodes;

var episode = new Episode();
TvMazeEpisode? tvMazeEpisode = null;

if (info.ParentIndexNumber.HasValue && info.ParentIndexNumber != 0 && info.IndexNumber.HasValue)
{
return null;
tvMazeEpisode = possibleEpisodes.FirstOrDefault(e => e.Season == info.ParentIndexNumber && e.Number == info.IndexNumber);

if (tvMazeEpisode != null)
{
episode.ParentIndexNumber = tvMazeEpisode.Season;
episode.IndexNumber = tvMazeEpisode.Number;
}
}

if (tvMazeEpisode == null)
{
var filename = Path.GetFileNameWithoutExtension(info.Path);

var dateMatch = _filenameDateRegex.Match(filename);
if (dateMatch.Success)
{
possibleEpisodes = possibleEpisodes.Where(e => e.AirDate == dateMatch.Value).ToArray();

if (possibleEpisodes.Length == 0)
{
// Get rid of the date filter because there is no episode found
possibleEpisodes = allEpisodes;
}
else if (possibleEpisodes.Length == 1)
{
tvMazeEpisode = possibleEpisodes.First();
}
}

if (tvMazeEpisode == null)
{
var idMatch = _filenameIdRegex.Match(filename);
if (idMatch.Success)
{
tvMazeEpisode = possibleEpisodes.FirstOrDefault(e => e.Id.ToString(CultureInfo.InvariantCulture) == idMatch.Groups[1].Value);
}

if (tvMazeEpisode == null)
{
var normalizedFileName = NormalizeEpisodeName(filename);
var nameMatchedEpisodes = possibleEpisodes.Where(e => normalizedFileName.Contains(NormalizeEpisodeName(e.Name), StringComparison.CurrentCultureIgnoreCase)).ToArray();
if (nameMatchedEpisodes.Length > 0)
{
possibleEpisodes = nameMatchedEpisodes;
}

tvMazeEpisode = possibleEpisodes.FirstOrDefault();
if (possibleEpisodes.Length > 1)
{
var potentialEpisodesString = string.Join(", ", possibleEpisodes.Take(10).Select(ep => $"{ep.Name}(ID: {ep.Id})"));
var fileNameWithIdExample = tvMazeEpisode!.Name + $"[{tvMazeEpisode.Id}]" + Path.GetExtension(info.Path);
_logger.LogWarning("[GetMetadata] Found multiple possible episodes: {possibleEpisodes}. Include the name or the ID of the correct episode in the file name to make the match unique. TVmaze IDs have to be in brackets. (e.G., '{fileNameWithIdExample}')", potentialEpisodesString, fileNameWithIdExample);
return null;
}
}
}
}

var tvMazeClient = new TvMazeClient(_httpClientFactory.CreateClient(NamedClient.Default), new RetryRateLimitingStrategy());
var tvMazeEpisode = await tvMazeClient.Shows.GetEpisodeByNumberAsync(tvMazeId.Value, info.ParentIndexNumber.Value, info.IndexNumber.Value).ConfigureAwait(false);
if (tvMazeEpisode == null)
{
// No episode found.
return null;
}

var episode = new Episode
switch (tvMazeEpisode.Type)
{
Name = tvMazeEpisode.Name,
IndexNumber = tvMazeEpisode.Number,
ParentIndexNumber = tvMazeEpisode.Season
};
case EpisodeType.Regular:
episode.ParentIndexNumber = tvMazeEpisode.Season;
episode.IndexNumber = tvMazeEpisode.Number;
break;
case EpisodeType.SignificantSpecial:
case EpisodeType.InsignificantSpecial:
episode.ParentIndexNumber = 0;
// This way of calculating the index number is not perfectly stable because older specials could be added later but we need some index number to allow sorting by.
episode.IndexNumber = allEpisodes
.Where(e => e.Type == EpisodeType.InsignificantSpecial || e.Type == EpisodeType.SignificantSpecial)
.TakeWhile(e => e.Id != tvMazeEpisode.Id)
.Aggregate(1, (i, _) => i + 1);
break;
default:
_logger.LogWarning("[GetMetadata] Found unknown episode type '{episodeType}'.", tvMazeEpisode.Type);
break;
}

if (tvMazeEpisode.Type == EpisodeType.SignificantSpecial)
{
SetPositionInSeason(allEpisodes, tvMazeEpisode, episode);
}

episode.Name = tvMazeEpisode.Name;

if (DateTime.TryParse(tvMazeEpisode.AirDate, out var airDate))
{
Expand All @@ -153,5 +241,48 @@ public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken

return episode;
}

private static void SetPositionInSeason(IReadOnlyList<TvMazeEpisode> allEpisodes, TvMazeEpisode tvMazeEpisode, Episode episode)
{
for (int i = 0; i < allEpisodes.Count; i++)
{
if (allEpisodes[i].Id != tvMazeEpisode.Id)
{
continue;
}

if (i == 0 || allEpisodes[i - 1].Season != tvMazeEpisode.Season)
{
episode.AirsBeforeSeasonNumber = tvMazeEpisode.Season;
}
else
{
var firstRegularEpisodeAfter = allEpisodes.Skip(i + 1).SkipWhile(e => e.Type != EpisodeType.Regular).FirstOrDefault();
if (firstRegularEpisodeAfter != null && firstRegularEpisodeAfter.Season == tvMazeEpisode.Season)
{
episode.AirsBeforeSeasonNumber = tvMazeEpisode.Season;
episode.AirsBeforeEpisodeNumber = firstRegularEpisodeAfter.Number;
}
else
{
episode.AirsAfterSeasonNumber = tvMazeEpisode.Season;
}
}

return;
}
}

private static string NormalizeEpisodeName(string originalName)
{
var normalizedNameBuilder = new StringBuilder();

foreach (var character in originalName.Where(character => !_normalizedEpisodeNamesIgnoredChars.Contains(character)))
{
normalizedNameBuilder.Append(character);
}

return normalizedNameBuilder.ToString();
}
}
}

0 comments on commit a3487bc

Please sign in to comment.