diff --git a/FoliCon/Models/Data/ParsedTitle.cs b/FoliCon/Models/Data/ParsedTitle.cs new file mode 100644 index 00000000..ea521fba --- /dev/null +++ b/FoliCon/Models/Data/ParsedTitle.cs @@ -0,0 +1,5 @@ +using FoliCon.Models.Enums; + +namespace FoliCon.Models.Data; + +public record ParsedTitle(string Title, IdType IdType, string Id, int Year); \ No newline at end of file diff --git a/FoliCon/Models/Enums/IdType.cs b/FoliCon/Models/Enums/IdType.cs new file mode 100644 index 00000000..9fcf5443 --- /dev/null +++ b/FoliCon/Models/Enums/IdType.cs @@ -0,0 +1,9 @@ +namespace FoliCon.Models.Enums; + +public enum IdType +{ + None, + Tvdb, + Tmdb, + Imdb +} \ No newline at end of file diff --git a/FoliCon/Modules/IGDB/IgdbService.cs b/FoliCon/Modules/IGDB/IgdbService.cs index 9abe4c2f..37f5bcfa 100644 --- a/FoliCon/Modules/IGDB/IgdbService.cs +++ b/FoliCon/Modules/IGDB/IgdbService.cs @@ -1,4 +1,5 @@ -using FoliCon.Models.Constants; +using System.Text; +using FoliCon.Models.Constants; using FoliCon.Models.Data; using NLog; diff --git a/FoliCon/Modules/TMDB/TMDB.cs b/FoliCon/Modules/TMDB/TMDB.cs index 4739766d..f1ac5a0a 100644 --- a/FoliCon/Modules/TMDB/TMDB.cs +++ b/FoliCon/Modules/TMDB/TMDB.cs @@ -96,6 +96,11 @@ public Task SearchAsync(string query, string searchMode) return _tmdbService.SearchAsync(query, searchMode); } + public Task SearchAsync(ParsedTitle parsedTitle, string searchMode) + { + return _tmdbService.SearchWithParamsAsync(parsedTitle, searchMode); + } + /// /// Searches TMDB media by ID as per media Type. /// diff --git a/FoliCon/Modules/TMDB/TMDBService.cs b/FoliCon/Modules/TMDB/TMDBService.cs index 230c99a6..e06e6fcc 100644 --- a/FoliCon/Modules/TMDB/TMDBService.cs +++ b/FoliCon/Modules/TMDB/TMDBService.cs @@ -1,6 +1,10 @@ -using FoliCon.Models.Constants; +using System.Windows.Documents; +using FoliCon.Models.Constants; using FoliCon.Models.Data; +using FoliCon.Models.Enums; using NLog; +using TMDbLib.Objects.Find; +using Collection = TMDbLib.Objects.Collections.Collection; namespace FoliCon.Modules.TMDB; @@ -99,4 +103,191 @@ public async Task SearchAsync(string query, string searchMode) }; return response; } -} \ No newline at end of file + + public async Task SearchWithParamsAsync(ParsedTitle parsedTitle, string searchMode) + { + Logger.Debug("Searching for {ParsedTitle} in {SearchMode}", parsedTitle, searchMode); + object r = null; + var mediaType = ""; + var query = parsedTitle.Title; + if (searchMode == MediaTypes.Movie) + { + if (query.ToLower(CultureInfo.InvariantCulture).Contains("collection")) + { + r = parsedTitle.IdType switch + { + IdType.None => await _serviceClient.SearchCollectionAsync(query), + IdType.Tvdb => GetMovieSearchContainer( + await _serviceClient.FindAsync(FindExternalSource.TvDb, parsedTitle.Id)), + IdType.Tmdb => GetCollectionSearchContainer( + await _serviceClient.GetCollectionAsync(Convert.ToInt32(parsedTitle.Id))), + IdType.Imdb => GetMovieSearchContainer( + await _serviceClient.FindAsync(FindExternalSource.Imdb, parsedTitle.Id)), + _ => await _serviceClient.SearchCollectionAsync(query) + }; + + mediaType = MediaTypes.Collection; + } + else + { + r = parsedTitle.IdType switch + { + IdType.None => parsedTitle.Year != 0 + ? await _serviceClient.SearchMovieAsync(query: query, year: parsedTitle.Year) + : await _serviceClient.SearchMovieAsync(query), + IdType.Tvdb => GetMovieSearchContainer(await _serviceClient.FindAsync(FindExternalSource.TvDb, parsedTitle.Id)), + IdType.Tmdb => GetMovieSearchContainer(await _serviceClient.GetMovieAsync(Convert.ToInt32(parsedTitle.Id))), + IdType.Imdb => await _serviceClient.FindAsync(FindExternalSource.Imdb, parsedTitle.Id), + _ => parsedTitle.Year != 0 + ? await _serviceClient.SearchMovieAsync(query: query, year: parsedTitle.Year) + : await _serviceClient.SearchMovieAsync(query) + }; + + mediaType = MediaTypes.Movie; + } + } + else if (searchMode == MediaTypes.Tv) + { + r = parsedTitle.IdType switch + { + IdType.None => parsedTitle.Year != 0 + ? await _serviceClient.SearchTvShowAsync(query: query, firstAirDateYear: parsedTitle.Year) + : await _serviceClient.SearchTvShowAsync(query), + IdType.Tvdb => GetTvSearchContainer(await _serviceClient.FindAsync(FindExternalSource.TvDb, parsedTitle.Id)), + IdType.Tmdb => GetTvSearchContainer(await _serviceClient.GetTvShowAsync(Convert.ToInt32(parsedTitle.Id))), + IdType.Imdb => GetTvSearchContainer(await _serviceClient.FindAsync(FindExternalSource.Imdb, parsedTitle.Id)), + _ => parsedTitle.Year != 0 + ? await _serviceClient.SearchTvShowAsync(query: query, firstAirDateYear: parsedTitle.Year) + : await _serviceClient.SearchTvShowAsync(query) + }; + mediaType = MediaTypes.Tv; + } + else if (searchMode == MediaTypes.Mtv) + { + r = parsedTitle.IdType switch + { + IdType.None => parsedTitle.Year != 0 + ? await _serviceClient.SearchMultiAsync(query: query, year: parsedTitle.Year) + : await _serviceClient.SearchMultiAsync(query), + IdType.Tvdb => GetMultiSearchContainer(await _serviceClient.FindAsync(FindExternalSource.TvDb, parsedTitle.Id)), + IdType.Imdb => GetMultiSearchContainer(await _serviceClient.FindAsync(FindExternalSource.Imdb, parsedTitle.Id)), + _ => parsedTitle.Year != 0 + ? await _serviceClient.SearchMultiAsync(query: query, year: parsedTitle.Year) + : await _serviceClient.SearchMultiAsync(query) + }; + mediaType = MediaTypes.Mtv; + } + + var response = new ResultResponse + { + Result = r, + MediaType = mediaType + }; + return response; + } + + private static SearchContainer GetMovieSearchContainer(FindContainer findContainer) + { + return new SearchContainer + { + TotalResults = findContainer.MovieResults.Count, + Results = findContainer.MovieResults + }; + } + + private static SearchContainer GetMovieSearchContainer(Movie movie) + { + return new SearchContainer + { + TotalResults = 1, + Results = movie is null ? [] : [ConvertMovieToSearchMovie(movie)] + }; + } + + private static SearchContainer GetCollectionSearchContainer(Collection collection) + { + return new SearchContainer + { + TotalResults = 1, + Results = collection is null ? [] : [ConvertCollectionToSearchCollection(collection)] + }; + } + + private static SearchContainer GetTvSearchContainer(FindContainer findContainer) + { + return new SearchContainer + { + TotalResults = findContainer.TvResults.Count, + Results = findContainer.TvResults + }; + } + + private static SearchContainer GetTvSearchContainer(TvShow tvShow) + { + var rd = tvShow; + return new SearchContainer + { + TotalResults = 1, + Results = tvShow is null ? [] : [ConvertTvShowToSearchTv(tvShow)] + }; + } + + private static SearchContainer GetMultiSearchContainer(FindContainer findContainer) + { + dynamic result; + if (findContainer.MovieResults.Count != 0) + { + result = findContainer.MovieResults; + } + else if (findContainer.TvResults.Count != 0) + { + result = findContainer.TvResults; + } + else + { + result = new List(); + } + return new SearchContainer + { + TotalResults = findContainer.MovieResults.Count + findContainer.TvResults.Count, + Results = result + }; + } + + private static SearchTv ConvertTvShowToSearchTv(TvShow tvShow) + { + return new SearchTv + { + Id = tvShow.Id, + Name = tvShow.Name, + FirstAirDate = tvShow.FirstAirDate, + Overview = tvShow.Overview, + PosterPath = tvShow.PosterPath, + VoteAverage = tvShow.VoteAverage + }; + } + + private static SearchMovie ConvertMovieToSearchMovie(Movie movie) + { + return new SearchMovie + { + Id = movie.Id, + Title = movie.Title, + ReleaseDate = movie.ReleaseDate, + Overview = movie.Overview, + PosterPath = movie.PosterPath, + VoteAverage = movie.VoteAverage + }; + } + + private static SearchCollection ConvertCollectionToSearchCollection(Collection collection) + { + return new SearchCollection + { + Id = collection.Id, + Name = collection.Name, + PosterPath = collection.PosterPath, + BackdropPath = collection.BackdropPath + }; + } + } \ No newline at end of file diff --git a/FoliCon/Modules/utils/DataUtils.cs b/FoliCon/Modules/utils/DataUtils.cs index c9401f5f..4e8d5861 100644 --- a/FoliCon/Modules/utils/DataUtils.cs +++ b/FoliCon/Modules/utils/DataUtils.cs @@ -1,4 +1,6 @@ -using NLog; +using FoliCon.Models.Data; +using FoliCon.Models.Enums; +using NLog; namespace FoliCon.Modules.utils; @@ -26,4 +28,9 @@ public static string FormatRating(double ratingInput) Logger.Debug("End FormatRatingString() - Formatted Rating : {FormattedRatingValue}", formattedRatingValue); return formattedRatingValue; } + + public static bool ShouldUseParsedTitle(ParsedTitle parsedTitle) + { + return parsedTitle != null && (parsedTitle.Year != 0 || (parsedTitle.IdType != IdType.None && parsedTitle.Id != "0")); + } } diff --git a/FoliCon/Modules/utils/TitleCleaner.cs b/FoliCon/Modules/utils/TitleCleaner.cs index 9ca7a436..9f482650 100644 --- a/FoliCon/Modules/utils/TitleCleaner.cs +++ b/FoliCon/Modules/utils/TitleCleaner.cs @@ -1,4 +1,6 @@ -using NLog; +using FoliCon.Models.Data; +using FoliCon.Models.Enums; +using NLog; using Logger = NLog.Logger; namespace FoliCon.Modules.utils; @@ -21,6 +23,13 @@ public static string Clean(string title) return cleanTitle; } + public static ParsedTitle CleanAndParse(string title) + { + var (normalizedTitle, idType, showId, year) = ExtractShowIdAndYear(title); + normalizedTitle = Clean(normalizedTitle); + return new ParsedTitle(normalizedTitle, idType, showId, year); + } + private static string NormalizeTitle(string title) { return title.Replace('-', ' ').Replace('_', ' ').Replace('.', ' '); @@ -28,19 +37,19 @@ private static string NormalizeTitle(string title) private static string CleanTitle(string title) { - // \s* --Remove any whitespace which would be left at the end after this substitution - // \(? --Remove optional bracket starting (720p) - // (\d{4}) --Remove year from movie - // (420)|(720)|(1080) resolutions - // (year|resolutions) find at least one main token to remove - // p?i? \)? --Not needed. To emphasize removal of 1080i, closing bracket etc, but not needed due to the last part - // .* --Remove all trailing information after having found year or resolution as junk usually follows + /*\s* --Remove any whitespace which would be left at the end after this substitution + \(? --Remove optional bracket starting (720p) + (\d{4}) --Remove year from movie + (420)|(720)|(1080) resolutions + (year|resolutions) find at least one main token to remove + p?i? \)? --Not needed. To emphasize removal of 1080i, closing bracket etc, but not needed due to the last part + .* --Remove all trailing information after having found year or resolution as junk usually follows*/ var cleanTitle = Regex.Replace(title, "\\s*\\(?((\\d{4})|(420)|(720)|(1080))p?i?\\)?.*", "", RegexOptions.IgnoreCase | RegexOptions.Compiled); cleanTitle = Regex.Replace(cleanTitle, @"\[.*\]", "", RegexOptions.IgnoreCase | RegexOptions.Compiled); cleanTitle = Regex.Replace(cleanTitle, " {2,}", " ", RegexOptions.IgnoreCase | RegexOptions.Compiled); - return string.IsNullOrWhiteSpace(cleanTitle) ? title : cleanTitle; + return string.IsNullOrWhiteSpace(cleanTitle) ? title.Trim() : cleanTitle.Trim(); } private static string RemoveReplaceUnicodeCharacters(string title) @@ -50,4 +59,29 @@ private static string RemoveReplaceUnicodeCharacters(string title) // Remove other remaining unicode characters return Regex.Replace(title, @"[^\u0000-\u007F]+", string.Empty); } + + private static (string, IdType, string, int) ExtractShowIdAndYear(string title) + { + const string showIdPattern = @"\{(tvdb|tmdb)-(\d+)\}"; + const string yearPattern = @"\((\d{4})\)"; + + var showIdMatch = Regex.Match(title, showIdPattern, RegexOptions.IgnoreCase | RegexOptions.Compiled); + var yearMatch = Regex.Match(title, yearPattern, RegexOptions.IgnoreCase | RegexOptions.Compiled); + + var showIdType = IdType.None; + var showId = "0"; + + if (showIdMatch.Success) + { + showIdType = Enum.TryParse(showIdMatch.Groups[1].Value, true, out IdType parsedShowIdType) ? parsedShowIdType : IdType.None; + showId = showIdMatch.Groups[2].Value; + title = Regex.Replace(title, showIdPattern, "", RegexOptions.IgnoreCase | RegexOptions.Compiled); + } + + if (!yearMatch.Success) return (title, showIdType, showId, 0); + var year = Convert.ToInt32(yearMatch.Groups[1].Value); + title = Regex.Replace(title, yearPattern, "", RegexOptions.IgnoreCase | RegexOptions.Compiled); + + return (title, showIdType, showId, year); + } } \ No newline at end of file diff --git a/FoliCon/ViewModels/MainWindowViewModel.cs b/FoliCon/ViewModels/MainWindowViewModel.cs index 69283c31..a8903848 100644 --- a/FoliCon/ViewModels/MainWindowViewModel.cs +++ b/FoliCon/ViewModels/MainWindowViewModel.cs @@ -402,7 +402,7 @@ private async Task ProcessPosterModeAsync() StatusBarProperties.AppStatusAdditional = itemTitle; // TODO: Set cursor to WAIT. var isAutoPicked = false; - var searchTitle = TitleCleaner.Clean(itemTitle); + var parsedTitle = TitleCleaner.CleanAndParse(itemTitle); var (id, mediaType) = FileUtils.ReadMediaInfo(fullFolderPath); var isPickedById = false; ResultResponse response; @@ -416,8 +416,10 @@ private async Task ProcessPosterModeAsync() { Logger.Info("MediaInfo not found for {ItemTitle}, Searching by Title", itemTitle); response = SearchMode == "Game" - ? await _igdbObject.SearchGameAsync(searchTitle) - : await _tmdbObject.SearchAsync(searchTitle, SearchMode); + ? await _igdbObject.SearchGameAsync(parsedTitle.Title) + : DataUtils.ShouldUseParsedTitle(parsedTitle) + ? await _tmdbObject.SearchAsync(parsedTitle, SearchMode) + : await _tmdbObject.SearchAsync(parsedTitle.Title, SearchMode); } int resultCount = isPickedById ? response.Result != null ? 1 : 0 : SearchMode == "Game" ? response.Result.Length : response.Result.TotalResults; Logger.Info("Search Result Count: {ResultCount}", resultCount); @@ -427,7 +429,7 @@ private async Task ProcessPosterModeAsync() Logger.Debug("No result found for {ItemTitle}, {Mode}", itemTitle, SearchMode); MessageBox.Show(CustomMessageBox.Info(LangProvider.GetLang("NothingFoundFor").Format(itemTitle), LangProvider.GetLang("NoResultFound"))); - _dialogService.ShowSearchResult(SearchMode, searchTitle, fullFolderPath, response, + _dialogService.ShowSearchResult(SearchMode, parsedTitle.Title, fullFolderPath, response, _tmdbObject, _igdbObject, isPickedById, r => { @@ -488,7 +490,7 @@ private async Task ProcessPosterModeAsync() "always show poster window: {IsPosterWindowShown}, Skip ambigous titles: {IsSkipAmbiguous}," + " showing poster window", itemTitle, SearchMode, IsPosterWindowShown, IsSkipAmbiguous); - _dialogService.ShowSearchResult(SearchMode, searchTitle, fullFolderPath, + _dialogService.ShowSearchResult(SearchMode, parsedTitle.Title, fullFolderPath, response, _tmdbObject, _igdbObject, isPickedById, r => {