using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Providers; using MediaBrowser.Controller.Resolvers; using System; using System.Collections.Generic; using System.Globalization; using System.IO; using System.Linq; using System.Text.RegularExpressions; namespace MediaBrowser.Controller.Library { /// /// Class TVUtils /// public static class TVUtils { /// /// The TVDB API key /// public static readonly string TvdbApiKey = "B89CE93890E9419B"; /// /// The banner URL /// public static readonly string BannerUrl = "http://www.thetvdb.com/banners/"; /// /// A season folder must contain one of these somewhere in the name /// private static readonly string[] SeasonFolderNames = { "season", "sæson", "temporada", "saison", "staffel", "series", "сезон" }; /// /// Used to detect paths that represent episodes, need to make sure they don't also /// match movie titles like "2001 A Space..." /// Currently we limit the numbers here to 2 digits to try and avoid this /// private static readonly Regex[] EpisodeExpressions = { new Regex( @".*(\\|\/)[sS]?(?\d{1,4})[xX](?\d{1,3})[^\\\/]*$", RegexOptions.Compiled), new Regex( @".*(\\|\/)[sS](?\d{1,4})[x,X]?[eE](?\d{1,3})[^\\\/]*$", RegexOptions.Compiled), new Regex( @".*(\\|\/)(?((?![sS]?\d{1,4}[xX]\d{1,3})[^\\\/])*)?([sS]?(?\d{1,4})[xX](?\d{1,3}))[^\\\/]*$", RegexOptions.Compiled), new Regex( @".*(\\|\/)(?[^\\\/]*)[sS](?\d{1,4})[xX\.]?[eE](?\d{1,3})[^\\\/]*$", RegexOptions.Compiled) }; private static readonly Regex[] MultipleEpisodeExpressions = { new Regex( @".*(\\|\/)[sS]?(?\d{1,4})[xX](?\d{1,3})((-| - )\d{1,4}[eExX](?\d{1,3}))+[^\\\/]*$", RegexOptions.Compiled), new Regex( @".*(\\|\/)[sS]?(?\d{1,4})[xX](?\d{1,3})((-| - )\d{1,4}[xX][eE](?\d{1,3}))+[^\\\/]*$", RegexOptions.Compiled), new Regex( @".*(\\|\/)[sS]?(?\d{1,4})[xX](?\d{1,3})((-| - )?[xXeE](?\d{1,3}))+[^\\\/]*$", RegexOptions.Compiled), new Regex( @".*(\\|\/)[sS]?(?\d{1,4})[xX](?\d{1,3})(-[xE]?[eE]?(?\d{1,3}))+[^\\\/]*$", RegexOptions.Compiled), new Regex( @".*(\\|\/)(?((?![sS]?\d{1,4}[xX]\d{1,3})[^\\\/])*)?([sS]?(?\d{1,4})[xX](?\d{1,3}))((-| - )\d{1,4}[xXeE](?\d{1,3}))+[^\\\/]*$", RegexOptions.Compiled), new Regex( @".*(\\|\/)(?((?![sS]?\d{1,4}[xX]\d{1,3})[^\\\/])*)?([sS]?(?\d{1,4})[xX](?\d{1,3}))((-| - )\d{1,4}[xX][eE](?\d{1,3}))+[^\\\/]*$", RegexOptions.Compiled), new Regex( @".*(\\|\/)(?((?![sS]?\d{1,4}[xX]\d{1,3})[^\\\/])*)?([sS]?(?\d{1,4})[xX](?\d{1,3}))((-| - )?[xXeE](?\d{1,3}))+[^\\\/]*$", RegexOptions.Compiled), new Regex( @".*(\\|\/)(?((?![sS]?\d{1,4}[xX]\d{1,3})[^\\\/])*)?([sS]?(?\d{1,4})[xX](?\d{1,3}))(-[xX]?[eE]?(?\d{1,3}))+[^\\\/]*$", RegexOptions.Compiled), new Regex( @".*(\\|\/)(?[^\\\/]*)[sS](?\d{1,4})[xX\.]?[eE](?\d{1,3})((-| - )?[xXeE](?\d{1,3}))+[^\\\/]*$", RegexOptions.Compiled), new Regex( @".*(\\|\/)(?[^\\\/]*)[sS](?\d{1,4})[xX\.]?[eE](?\d{1,3})(-[xX]?[eE]?(?\d{1,3}))+[^\\\/]*$", RegexOptions.Compiled) }; /// /// To avoid the following matching movies they are only valid when contained in a folder which has been matched as a being season, or the media type is TV series /// private static readonly Regex[] EpisodeExpressionsWithoutSeason = { new Regex( @".*[\\\/](?\d{1,3})(-(?\d{2,3}))*\.\w+$", RegexOptions.Compiled), // "01.avi" new Regex( @".*(\\|\/)(?\d{1,3})(-(?\d{2,3}))*\s?-\s?[^\\\/]*$", RegexOptions.Compiled), // "01 - blah.avi", "01-blah.avi" new Regex( @".*(\\|\/)(?\d{1,3})(-(?\d{2,3}))*\.[^\\\/]+$", RegexOptions.Compiled), // "01.blah.avi" new Regex( @".*[\\\/][^\\\/]* - (?\d{1,3})(-(?\d{2,3}))*[^\\\/]*$", RegexOptions.Compiled), // "blah - 01.avi", "blah 2 - 01.avi", "blah - 01 blah.avi", "blah 2 - 01 blah", "blah - 01 - blah.avi", "blah 2 - 01 - blah" }; /// /// Gets the season number from path. /// /// The path. /// System.Nullable{System.Int32}. public static int? GetSeasonNumberFromPath(string path) { var filename = Path.GetFileName(path); if (string.Equals(path, "specials", StringComparison.OrdinalIgnoreCase)) { return 0; } int val; if (int.TryParse(filename, NumberStyles.Integer, CultureInfo.InvariantCulture, out val)) { return val; } // Look for one of the season folder names foreach (var name in SeasonFolderNames) { var index = filename.IndexOf(name, StringComparison.OrdinalIgnoreCase); if (index != -1) { return GetSeasonNumberFromPathSubstring(filename.Substring(index + name.Length)); } } return null; } /// /// Extracts the season number from the second half of the Season folder name (everything after "Season", or "Staffel") /// /// The path. /// System.Nullable{System.Int32}. private static int? GetSeasonNumberFromPathSubstring(string path) { var numericStart = -1; var length = 0; // Find out where the numbers start, and then keep going until they end for (var i = 0; i < path.Length; i++) { if (char.IsNumber(path, i)) { if (numericStart == -1) { numericStart = i; } length++; } else if (numericStart != -1) { break; } } if (numericStart == -1) { return null; } return int.Parse(path.Substring(numericStart, length)); } /// /// Determines whether [is season folder] [the specified path]. /// /// The path. /// The directory service. /// true if [is season folder] [the specified path]; otherwise, false. private static bool IsSeasonFolder(string path, IDirectoryService directoryService) { // It's a season folder if it's named as such and does not contain any audio files, apart from theme.mp3 return GetSeasonNumberFromPath(path) != null && !directoryService.GetFiles(path).Any(i => EntityResolutionHelper.IsAudioFile(i.FullName) && !string.Equals(Path.GetFileNameWithoutExtension(i.FullName), BaseItem.ThemeSongFilename)); } /// /// Determines whether [is series folder] [the specified path]. /// /// The path. /// if set to true [consider seasonless series]. /// The file system children. /// The directory service. /// true if [is series folder] [the specified path]; otherwise, false. public static bool IsSeriesFolder(string path, bool considerSeasonlessSeries, IEnumerable fileSystemChildren, IDirectoryService directoryService) { // A folder with more than 3 non-season folders in will not becounted as a series var nonSeriesFolders = 0; foreach (var child in fileSystemChildren) { var attributes = child.Attributes; if ((attributes & FileAttributes.Hidden) == FileAttributes.Hidden) { continue; } if ((attributes & FileAttributes.System) == FileAttributes.System) { continue; } if ((attributes & FileAttributes.Directory) == FileAttributes.Directory) { if (IsSeasonFolder(child.FullName, directoryService)) { return true; } if (!EntityResolutionHelper.IgnoreFolders.Contains(child.Name, StringComparer.OrdinalIgnoreCase)) { nonSeriesFolders++; } if (nonSeriesFolders >= 3) { return false; } } else { var fullName = child.FullName; if (EntityResolutionHelper.IsVideoFile(fullName) || EntityResolutionHelper.IsVideoPlaceHolder(fullName)) { if (GetEpisodeNumberFromFile(fullName, considerSeasonlessSeries).HasValue) { return true; } } } } return false; } /// /// Episodes the number from file. /// /// The full path. /// if set to true [is in season]. /// System.String. public static int? GetEpisodeNumberFromFile(string fullPath, bool considerSeasonlessNames) { string fl = fullPath.ToLower(); foreach (var r in EpisodeExpressions) { Match m = r.Match(fl); if (m.Success) return ParseEpisodeNumber(m.Groups["epnumber"].Value); } if (considerSeasonlessNames) { var match = EpisodeExpressionsWithoutSeason.Select(r => r.Match(fl)) .FirstOrDefault(m => m.Success); if (match != null) { return ParseEpisodeNumber(match.Groups["epnumber"].Value); } } return null; } public static int? GetEndingEpisodeNumberFromFile(string fullPath) { var fl = fullPath.ToLower(); foreach (var r in MultipleEpisodeExpressions) { var m = r.Match(fl); if (m.Success && !string.IsNullOrEmpty(m.Groups["endingepnumber"].Value)) return ParseEpisodeNumber(m.Groups["endingepnumber"].Value); } foreach (var r in EpisodeExpressionsWithoutSeason) { var m = r.Match(fl); if (m.Success && !string.IsNullOrEmpty(m.Groups["endingepnumber"].Value)) return ParseEpisodeNumber(m.Groups["endingepnumber"].Value); } return null; } private static readonly CultureInfo UsCulture = new CultureInfo("en-US"); private static int? ParseEpisodeNumber(string val) { int num; if (!string.IsNullOrEmpty(val) && int.TryParse(val, NumberStyles.Integer, UsCulture, out num)) { return num; } return null; } /// /// Seasons the number from episode file. /// /// The full path. /// System.String. public static int? GetSeasonNumberFromEpisodeFile(string fullPath) { string fl = fullPath.ToLower(); foreach (var r in EpisodeExpressions) { Match m = r.Match(fl); if (m.Success) { Group g = m.Groups["seasonnumber"]; if (g != null) { var val = g.Value; if (!string.IsNullOrWhiteSpace(val)) { int num; if (int.TryParse(val, NumberStyles.Integer, UsCulture, out num)) { return num; } } } return null; } } return null; } public static string GetSeriesNameFromEpisodeFile(string fullPath) { var fl = fullPath.ToLower(); foreach (var r in EpisodeExpressions) { var m = r.Match(fl); if (m.Success) { var g = m.Groups["seriesname"]; if (g != null) { var val = g.Value; if (!string.IsNullOrWhiteSpace(val)) { return val; } } return null; } } return null; } /// /// Gets the air days. /// /// The day. /// List{DayOfWeek}. public static List GetAirDays(string day) { if (!string.IsNullOrWhiteSpace(day)) { if (day.Equals("Daily", StringComparison.OrdinalIgnoreCase)) { return new List { DayOfWeek.Sunday, DayOfWeek.Monday, DayOfWeek.Tuesday, DayOfWeek.Wednesday, DayOfWeek.Thursday, DayOfWeek.Friday, DayOfWeek.Saturday }; } DayOfWeek value; if (Enum.TryParse(day, true, out value)) { return new List { value }; } return new List(); } return null; } } }