Feature: match movies using Levenshtein (#585)

This commit is contained in:
Jason Lyu
2025-11-02 15:48:14 -05:00
committed by GitHub
parent d5daf56076
commit ea4483072d
2 changed files with 102 additions and 7 deletions

View File

@@ -0,0 +1,54 @@
namespace Jellyfin.Plugin.MetaTube.Helpers;
public static class Levenshtein
{
public static int Distance(string value1, string value2)
{
if (value2.Length == 0)
{
return value1.Length;
}
int[] costs = new int[value2.Length];
for (int i = 0; i < costs.Length;)
{
costs[i] = ++i;
}
for (int i = 0; i < value1.Length; i++)
{
int cost = i;
int previousCost = i;
char value1Char = value1[i];
for (int j = 0; j < value2.Length; j++)
{
int currentCost = cost;
cost = costs[j];
if (value1Char != value2[j])
{
if (previousCost < currentCost)
{
currentCost = previousCost;
}
if (cost < currentCost)
{
currentCost = cost;
}
++currentCost;
}
costs[j] = currentCost;
previousCost = currentCost;
}
}
return costs[costs.Length - 1];
}
}

View File

@@ -1,6 +1,8 @@
using System.Text;
using System.Text.RegularExpressions;
using Jellyfin.Plugin.MetaTube.Configuration;
using Jellyfin.Plugin.MetaTube.Extensions;
using Jellyfin.Plugin.MetaTube.Helpers;
using Jellyfin.Plugin.MetaTube.Metadata;
using Jellyfin.Plugin.MetaTube.Translation;
using MediaBrowser.Controller.Entities;
@@ -148,7 +150,6 @@ public class MovieProvider : BaseProvider, IRemoteMetadataProvider<Movie, MovieI
Logger.Info("Add Collection for movie {0} [{1}]", pid.ToString(), m.Series);
}
// Add studio.
if (!string.IsNullOrWhiteSpace(m.Maker))
result.Item.AddStudio(m.Maker);
@@ -300,16 +301,31 @@ public class MovieProvider : BaseProvider, IRemoteMetadataProvider<Movie, MovieI
if (searchResults?.Any() != true)
{
Logger.Warn("Movie not found on AVBASE: {0}", m.Id);
return;
}
else if (searchResults.Count > 1)
var matched = false;
foreach (var result in searchResults)
{
// Ignore multiple results to avoid ambiguity.
Logger.Warn("Multiple movies found on AVBASE: {0}", m.Id);
var similarity = CalculateTitleSimilarity(m, result);
Logger.Info("Calculate movie title similarity for {0} and {1}: {2:P2}",
m.Id, result.Id, similarity);
if (similarity >= 0.8)
{
if (result.Actors?.Any() == true)
m.Actors = result.Actors;
matched = true;
break;
}
}
else
if (!matched)
{
var firstResult = searchResults.First();
if (firstResult.Actors?.Any() == true) m.Actors = firstResult.Actors;
Logger.Warn("No matching movie found on AVBASE for {0}", m.Id);
}
}
catch (Exception e)
@@ -318,6 +334,31 @@ public class MovieProvider : BaseProvider, IRemoteMetadataProvider<Movie, MovieI
}
}
private static double CalculateTitleSimilarity(MovieSearchResult source, MovieSearchResult target)
{
var sourceKey = Normalize(source.Number + source.Title);
var targetKey = Normalize(target.Number + target.Title);
if (string.IsNullOrWhiteSpace(sourceKey) || string.IsNullOrWhiteSpace(targetKey))
return 0.0;
var distance = Levenshtein.Distance(sourceKey, targetKey);
var avgLength = (sourceKey.Length + targetKey.Length) / 2.0;
var similarity = 1.0 - distance / avgLength;
return Math.Clamp(similarity, 0.0, 1.0);
string Normalize(string s)
{
if (string.IsNullOrWhiteSpace(s))
return string.Empty;
s = s.ToLowerInvariant();
s = Regex.Replace(s, @"[\s\[\]\(\)【】()]", "");
return s.Trim();
}
}
private async Task TranslateMovieInfo(Metadata.MovieInfo m, string language, CancellationToken cancellationToken)
{
try