public AutoTagSettings(string configPath) { this.configPath = configPath; if (File.Exists(configPath)) { try { config = JsonSerializer.Deserialize <AutoTagConfig>(File.ReadAllText(configPath)); } catch (JsonException) { Console.Error.WriteLine($"Error parsing config file '{configPath}'"); } catch (UnauthorizedAccessException) { Console.Error.WriteLine($"Config file '{configPath}' not readable"); } catch (Exception e) { Console.Error.WriteLine($"Error reading config file '{configPath}': {e.Message}"); } if (config.configVer != AutoTagConfig.currentVer) { if (config.configVer < 5 && config.tvRenamePattern == "%1 - %2x%3 - %4") { config.tvRenamePattern = "%1 - %2x%3:00 - %4"; } // if config file outdated, update it with new options config.configVer = AutoTagConfig.currentVer; Save(); } } else { Console.WriteLine($"Generating new config file with default options: '{configPath}'"); FileInfo configFile = new FileInfo(configPath); configFile.Directory.Create(); try { File.WriteAllText(configPath, JsonSerializer.Serialize <AutoTagConfig>(new AutoTagConfig(), jsonOptions)); } catch (UnauthorizedAccessException) { Console.Error.WriteLine($"Config file '{configPath}'not writeable"); } catch (Exception e) { Console.Error.WriteLine($"Error writing config file '{configPath}': {e.Message}"); } try { config = JsonSerializer.Deserialize <AutoTagConfig>(File.ReadAllText(configPath)); } catch (JsonException) { Console.Error.WriteLine($"Error parsing config file '{configPath}'"); } catch (UnauthorizedAccessException) { Console.Error.WriteLine($"Config file '{configPath}' not readable"); } catch (Exception e) { Console.Error.WriteLine($"Error reading config file '{configPath}': {e.Message}"); } } }
public static async Task <bool> Write(string filePath, FileMetadata metadata, Action <string> setPath, Action <string, MessageType> setStatus, AutoTagConfig config) { bool fileSuccess = true; if (config.tagFiles) { if (invalidFilenameChars == null) { invalidFilenameChars = Path.GetInvalidFileNameChars(); if (config.windowsSafe) { invalidFilenameChars = invalidFilenameChars.Union(invalidNtfsChars).ToArray(); } } TagLib.File file = null; try { file = TagLib.File.Create(filePath); file.Tag.Title = metadata.Title; file.Tag.Description = metadata.Overview; if (metadata.Genres != null && metadata.Genres.Any()) { file.Tag.Genres = metadata.Genres; } if (config.extendedTagging && file.MimeType == "video/x-matroska") { if (metadata.FileType == FileMetadata.Types.TV && metadata.Id.HasValue) { var custom = (TagLib.Matroska.Tag)file.GetTag(TagLib.TagTypes.Matroska); custom.Set("TMDB", "", $"tv/{metadata.Id}"); } file.Tag.Conductor = metadata.Director; file.Tag.Performers = metadata.Actors; file.Tag.PerformersRole = metadata.Characters; } if (metadata.FileType == FileMetadata.Types.TV) { file.Tag.Album = metadata.SeriesName; file.Tag.Disc = (uint)metadata.Season; file.Tag.Track = (uint)metadata.Episode; file.Tag.TrackCount = (uint)metadata.SeasonEpisodes; } else { file.Tag.Year = (uint)metadata.Date.Year; } if (!string.IsNullOrEmpty(metadata.CoverFilename) && config.addCoverArt == true) // if there is an image available and cover art is enabled { if (!_images.ContainsKey(metadata.CoverFilename)) { var response = await _client.GetAsync(metadata.CoverURL, HttpCompletionOption.ResponseHeadersRead); if (response.IsSuccessStatusCode) { _images[metadata.CoverFilename] = await response.Content.ReadAsByteArrayAsync(); } else { if (config.verbose) { setStatus($"Error: failed to download cover art ({(int) response.StatusCode}:{metadata.CoverURL})", MessageType.Error); } else { setStatus($"Error: failed to download cover art", MessageType.Error); } fileSuccess = false; } } if (_images.TryGetValue(metadata.CoverFilename, out byte[] imgBytes))
public async Task <bool> process( string filePath, Action <string> setPath, Action <string, MessageType> setStatus, Func <List <Tuple <string, string> >, int> selectResult, AutoTagConfig config ) { if (tvdb.Authentication.Token == null) { await tvdb.Authentication.AuthenticateAsync(apiKey); } FileMetadata result = new FileMetadata(FileMetadata.Types.TV); #region Filename parsing FileMetadata episodeData; if (string.IsNullOrEmpty(config.parsePattern)) { try { episodeData = EpisodeParser.ParseEpisodeInfo(Path.GetFileName(filePath)); // Parse info from filename } catch (FormatException ex) { setStatus($"Error: {ex.Message}", MessageType.Error); return(false); } } else { try { var match = Regex.Match(Path.GetFullPath(filePath), config.parsePattern, RegexOptions.IgnoreCase | RegexOptions.CultureInvariant); episodeData = new FileMetadata(FileMetadata.Types.TV); episodeData.SeriesName = match.Groups["SeriesName"].Value; episodeData.Season = int.Parse(match.Groups["Season"].Value); episodeData.Episode = int.Parse(match.Groups["Episode"].Value); } catch (FormatException ex) { if (config.verbose) { setStatus($"Error: Unable to parse required information from filename ({ex.GetType().Name}: {ex.Message})", MessageType.Error); } else { setStatus($"Error: Unable to parse required information from filename", MessageType.Error); } return(false); } } result.Season = episodeData.Season; result.Episode = episodeData.Episode; setStatus($"Parsed file as {episodeData}", MessageType.Information); #endregion #region TVDB API searching if (!seriesResultCache.ContainsKey(episodeData.SeriesName)) // if not already searched for series { TvDbResponse <SeriesSearchResult[]> seriesIdResponse; try { seriesIdResponse = await tvdb.Search.SearchSeriesByNameAsync(episodeData.SeriesName); } catch (TvDbServerException ex) { if (config.verbose) { setStatus($"Error: Cannot find series {episodeData.SeriesName} ({ex.GetType().Name}: {ex.Message})", MessageType.Error); } else { setStatus($"Error: Cannot find series {episodeData.SeriesName} on TheTVDB", MessageType.Error); } return(false); } // sort results by similarity to parsed series name List <SeriesSearchResult> seriesResults = seriesIdResponse.Data .OrderByDescending(seriesResult => SeriesNameSimilarity(episodeData.SeriesName, seriesResult.SeriesName)) .ToList(); if (config.manualMode && seriesResults.Count > 1) { int chosen = selectResult(seriesResults.Select( r => new Tuple <string, string>(r.SeriesName, r.FirstAired ?? "Unknown") ).ToList() ); // add only the chosen series to cache if in manual mode seriesResultCache.Add(episodeData.SeriesName, new List <SeriesSearchResult> { seriesResults[chosen] }); } else { seriesResultCache.Add(episodeData.SeriesName, seriesResults); } } // try searching for each series search result foreach (var series in seriesResultCache[episodeData.SeriesName]) { result.Id = series.Id; result.SeriesName = series.SeriesName; try { TvDbResponse <EpisodeRecord[]> episodeResponse = await tvdb.Series.GetEpisodesAsync(series.Id, 1, new EpisodeQuery { AiredSeason = episodeData.Season, AiredEpisode = episodeData.Episode } ); result.Title = episodeResponse.Data[0].EpisodeName; result.Overview = episodeResponse.Data[0].Overview; break; } catch (TvDbServerException ex) { if (series.Id == seriesResultCache[episodeData.SeriesName].Last().Id) { if (config.verbose) { setStatus($"Error: Cannot find {episodeData} ({ex.GetType().Name}: {ex.Message})", MessageType.Error); } else { setStatus($"Error: Cannot find {episodeData} on TheTVDB", MessageType.Error); } return(false); } } } setStatus($"Found {episodeData} ({result.Title}) on TheTVDB", MessageType.Information); TvDbResponse <TvDbSharper.Dto.Image[]> imagesResponse = null; if (config.addCoverArt) { try { imagesResponse = await tvdb.Series.GetImagesAsync(result.Id, new ImagesQuery { KeyType = KeyType.Season, SubKey = episodeData.Season.ToString() }); } catch (TvDbServerException) { try { // use a series image if a season-specific image is not available imagesResponse = await tvdb.Series.GetImagesAsync(result.Id, new ImagesQuery { KeyType = KeyType.Series }); } catch (TvDbServerException ex) { if (config.verbose) { setStatus($"Error: Failed to find episode cover ({ex.GetType().Name}: {ex.Message})", MessageType.Error); } else { setStatus("Error: Failed to find episode cover", MessageType.Error); } result.Complete = false; } } } string imageFilename = ""; if (imagesResponse != null) { imageFilename = imagesResponse.Data .OrderByDescending(img => img.RatingsInfo.Average) .First().FileName; // Find highest rated image } #endregion result.CoverURL = (String.IsNullOrEmpty(imageFilename)) ? null : $"https://artworks.thetvdb.com/banners/{imageFilename}"; result.CoverFilename = imageFilename.Split('/').Last(); bool taggingSuccess = FileWriter.write(filePath, result, setPath, setStatus, config); return(taggingSuccess && result.Success && result.Complete); }
public async Task <bool> process( string filePath, Action <string> setPath, Action <string, MessageType> setStatus, Func <List <Tuple <string, string> >, int> selectResult, AutoTagConfig config ) { FileMetadata result = new FileMetadata(FileMetadata.Types.Movie); #region "Filename parsing" string pattern = "^((?<Title>.+?)[\\. _-]?)" + // get title by reading from start to a field (whichever field comes first) "?(" + "([\\(]?(?<Year>(19|20)[0-9]{2})[\\)]?)|" + // year - extract for use in searching "([0-9]{3,4}(p|i))|" + // resolution (e.g. 1080p, 720i) "((?:PPV\\.)?[HPS]DTV|[. ](?:HD)?CAM[| ]|B[DR]Rip|[.| ](?:HD-?)?TS[.| ]|(?:PPV )?WEB-?DL(?: DVDRip)?|HDRip|DVDRip|CamRip|W[EB]Rip|BluRay|DvDScr|hdtv|REMUX|3D|Half-(OU|SBS)+|4K|NF|AMZN)|" + // rip type "(xvid|[hx]\\.?26[45]|AVC)|" + // video codec "(MP3|DD5\\.?1|Dual[\\- ]Audio|LiNE|DTS[-HD]+|AAC[.-]LC|AAC(?:\\.?2\\.0)?|AC3(?:\\.5\\.1)?|7\\.1|DDP5.1)|" + // audio codec "(REPACK|INTERNAL|PROPER)|" + // scene tags "\\.(mp4|m4v|mkv)$" + // file extensions ")"; Match match = Regex.Match(Path.GetFileName(filePath), pattern); string title, year; if (match.Success) { title = match.Groups["Title"].ToString(); year = match.Groups["Year"].ToString(); } else { setStatus("Error: Failed to parse required information from filename", MessageType.Error); return(false); } title = title.Replace('.', ' '); // change dots to spaces if (String.IsNullOrWhiteSpace(title)) { setStatus("Error: Failed to parse required information from filename", MessageType.Error); return(false); } setStatus($"Parsed file as {title}", MessageType.Information); #endregion #region "TMDB API Searching" SearchContainer <SearchMovie> searchResults; if (!String.IsNullOrWhiteSpace(year)) { searchResults = await tmdb.SearchMovieAsync(query : title, year : int.Parse(year)); // if year was parsed, use it to narrow down search further } else { searchResults = await tmdb.SearchMovieAsync(query : title); } int selected = 0; if (searchResults.Results.Count > 1 && (searchResults.Results[0].Title != title || config.manualMode)) { selected = selectResult( searchResults.Results .Select(m => new Tuple <string, string>( m.Title, m.ReleaseDate == null ? "Unknown" : m.ReleaseDate.Value.Year.ToString() )).ToList() ); } else if (searchResults.Results.Count == 0) { setStatus($"Error: failed to find title {title} on TheMovieDB", MessageType.Error); result.Success = false; return(false); } SearchMovie selectedResult = searchResults.Results[selected]; setStatus($"Found {selectedResult.Title} ({selectedResult.ReleaseDate.Value.Year}) on TheMovieDB", MessageType.Information); #endregion result.Title = selectedResult.Title; result.Overview = selectedResult.Overview; result.CoverURL = (String.IsNullOrEmpty(selectedResult.PosterPath)) ? null : $"https://image.tmdb.org/t/p/original{selectedResult.PosterPath}"; result.CoverFilename = selectedResult.PosterPath.Replace("/", ""); result.Date = selectedResult.ReleaseDate.Value; if (String.IsNullOrEmpty(result.CoverURL)) { setStatus("Error: failed to fetch movie cover", MessageType.Error); result.Complete = false; } bool taggingSuccess = FileWriter.write(filePath, result, setPath, setStatus, config); return(taggingSuccess && result.Success && result.Complete); }
public static bool write(string filePath, FileMetadata metadata, Action <string> setPath, Action <string, MessageType> setStatus, AutoTagConfig config) { bool fileSuccess = true; if (config.tagFiles) { if (invalidFilenameChars == null) { invalidFilenameChars = Path.GetInvalidFileNameChars(); if (config.windowsSafe) { invalidFilenameChars = invalidFilenameChars.Union(invalidNtfsChars).ToArray(); } } try { TagLib.File file = TagLib.File.Create(filePath); file.Tag.Title = metadata.Title; file.Tag.Comment = metadata.Overview; file.Tag.Genres = new string[] { (metadata.FileType == FileMetadata.Types.TV) ? "TVShows" : "Movie" }; if (metadata.FileType == FileMetadata.Types.TV) { file.Tag.Album = metadata.SeriesName; file.Tag.Disc = (uint)metadata.Season; file.Tag.Track = (uint)metadata.Episode; } else { file.Tag.Year = (uint)metadata.Date.Year; } if (metadata.CoverFilename != "" && config.addCoverArt == true) // if there is an image available and cover art is enabled { string downloadPath = Path.Combine(Path.GetTempPath(), "autotag"); string downloadFile = Path.Combine(downloadPath, metadata.CoverFilename); if (!File.Exists(downloadFile)) // only download file if it hasn't already been downloaded { if (!Directory.Exists(downloadPath)) { Directory.CreateDirectory(downloadPath); // create temp directory } try { using (WebClient client = new WebClient()) { client.DownloadFile(metadata.CoverURL, downloadFile); // download image } file.Tag.Pictures = new TagLib.Picture[] { new TagLib.Picture(downloadFile) { Filename = "cover.jpg" } }; } catch (WebException ex) { if (config.verbose) { setStatus($"Error: Failed to download cover art ({ex.GetType().Name}: {ex.Message})", MessageType.Error); } else { setStatus("Error: Failed to download cover art", MessageType.Error); } fileSuccess = false; } } else { // overwrite default file name - allows software such as Icaros to display cover art thumbnails - default isn't compliant with Matroska guidelines file.Tag.Pictures = new TagLib.Picture[] { new TagLib.Picture(downloadFile) { Filename = "cover.jpg" } }; } } else if (String.IsNullOrEmpty(metadata.CoverFilename) && config.addCoverArt == true) { fileSuccess = false; } file.Save(); if (fileSuccess == true) { setStatus($"Successfully tagged file as {metadata}", MessageType.Information); } } catch (Exception ex) { if (config.verbose) { setStatus($"Error: Failed to write tags to file ({ex.GetType().Name}: {ex.Message})", MessageType.Error); } else { setStatus("Error: Failed to write tags to file", MessageType.Error); } fileSuccess = false; } } if (config.renameFiles) { string newPath; if (config.mode == 0) { newPath = Path.Combine( Path.GetDirectoryName(filePath), EscapeFilename( String.Format( GetTVRenamePattern(config), metadata.SeriesName, metadata.Season, metadata.Episode.ToString("00"), metadata.Title ), Path.GetFileNameWithoutExtension(filePath), setStatus ) + Path.GetExtension(filePath) ); } else { newPath = Path.Combine( Path.GetDirectoryName(filePath), EscapeFilename( String.Format( GetMovieRenamePattern(config), metadata.Title, metadata.Date.Year ), Path.GetFileNameWithoutExtension(filePath), setStatus ) + Path.GetExtension(filePath) ); } if (filePath != newPath) { try { if (File.Exists(newPath)) { setStatus("Error: Could not rename - file already exists", MessageType.Error); fileSuccess = false; } else { File.Move(filePath, newPath); setPath(newPath); setStatus($"Successfully renamed file to '{Path.GetFileName(newPath)}'", MessageType.Information); } } catch (Exception ex) { if (config.verbose) { setStatus($"Error: Failed to rename file ({ex.GetType().Name}: {ex.Message})", MessageType.Error); } else { setStatus("Error: Failed to rename file", MessageType.Error); } fileSuccess = false; } } } return(fileSuccess); }
private static string GetMovieRenamePattern(AutoTagConfig config) // Get usable renaming pattern { return(config.movieRenamePattern.Replace("%1", "{0}").Replace("%2", "{1}")); }
private static string GetTVRenamePattern(AutoTagConfig config) // Get usable renaming pattern { return(config.tvRenamePattern.Replace("%1", "{0}").Replace("%2", "{1}").Replace("%3", "{2}").Replace("%4", "{3}")); }