Skip to content

Commit

Permalink
release(minor): Version 4.1.0
Browse files Browse the repository at this point in the history
So series merging for non-VFS libraries was fixed. Turned out it there is a constraint with how Jellyfin deserialize the provider ids which we didn't meet, because the id **cannot** contain a `=`, and we had one, so it wouldn't set the id used for series merging on normal scans. Other than that, this is a minor release because we now allow disabling the filtering on movie libraries.

 ## Changes since last release

Here are the main changes since the last stable release (4.0.1):

---

`fix`: **Fix mapping for series id on episode model**.

`misc`: **Update exception message**.

`fix`: **Populate all lookup tables before returning**.

`misc`: **Add another extra type conditional**.

`feat`: **Allow disabling the filter for movie libraries**:

- Allow users to disable the filtering done on movie type libraries to keep out anything that's not a movie from the library. Use at your own risk. This essentially allows you to put any series in a movie type library and they will all show up as movies.

`fix`: **Change custom id format**:
Thanks to @EraYaN for the help debugging and finding the root cause of this issue!

- Fixes #62 (Series merging for non-VFS libraries)

- The previous custom id format caused problems because it contained a '=', which is not usable in the id (for now at least).

`fix`: **Change the direct access on ProviderIds**. Thanks to @EraYaN for their contribution.

`fix`: **Add fallback if lookup fails**:

- Added a fallback for when the lookup tables fails. The fallback will be slower, but at least it will provide a value and populate the lookup table for the next time.

- Trimmed trailing whitespaces.

For the full list of changes, please check out the [complete changelog](4.0.1...4.1.0) here on GitHub.
  • Loading branch information
revam committed Jul 14, 2024
2 parents 6be769d + 67edfa7 commit 1b88da3
Show file tree
Hide file tree
Showing 27 changed files with 181 additions and 122 deletions.
11 changes: 10 additions & 1 deletion Shokofin.sln
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@

Microsoft Visual Studio Solution File, Format Version 12.00
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Shokofin", "Shokofin\Shokofin.csproj", "{1DD876AE-9E68-4867-BDF6-B9050E63E936}"
# Visual Studio Version 17
VisualStudioVersion = 17.10.35013.160
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Shokofin", "Shokofin\Shokofin.csproj", "{1DD876AE-9E68-4867-BDF6-B9050E63E936}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Expand All @@ -13,4 +16,10 @@ Global
{1DD876AE-9E68-4867-BDF6-B9050E63E936}.Release|Any CPU.ActiveCfg = Release|Any CPU
{1DD876AE-9E68-4867-BDF6-B9050E63E936}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {61FD2206-EAAB-47E0-BB8D-1033C70B3973}
EndGlobalSection
EndGlobal
6 changes: 3 additions & 3 deletions Shokofin/API/Info/SeasonInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -163,17 +163,17 @@ public SeasonInfo(Series series, SeriesType? customType, IEnumerable<string> ext
// the previous episode anchors right.
var seriesIdOrder = new string[] { seriesId }.Concat(extraIds).ToList();
episodesList = episodesList
.OrderBy(e => seriesIdOrder.IndexOf(e.Shoko.IDs.Series.ToString()))
.OrderBy(e => seriesIdOrder.IndexOf(e.Shoko.IDs.ParentSeries.ToString()))
.ThenBy(e => e.AniDB.Type)
.ThenBy(e => e.AniDB.EpisodeNumber)
.ToList();
specialsList = specialsList
.OrderBy(e => seriesIdOrder.IndexOf(e.Shoko.IDs.Series.ToString()))
.OrderBy(e => seriesIdOrder.IndexOf(e.Shoko.IDs.ParentSeries.ToString()))
.ThenBy(e => e.AniDB.Type)
.ThenBy(e => e.AniDB.EpisodeNumber)
.ToList();
altEpisodesList = altEpisodesList
.OrderBy(e => seriesIdOrder.IndexOf(e.Shoko.IDs.Series.ToString()))
.OrderBy(e => seriesIdOrder.IndexOf(e.Shoko.IDs.ParentSeries.ToString()))
.ThenBy(e => e.AniDB.Type)
.ThenBy(e => e.AniDB.EpisodeNumber)
.ToList();
Expand Down
2 changes: 1 addition & 1 deletion Shokofin/API/Models/Episode.cs
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ public class TvDB

public class EpisodeIDs : IDs
{
public int Series { get; set; }
public int ParentSeries { get; set; }

public int AniDB { get; set; }

Expand Down
138 changes: 95 additions & 43 deletions Shokofin/API/ShokoAPIManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,14 +40,10 @@ public class ShokoAPIManager : IDisposable

private readonly ConcurrentDictionary<string, (string FileId, string SeriesId)> PathToFileIdAndSeriesIdDictionary = new();

private readonly ConcurrentDictionary<string, string> SeriesIdToPathDictionary = new();

private readonly ConcurrentDictionary<string, string> SeriesIdToDefaultSeriesIdDictionary = new();

private readonly ConcurrentDictionary<string, string?> SeriesIdToCollectionIdDictionary = new();

private readonly ConcurrentDictionary<string, string> EpisodeIdToEpisodePathDictionary = new();

private readonly ConcurrentDictionary<string, string> EpisodeIdToSeriesIdDictionary = new();

private readonly ConcurrentDictionary<string, List<string>> FileAndSeriesIdToEpisodeIdDictionary = new();
Expand Down Expand Up @@ -108,9 +104,9 @@ private void OnTrackerStalled(object? sender, EventArgs eventArgs)
public string StripMediaFolder(string fullPath)
{
Folder? mediaFolder = null;
lock (MediaFolderListLock)
lock (MediaFolderListLock)
mediaFolder = MediaFolderList.FirstOrDefault((folder) => fullPath.StartsWith(folder.Path + Path.DirectorySeparatorChar));
if (mediaFolder is not null)
if (mediaFolder is not null)
return fullPath[mediaFolder.Path.Length..];
if (Path.GetDirectoryName(fullPath) is not string directoryPath || LibraryManager.FindByPath(directoryPath, true)?.GetTopParent() is not Folder topParent)
return fullPath;
Expand All @@ -131,7 +127,6 @@ public void Dispose()
public void Clear()
{
Logger.LogDebug("Clearing data…");
EpisodeIdToEpisodePathDictionary.Clear();
EpisodeIdToSeriesIdDictionary.Clear();
FileAndSeriesIdToEpisodeIdDictionary.Clear();
lock (MediaFolderListLock)
Expand All @@ -142,7 +137,6 @@ public void Clear()
NameToSeriesIdDictionary.Clear();
SeriesIdToDefaultSeriesIdDictionary.Clear();
SeriesIdToCollectionIdDictionary.Clear();
SeriesIdToPathDictionary.Clear();
DataCache.Clear();
Logger.LogDebug("Cleanup complete.");
}
Expand Down Expand Up @@ -481,10 +475,6 @@ internal void AddFileLookupIds(string path, string fileId, string seriesId, IEnu
// Find the file info for the series.
var fileInfo = await CreateFileInfo(file, fileId, seriesId).ConfigureAwait(false);

// Add pointers for faster lookup.
foreach (var episodeInfo in fileInfo.EpisodeList)
EpisodeIdToEpisodePathDictionary.TryAdd(episodeInfo.Id, path);

// Add pointers for faster lookup.
AddFileLookupIds(path, fileId, seriesId, fileInfo.EpisodeList.Select(episode => episode.Id));

Expand Down Expand Up @@ -557,13 +547,27 @@ private Task<FileInfo> CreateFileInfo(File file, string fileId, string seriesId)
}
);

public bool TryGetFileIdForPath(string path, out string? fileId)
public bool TryGetFileIdForPath(string path, [NotNullWhen(true)] out string? fileId)
{
if (!string.IsNullOrEmpty(path) && PathToFileIdAndSeriesIdDictionary.TryGetValue(path, out var pair)) {
if (string.IsNullOrEmpty(path)) {
fileId = null;
return false;
}

// Fast path; using the lookup.
if (PathToFileIdAndSeriesIdDictionary.TryGetValue(path, out var pair)) {
fileId = pair.FileId;
return true;
}

// Slow path; getting the show from cache or remote and finding the default season's id.
Logger.LogDebug("Trying to find file id using the slow path. (Path={FullPath})", path);
if (GetFileInfoByPath(path).ConfigureAwait(false).GetAwaiter().GetResult() is { } tuple && tuple.Item1 is not null) {
var (fileInfo, _, _) = tuple;
fileId = fileInfo.Id;
return true;
}

fileId = null;
return false;
}
Expand Down Expand Up @@ -594,51 +598,85 @@ private EpisodeInfo CreateEpisodeInfo(Episode episode, string episodeId)
}
);

public bool TryGetEpisodeIdForPath(string path, out string? episodeId)
public bool TryGetEpisodeIdForPath(string path, [NotNullWhen(true)] out string? episodeId)
{
if (string.IsNullOrEmpty(path)) {
episodeId = null;
return false;
}
var result = PathToEpisodeIdsDictionary.TryGetValue(path, out var episodeIds);

var result = TryGetEpisodeIdsForPath(path, out var episodeIds);
episodeId = episodeIds?.FirstOrDefault();
return result;
}

public bool TryGetEpisodeIdsForPath(string path, out List<string>? episodeIds)
public bool TryGetEpisodeIdsForPath(string path, [NotNullWhen(true)] out List<string>? episodeIds)
{
if (string.IsNullOrEmpty(path)) {
episodeIds = null;
return false;
}
return PathToEpisodeIdsDictionary.TryGetValue(path, out episodeIds);

// Fast path; using the lookup.
if (PathToEpisodeIdsDictionary.TryGetValue(path, out episodeIds))
return true;

// Slow path; getting the show from cache or remote and finding the default season's id.
Logger.LogDebug("Trying to find episode ids using the slow path. (Path={FullPath})", path);
if (GetFileInfoByPath(path).ConfigureAwait(false).GetAwaiter().GetResult() is { } tuple && tuple.Item1 is not null) {
var (fileInfo, _, _) = tuple;
episodeIds = fileInfo.EpisodeList.Select(episodeInfo => episodeInfo.Id).ToList();
return episodeIds.Count is > 0;
}

episodeIds = null;
return false;
}

public bool TryGetEpisodeIdsForFileId(string fileId, string seriesId, out List<string>? episodeIds)
public bool TryGetEpisodeIdsForFileId(string fileId, string seriesId, [NotNullWhen(true)] out List<string>? episodeIds)
{
if (string.IsNullOrEmpty(fileId) || string.IsNullOrEmpty(seriesId)) {
episodeIds = null;
return false;
}
return FileAndSeriesIdToEpisodeIdDictionary.TryGetValue($"{fileId}:{seriesId}", out episodeIds);

// Fast path; using the lookup.
if (FileAndSeriesIdToEpisodeIdDictionary.TryGetValue($"{fileId}:{seriesId}", out episodeIds))
return true;

// Slow path; getting the show from cache or remote and finding the default season's id.
Logger.LogDebug("Trying to find episode ids using the slow path. (Series={SeriesId},File={FileId})", seriesId, fileId);
if (GetFileInfo(fileId, seriesId).ConfigureAwait(false).GetAwaiter().GetResult() is { } fileInfo) {
episodeIds = fileInfo.EpisodeList.Select(episodeInfo => episodeInfo.Id).ToList();
return true;
}

episodeIds = null;
return false;
}

public bool TryGetEpisodePathForId(string episodeId, out string? path)
public bool TryGetSeriesIdForEpisodeId(string episodeId, [NotNullWhen(true)] out string? seriesId)
{
if (string.IsNullOrEmpty(episodeId)) {
path = null;
seriesId = null;
return false;
}
return EpisodeIdToEpisodePathDictionary.TryGetValue(episodeId, out path);
}

public bool TryGetSeriesIdForEpisodeId(string episodeId, out string? seriesId)
{
if (string.IsNullOrEmpty(episodeId)) {
// Fast path; using the lookup.
if (EpisodeIdToSeriesIdDictionary.TryGetValue(episodeId, out seriesId))
return true;

// Slow path; asking the http client to get the series from remote to look up it's id.
Logger.LogDebug("Trying to find episode ids using the slow path. (Episode={EpisodeId})", episodeId);
try {
var series = APIClient.GetSeriesFromEpisode(episodeId).ConfigureAwait(false).GetAwaiter().GetResult();
seriesId = series.IDs.Shoko.ToString();
return true;
}
catch (ApiException ex) when (ex.StatusCode is System.Net.HttpStatusCode.NotFound) {
seriesId = null;
return false;
}
return EpisodeIdToSeriesIdDictionary.TryGetValue(episodeId, out seriesId);
}

#endregion
Expand Down Expand Up @@ -813,7 +851,7 @@ private async Task<SeasonInfo> CreateSeasonInfo(Series series)

Logger.LogTrace("Creating new series-to-season mapping for series. (Series={SeriesId})", primaryId);

// We potentially have a "follow-up" season candidate, so look for the "primary" season candidate, then jump into that.
// We potentially have a "follow-up" season candidate, so look for the "primary" season candidate, then jump into that.
var relations = await APIClient.GetSeriesRelations(primaryId).ConfigureAwait(false);
var mainTitle = series.AniDBEntity.Titles.First(title => title.Type == TitleType.Main).Value;
var result = YearRegex.Match(mainTitle);
Expand Down Expand Up @@ -926,16 +964,20 @@ public bool TryGetSeriesIdForPath(string path, [NotNullWhen(true)] out string? s
seriesId = null;
return false;
}
return PathToSeriesIdDictionary.TryGetValue(path, out seriesId);
}

public bool TryGetSeriesPathForId(string seriesId, [NotNullWhen(true)] out string? path)
{
if (string.IsNullOrEmpty(seriesId)) {
path = null;
return false;
// Fast path; using the lookup.
if (PathToSeriesIdDictionary.TryGetValue(path, out seriesId))
return true;

// Slow path; getting the show from cache or remote and finding the season's series id.
Logger.LogDebug("Trying to find the season's series id for {Path} using the slow path.", path);
if (GetSeasonInfoByPath(path).ConfigureAwait(false).GetAwaiter().GetResult() is { } seasonInfo) {
seriesId = seasonInfo.Id;
return true;
}
return SeriesIdToPathDictionary.TryGetValue(seriesId, out path);

seriesId = null;
return false;
}

public bool TryGetDefaultSeriesIdForSeriesId(string seriesId, [NotNullWhen(true)] out string? defaultSeriesId)
Expand All @@ -944,7 +986,20 @@ public bool TryGetDefaultSeriesIdForSeriesId(string seriesId, [NotNullWhen(true)
defaultSeriesId = null;
return false;
}
return SeriesIdToDefaultSeriesIdDictionary.TryGetValue(seriesId, out defaultSeriesId);

// Fast path; using the lookup.
if (SeriesIdToDefaultSeriesIdDictionary.TryGetValue(seriesId, out defaultSeriesId))
return true;

// Slow path; getting the show from cache or remote and finding the default season's id.
Logger.LogDebug("Trying to find the default series id for series using the slow path. (Series={SeriesId})", seriesId);
if (GetShowInfoForSeries(seriesId).ConfigureAwait(false).GetAwaiter().GetResult() is { } showInfo) {
defaultSeriesId = showInfo.Id;
return true;
}

defaultSeriesId = null;
return false;
}

private async Task<string?> GetSeriesIdForPath(string path)
Expand All @@ -959,8 +1014,6 @@ public bool TryGetDefaultSeriesIdForSeriesId(string seriesId, [NotNullWhen(true)
return null;

PathToSeriesIdDictionary[path] = seriesId;
SeriesIdToPathDictionary.TryAdd(seriesId, path);

return seriesId;
}

Expand All @@ -981,10 +1034,9 @@ public bool TryGetDefaultSeriesIdForSeriesId(string seriesId, [NotNullWhen(true)
continue;

PathToSeriesIdDictionary[path] = primaryId;
SeriesIdToPathDictionary.TryAdd(primaryId, path);

return primaryId;
}

return primaryId;
}

// In the edge case for series with only files with multiple
Expand Down
5 changes: 3 additions & 2 deletions Shokofin/Collections/CollectionManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
using MediaBrowser.Controller.Entities.Movies;
using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Library;
using MediaBrowser.Model.Entities;
using Microsoft.Extensions.Logging;
using Shokofin.API;
using Shokofin.API.Info;
Expand Down Expand Up @@ -666,7 +667,7 @@ private Dictionary<string, IReadOnlyList<BoxSet>> GetSeriesCollections()
Recursive = true,
})
.Cast<BoxSet>()
.Select(x => x.ProviderIds.TryGetValue(ShokoCollectionSeriesId.Name, out var seriesId) && !string.IsNullOrEmpty(seriesId) ? new { SeriesId = seriesId, BoxSet = x } : null)
.Select(x => x.TryGetProviderId(ShokoCollectionSeriesId.Name, out var seriesId) ? new { SeriesId = seriesId, BoxSet = x } : null)
.Where(x => x != null)
.GroupBy(x => x!.SeriesId, x => x!.BoxSet)
.ToDictionary(x => x.Key, x => x.ToList() as IReadOnlyList<BoxSet>);
Expand All @@ -683,7 +684,7 @@ private Dictionary<string, IReadOnlyList<BoxSet>> GetGroupCollections()
Recursive = true,
})
.Cast<BoxSet>()
.Select(x => x.ProviderIds.TryGetValue(ShokoCollectionGroupId.Name, out var groupId) && !string.IsNullOrEmpty(groupId) ? new { GroupId = groupId, BoxSet = x } : null)
.Select(x => x.TryGetProviderId(ShokoCollectionGroupId.Name, out var groupId) ? new { GroupId = groupId, BoxSet = x } : null)
.Where(x => x != null)
.GroupBy(x => x!.GroupId, x => x!.BoxSet)
.ToDictionary(x => x.Key, x => x.ToList() as IReadOnlyList<BoxSet>);
Expand Down
6 changes: 6 additions & 0 deletions Shokofin/Configuration/PluginConfiguration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,11 @@ public virtual string PrettyUrl
/// </summary>
public bool SeparateMovies { get; set; }

/// <summary>
/// Filter out anything that's not a movie in a movie library.
/// </summary>
public bool FilterMovieLibraries { get; set; }

/// <summary>
/// Append all specials in AniDB movie series as special features for
/// the movies.
Expand Down Expand Up @@ -542,6 +547,7 @@ public PluginConfiguration()
VFS_AddResolution = false;
UseGroupsForShows = false;
SeparateMovies = false;
FilterMovieLibraries = true;
MovieSpecialsAsExtraFeaturettes = false;
AddTrailers = true;
AddCreditsAsThemeVideos = true;
Expand Down
3 changes: 3 additions & 0 deletions Shokofin/Configuration/configController.js
Original file line number Diff line number Diff line change
Expand Up @@ -323,6 +323,7 @@ async function defaultSubmit(form) {
config.CollectionGrouping = form.querySelector("#CollectionGrouping").value;
config.CollectionMinSizeOfTwo = form.querySelector("#CollectionMinSizeOfTwo").checked;
config.SeparateMovies = form.querySelector("#SeparateMovies").checked;
config.FilterMovieLibraries = form.querySelector("#FilterMovieLibraries").checked;
config.SpecialsPlacement = form.querySelector("#SpecialsPlacement").value;
config.MovieSpecialsAsExtraFeaturettes = form.querySelector("#MovieSpecialsAsExtraFeaturettes").checked;
config.AddTrailers = form.querySelector("#AddTrailers").checked;
Expand Down Expand Up @@ -533,6 +534,7 @@ async function syncSettings(form) {
config.UseGroupsForShows = form.querySelector("#UseGroupsForShows").checked;
config.SeasonOrdering = form.querySelector("#SeasonOrdering").value;
config.SeparateMovies = form.querySelector("#SeparateMovies").checked;
config.FilterMovieLibraries = form.querySelector("#FilterMovieLibraries").checked;
config.CollectionGrouping = form.querySelector("#CollectionGrouping").value;
config.CollectionMinSizeOfTwo = form.querySelector("#CollectionMinSizeOfTwo").checked;
config.SpecialsPlacement = form.querySelector("#SpecialsPlacement").value;
Expand Down Expand Up @@ -1005,6 +1007,7 @@ export default function (page) {
form.querySelector("#CollectionGrouping").value = config.CollectionGrouping;
form.querySelector("#CollectionMinSizeOfTwo").checked = config.CollectionMinSizeOfTwo;
form.querySelector("#SeparateMovies").checked = config.SeparateMovies;
form.querySelector("#FilterMovieLibraries").checked = config.FilterMovieLibraries;
form.querySelector("#SpecialsPlacement").value = config.SpecialsPlacement === "Default" ? "AfterSeason" : config.SpecialsPlacement;
form.querySelector("#MovieSpecialsAsExtraFeaturettes").checked = config.MovieSpecialsAsExtraFeaturettes;
form.querySelector("#AddTrailers").checked = config.AddTrailers;
Expand Down
Loading

0 comments on commit 1b88da3

Please sign in to comment.