From bf509b7bf0e6bc29ce1a2c37fea7dc89f4a552ac Mon Sep 17 00:00:00 2001 From: Umut Ozel Date: Mon, 20 Apr 2015 01:05:10 +0300 Subject: [PATCH] Major Improvement: - File rename settings added. After movie or tv show episode downloaded (or subtitle downloaded when enabled) files (and subtitles if enabled) will be renamed based on the given template (from settings). - Safe file naming added (replacing invalid chars) --- Novaroma.Engine/EngineSettings.cs | 24 +++++ Novaroma.Engine/NovaromaEngine.cs | 85 ++++++++++++++---- .../SearchMedia/InfoSearchMediaViewModel.cs | 4 +- Novaroma/Helper.cs | 89 +++++++++++++++++-- Novaroma/Properties/Resources.Designer.cs | 36 ++++++++ Novaroma/Properties/Resources.resx | 12 +++ Novaroma/Properties/Resources.tr.resx | 12 +++ 7 files changed, 238 insertions(+), 24 deletions(-) diff --git a/Novaroma.Engine/EngineSettings.cs b/Novaroma.Engine/EngineSettings.cs index 2f2c57b..0e83011 100644 --- a/Novaroma.Engine/EngineSettings.cs +++ b/Novaroma.Engine/EngineSettings.cs @@ -18,7 +18,9 @@ public class EngineSettings : ModelBase { private readonly SettingSingleSelection> _languageSelection; private readonly DirectorySelection _movieDirectory; private readonly DirectorySelection _tvShowDirectory; + private string _movieFileNameTemplate; private string _tvShowSeasonDirectoryTemplate; + private string _tvShowEpisodeFileNameTemplate; private readonly SettingMultiSelection> _subtitleLanguages; private bool _deleteDirectoriesAlso; private readonly SettingSingleSelection _infoProvider; @@ -92,6 +94,28 @@ public string TvShowSeasonDirectoryTemplate { } } + [Display(Name = "MovieFileNameTemplate", Description = "MovieFileNameTemplateDescription", GroupName = "Main", ResourceType = typeof(Resources))] + public string MovieFileNameTemplate { + get { return _movieFileNameTemplate; } + set { + if (_movieFileNameTemplate == value) return; + + _movieFileNameTemplate = value; + RaisePropertyChanged("MovieFileNameTemplate"); + } + } + + [Display(Name = "TvShowEpisodeFileNameTemplate", Description = "TvShowEpisodeFileNameTemplateDescription", GroupName = "Main", ResourceType = typeof(Resources))] + public string TvShowEpisodeFileNameTemplate { + get { return _tvShowEpisodeFileNameTemplate; } + set { + if (_tvShowEpisodeFileNameTemplate == value) return; + + _tvShowEpisodeFileNameTemplate = value; + RaisePropertyChanged("TvShowEpisodeFileNameTemplate"); + } + } + [Display(Name = "SubtitleLanguages", GroupName = "Main", ResourceType = typeof(Resources))] public SettingMultiSelection> SubtitleLanguages { get { return _subtitleLanguages; } diff --git a/Novaroma.Engine/NovaromaEngine.cs b/Novaroma.Engine/NovaromaEngine.cs index b09b33b..6472677 100644 --- a/Novaroma.Engine/NovaromaEngine.cs +++ b/Novaroma.Engine/NovaromaEngine.cs @@ -181,9 +181,14 @@ private async void DirectoryWatcherOnDeleted(object sender, FileSystemEventArgs var tvShow = media as TvShow; var movie = media as Movie; if (tvShow != null) - tvShow.Seasons.ToList().ForEach(s => s.Episodes.ToList().ForEach(e => e.FilePath = string.Empty)); - else if (movie != null) + tvShow.Seasons.ToList().ForEach(s => s.Episodes.ToList().ForEach(e => { + e.FilePath = string.Empty; + e.SubtitleDownloaded = false; + })); + else if (movie != null) { movie.FilePath = string.Empty; + movie.SubtitleDownloaded = false; + } context.Update(media); await context.SaveChanges(); @@ -198,6 +203,7 @@ private async void DirectoryWatcherOnDeleted(object sender, FileSystemEventArgs var movie = context.Movies.FirstOrDefault(m => string.Equals(m.FilePath, args.FullPath, StringComparison.OrdinalIgnoreCase)); if (movie != null) { movie.FilePath = string.Empty; + movie.SubtitleDownloaded = false; context.Update(movie); await context.SaveChanges(); @@ -207,6 +213,7 @@ private async void DirectoryWatcherOnDeleted(object sender, FileSystemEventArgs var tvShowEpisode = context.TvShows.Episodes().FirstOrDefault(e => string.Equals(e.FilePath, args.FullPath, StringComparison.OrdinalIgnoreCase)); if (tvShowEpisode != null) { tvShowEpisode.FilePath = string.Empty; + tvShowEpisode.SubtitleDownloaded = false; context.Update(tvShowEpisode.TvShowSeason.TvShow); await context.SaveChanges(); @@ -276,11 +283,16 @@ private void DownloaderOnDownloadCompleted(object sender, DownloadCompletedEvent episode.FilePath = Directory.GetFiles(directory, fileName).FirstOrDefault(); } episode.DownloadKey = string.Empty; + try { + if (!episode.BackgroundSubtitleDownload) + Helper.RenameEpisodeFile(episode, Settings.TvShowEpisodeFileNameTemplate); + OnTvShowEpisodeDownloadCompleted(episode); } - // ReSharper disable once EmptyGeneralCatchClause - catch { } + catch (Exception ex) { + _exceptionHandler.HandleException(ex); + } var season = episode.TvShowSeason; var show = season.TvShow; @@ -311,11 +323,16 @@ private void DownloaderOnDownloadCompleted(object sender, DownloadCompletedEvent movie.FilePath = Directory.GetFiles(movie.Directory, fileName).FirstOrDefault(); } movie.DownloadKey = string.Empty; + try { + if (!movie.BackgroundSubtitleDownload) + Helper.RenameMovieFile(movie, Settings.MovieFileNameTemplate); + OnMovieDownloadCompleted(movie); } - // ReSharper disable once EmptyGeneralCatchClause - catch { } + catch (Exception ex) { + _exceptionHandler.HandleException(ex); + } var activity = CreateActivity(string.Format(Resources.MovieDownloaded, movie.Title), movie.FilePath); context.Insert(activity); @@ -337,6 +354,30 @@ private void DownloaderOnDownloadCompleted(object sender, DownloadCompletedEvent } } + private void MovieSubtitleDownloaded(Movie movie) { + Helper.SetSubtitleDownloadProperties(true, movie); + + try { + Helper.RenameMovieFile(movie, Settings.MovieFileNameTemplate); + OnMovieSubtitleDownloadCompleted(movie); + } + catch (Exception ex) { + _exceptionHandler.HandleException(ex); + } + } + + private void EpisodeSubtitleDownloaded(TvShowEpisode episode) { + Helper.SetSubtitleDownloadProperties(true, episode); + + try { + Helper.RenameEpisodeFile(episode, Settings.TvShowEpisodeFileNameTemplate); + OnTvShowEpisodeSubtitleDownloadCompleted(episode); + } + catch (Exception ex) { + _exceptionHandler.HandleException(ex); + } + } + private static QueryResult FilterMediaQuery(IQueryable query, MediaSearchModel searchModel) where TMedia : Media { if (!string.IsNullOrWhiteSpace(searchModel.Query)) query = query.Where(x => x.Title.StartsWith(searchModel.Query, StringComparison.OrdinalIgnoreCase) @@ -462,7 +503,8 @@ private void SetMovieDirectory(Movie movie) { if (string.IsNullOrEmpty(movieDirectory)) movieDirectory = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyVideos), Resources.Movies); - movie.Directory = Path.Combine(movieDirectory, movie.Title); + var title = Helper.MakeValidFileName(movie.Title); + movie.Directory = Path.Combine(movieDirectory, title); } private void SetTvShowDirectory(TvShow tvShow) { @@ -470,7 +512,8 @@ private void SetTvShowDirectory(TvShow tvShow) { if (string.IsNullOrEmpty(tvShowDirectory)) tvShowDirectory = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyVideos), Resources.TvShows); - tvShow.Directory = Path.Combine(tvShowDirectory, tvShow.Title); + var title = Helper.MakeValidFileName(tvShow.Title); + tvShow.Directory = Path.Combine(tvShowDirectory, title); } private async Task ValidateSubtitleDownload(string filePath) { @@ -1140,8 +1183,7 @@ public async Task DownloadSubtitleForMovie(Movie movie) { var result = await subtitleDownloader.DownloadForMovie(movie.OriginalTitle, movie.FilePath, languages, movie.ImdbId); if (!result) continue; - Helper.SetSubtitleDownloadProperties(true, movie); - OnMovieSubtitleDownloadCompleted(movie); + MovieSubtitleDownloaded(movie); var activity = CreateActivity(string.Format(Resources.MovieSubtitleDownloaded, movie.Title), movie.FilePath); await SaveChanges(new[] { activity }, new[] { movie }); @@ -1173,8 +1215,7 @@ public async Task DownloadSubtitleForTvShowEpisode(TvShowEpisode episode) var result = await subtitleDownloader.DownloadForTvShowEpisode(show.OriginalTitle, season.Season, episode.Episode, episode.FilePath, languages, show.ImdbId); if (!result) continue; - Helper.SetSubtitleDownloadProperties(true, episode); - OnTvShowEpisodeSubtitleDownloadCompleted(episode); + EpisodeSubtitleDownloaded(episode); var description = string.Format(Resources.TvShowEpisodeSubtitleDownloaded, show.Title, season.Season, episode.Episode); var activity = CreateActivity(description, episode.FilePath); @@ -1323,20 +1364,20 @@ public async Task DownloadSubtitle(string filePath, ISubtitleSearchResult if (result) { var movie = downloadable as Movie; if (movie != null) { + MovieSubtitleDownloaded(movie); + var activity = CreateActivity(string.Format(Resources.MovieSubtitleDownloaded, movie.Title), movie.FilePath); await InsertEntity(activity); - - OnMovieSubtitleDownloadCompleted(movie); } else { var episode = downloadable as TvShowEpisode; if (episode != null) { + EpisodeSubtitleDownloaded(episode); + var season = episode.TvShowSeason; var show = episode.TvShowSeason.TvShow; var activity = CreateActivity(string.Format(Resources.TvShowEpisodeSubtitleDownloaded, show.Title, season.Season, episode.Episode), episode.FilePath); await InsertEntity(activity); - - OnTvShowEpisodeSubtitleDownloadCompleted(episode); } } } @@ -1525,7 +1566,9 @@ string IConfigurable.SerializeSettings() { Language = (int)Settings.LanguageSelection.SelectedItem.Item, MovieDirectory = Settings.MovieDirectory.Path, TvShowDirectory = Settings.TvShowDirectory.Path, + Settings.MovieFileNameTemplate, Settings.TvShowSeasonDirectoryTemplate, + Settings.TvShowEpisodeFileNameTemplate, Settings.MakeSpecialFolder, Settings.DownloadInterval, Settings.SubtitleDownloadInterval, @@ -1556,6 +1599,15 @@ void IConfigurable.DeserializeSettings(string settings) { Settings.MovieDirectory.Path = (string)o["MovieDirectory"]; Settings.TvShowDirectory.Path = (string)o["TvShowDirectory"]; Settings.TvShowSeasonDirectoryTemplate = (string)o["TvShowSeasonDirectoryTemplate"]; + + var movieFileNameTemplate = o["MovieFileNameTemplate"]; + if (movieFileNameTemplate != null) + Settings.MovieFileNameTemplate = movieFileNameTemplate.ToString(); + + var tvShowEpisodeFileNameTemplate = o["TvShowEpisodeFileNameTemplate"]; + if (tvShowEpisodeFileNameTemplate != null) + Settings.TvShowEpisodeFileNameTemplate = tvShowEpisodeFileNameTemplate.ToString(); + var makeSpecialFolder = o["MakeSpecialFolder"]; if (makeSpecialFolder != null) Settings.MakeSpecialFolder = (bool)makeSpecialFolder; @@ -1573,7 +1625,6 @@ void IConfigurable.DeserializeSettings(string settings) { if (deleteExtensions != null) Settings.DeleteExtensions = (string)deleteExtensions; - var subtitleLanguages = (JArray)o["SubtitleLanguages"]; if (subtitleLanguages != null) Settings.SubtitleLanguages.SelectedItemNames = subtitleLanguages.Select(x => x.ToString()); diff --git a/Novaroma.Win/ViewModels/SearchMedia/InfoSearchMediaViewModel.cs b/Novaroma.Win/ViewModels/SearchMedia/InfoSearchMediaViewModel.cs index 7ea1709..2e177b7 100644 --- a/Novaroma.Win/ViewModels/SearchMedia/InfoSearchMediaViewModel.cs +++ b/Novaroma.Win/ViewModels/SearchMedia/InfoSearchMediaViewModel.cs @@ -38,8 +38,8 @@ public async Task InitFromImdbId(string imdbId) { } private Media InitMedia(Media media) { - if (!string.IsNullOrEmpty(_directory)) - media.Directory = _isParentDirectory ? Path.Combine(_directory, media.Title) : _directory; + if (!string.IsNullOrEmpty(_directory) && !_isParentDirectory) + media.Directory = _directory; var tvShow = media as TvShow; if (tvShow != null) diff --git a/Novaroma/Helper.cs b/Novaroma/Helper.cs index 35439a0..f4004d5 100644 --- a/Novaroma/Helper.cs +++ b/Novaroma/Helper.cs @@ -26,7 +26,7 @@ namespace Novaroma { public static class Helper { public static string[] VideoExtensions = { ".avi", ".mkv", ".mp4", ".m4p", ".m4v", ".mpg", ".mp2", ".mpeg", ".mpe", ".mpv", ".m2v", ".wmv" }; public static string[] SubtitleExtensions = { ".srt", ".sub" }; - private static readonly IEnumerable _paranthesis = new List {'(', '[', '{'}; + private static readonly IEnumerable _paranthesis = new List { '(', '[', '{' }; public static void SetCulture(Language language) { var cultureCode = GetTwoLetterLanguageCode(language); @@ -329,6 +329,85 @@ public static void SetSubtitleDownloadProperties(bool? isDownloaded, IDownloadab downloadable.SubtitleNotFound = true; } + public static void RenameMovieFile(Movie movie, string template) { + if (string.IsNullOrEmpty(template) || string.IsNullOrEmpty(movie.FilePath)) return; + var fileInfo = new FileInfo(movie.FilePath); + if (!fileInfo.Exists || fileInfo.DirectoryName == null) return; + + template = Regex.Replace(template, "%movieName%", movie.Title, RegexOptions.IgnoreCase); + template = Regex.Replace(template, "%year%", movie.Year.ToString(), RegexOptions.IgnoreCase); + template = Regex.Replace(template, "%rating%", movie.Rating.ToString(), RegexOptions.IgnoreCase); + template = Regex.Replace(template, "%genres%", string.Join(", ", movie.Genres.Select(g => g.Name)), RegexOptions.IgnoreCase); + template = Regex.Replace(template, "%voteCount%", movie.VoteCount.ToString(), RegexOptions.IgnoreCase); + template = Regex.Replace(template, "%runtime%", movie.Runtime.ToString(), RegexOptions.IgnoreCase); + var videQuality = movie.VideoQuality; + if (videQuality != VideoQuality.Any) { + var videoQualityStr = videQuality == VideoQuality.P720 ? Resources.P720 : Resources.P1080; + template = Regex.Replace(template, "%videoQuality%", videoQualityStr, RegexOptions.IgnoreCase); + } + + movie.FilePath = RenameVideoFile(fileInfo, template); + } + + public static void RenameEpisodeFile(TvShowEpisode episode, string template) { + if (string.IsNullOrEmpty(template) || string.IsNullOrEmpty(episode.FilePath)) return; + var fileInfo = new FileInfo(episode.FilePath); + if (!fileInfo.Exists) return; + + var season = episode.TvShowSeason; + var tvShow = season.TvShow; + template = Regex.Replace(template, "%showName%", tvShow.Title, RegexOptions.IgnoreCase); + template = Regex.Replace(template, "%season%", season.Season.ToString("D2"), RegexOptions.IgnoreCase); + template = Regex.Replace(template, "%episode%", episode.Episode.ToString("D2"), RegexOptions.IgnoreCase); + template = Regex.Replace(template, "%episodeName%", episode.Name, RegexOptions.IgnoreCase); + template = Regex.Replace(template, "%year%", episode.AirDate.HasValue ? episode.AirDate.Value.Year.ToString(CultureInfo.InvariantCulture) : string.Empty, RegexOptions.IgnoreCase); + template = Regex.Replace(template, "%rating%", tvShow.Rating.ToString(), RegexOptions.IgnoreCase); + template = Regex.Replace(template, "%genres%", string.Join(", ", tvShow.Genres.Select(g => g.Name)), RegexOptions.IgnoreCase); + template = Regex.Replace(template, "%voteCount%", tvShow.VoteCount.ToString(), RegexOptions.IgnoreCase); + template = Regex.Replace(template, "%runtime%", tvShow.Runtime.ToString(), RegexOptions.IgnoreCase); + var videQuality = tvShow.VideoQuality; + if (videQuality != VideoQuality.Any) { + var videoQualityStr = videQuality == VideoQuality.P720 ? Resources.P720 : Resources.P1080; + template = Regex.Replace(template, "%videoQuality%", videoQualityStr, RegexOptions.IgnoreCase); + } + + episode.FilePath = RenameVideoFile(fileInfo, template); + } + + private static string RenameVideoFile(FileInfo videoFileInfo, string template) { + if (videoFileInfo.DirectoryName == null) return videoFileInfo.FullName; + + template = MakeValidFileName(template); + + RenameSubtitleFile(videoFileInfo, template); + + var newFilePath = Path.Combine(videoFileInfo.DirectoryName, template) + videoFileInfo.Extension; + if (File.Exists(newFilePath)) + File.Delete(newFilePath); + videoFileInfo.MoveTo(newFilePath); + return newFilePath; + } + + private static void RenameSubtitleFile(FileInfo videoFileInfo, string template) { + if (videoFileInfo.DirectoryName == null) return; + + var subtitleFilePath = GetSubtitleFilePath(videoFileInfo); + if (string.IsNullOrEmpty(subtitleFilePath)) return; + + var subtitleFileInfo = new FileInfo(subtitleFilePath); + if (!subtitleFileInfo.Exists) return; + + var newSubtitleFilePath = Path.Combine(videoFileInfo.DirectoryName, template) + subtitleFileInfo.Extension; + if (File.Exists(newSubtitleFilePath)) + File.Delete(newSubtitleFilePath); + subtitleFileInfo.MoveTo(newSubtitleFilePath); + } + + public static string MakeValidFileName(string path, char replaceChar = '-') { + var invalidChars = Path.GetInvalidFileNameChars(); + return new string(path.Select(c => invalidChars.Contains(c) ? replaceChar : c).ToArray()); + } + public static IEnumerable GetEnumInfo(Type enumType) { var method = typeof(Helper).GetMethod("GetEnumInfo", new Type[] { }); return method.MakeGenericMethod(enumType).Invoke(null, null) as IEnumerable; @@ -347,8 +426,8 @@ public static IEnumerable> GetEnumInfo() { return infos; } - public static TAttribute GetMemberAttribute(string memberName, bool checkMetadataType = false, bool inherit = false) where TAttribute: Attribute { - return GetMemberAttribute(typeof (TType), memberName, checkMetadataType, inherit); + public static TAttribute GetMemberAttribute(string memberName, bool checkMetadataType = false, bool inherit = false) where TAttribute : Attribute { + return GetMemberAttribute(typeof(TType), memberName, checkMetadataType, inherit); } public static TAttribute GetMemberAttribute(Type type, string memberName, bool checkMetadataType = false, bool inherit = false) where TAttribute : Attribute { @@ -357,7 +436,7 @@ public static TAttribute GetMemberAttribute(Type type, string member } public static string GetTwoLetterLanguageCode(Language language) { - var langInfo = GetMemberAttribute(typeof (Language), language.ToString()); + var langInfo = GetMemberAttribute(typeof(Language), language.ToString()); return langInfo.TwoLetterCode; } @@ -527,7 +606,7 @@ public async static Task RunTask(Func> taskGette return result; } catch (Exception ex) { - if (exceptionHandler == null) + if (exceptionHandler == null) throw; // ReSharper disable ExplicitCallerInfoArgument diff --git a/Novaroma/Properties/Resources.Designer.cs b/Novaroma/Properties/Resources.Designer.cs index 1ca9cb7..55410bd 100644 --- a/Novaroma/Properties/Resources.Designer.cs +++ b/Novaroma/Properties/Resources.Designer.cs @@ -1696,6 +1696,24 @@ public static string MovieDownloadStarted { } } + /// + /// Looks up a localized string similar to Movie File Rename Template. + /// + public static string MovieFileNameTemplate { + get { + return ResourceManager.GetString("MovieFileNameTemplate", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Available parameters: %movieName%, %year%, %rating%, %genres%, %voteCount%, %runtime%, %videoQuality%. + /// + public static string MovieFileNameTemplateDescription { + get { + return ResourceManager.GetString("MovieFileNameTemplateDescription", resourceCulture); + } + } + /// /// Looks up a localized string similar to Movie Providers. /// @@ -2587,6 +2605,24 @@ public static string TvShowEpisodeDownloadStarted { } } + /// + /// Looks up a localized string similar to Episode File Rename Template. + /// + public static string TvShowEpisodeFileNameTemplate { + get { + return ResourceManager.GetString("TvShowEpisodeFileNameTemplate", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Available parameters: %showName%, %season%, %episode%, %episodeName%, %year%, %rating%, %genres%, %voteCount%, %runtime%, %videoQuality%. + /// + public static string TvShowEpisodeFileNameTemplateDescription { + get { + return ResourceManager.GetString("TvShowEpisodeFileNameTemplateDescription", resourceCulture); + } + } + /// /// Looks up a localized string similar to Tv Show Episode Search Pattern. /// diff --git a/Novaroma/Properties/Resources.resx b/Novaroma/Properties/Resources.resx index b363a75..dac6884 100644 --- a/Novaroma/Properties/Resources.resx +++ b/Novaroma/Properties/Resources.resx @@ -1060,4 +1060,16 @@ Update Available + + Movie File Rename Template + + + Available parameters: %movieName%, %year%, %rating%, %genres%, %voteCount%, %runtime%, %videoQuality% + + + Episode File Rename Template + + + Available parameters: %showName%, %season%, %episode%, %episodeName%, %year%, %rating%, %genres%, %voteCount%, %runtime%, %videoQuality% + \ No newline at end of file diff --git a/Novaroma/Properties/Resources.tr.resx b/Novaroma/Properties/Resources.tr.resx index 651cce7..7b65800 100644 --- a/Novaroma/Properties/Resources.tr.resx +++ b/Novaroma/Properties/Resources.tr.resx @@ -1011,4 +1011,16 @@ Yeni Versiyon Yükle + + Film Dosyası İsimlendirme Şablonu + + + Geçerli parametreler: %movieName%, %year%, %rating%, %genres%, %voteCount%, %runtime%, %videoQuality% + + + Dizi Bölüm Dosyası İsimlendirme Şablonu + + + Geçerli parametreler: %showName%, %season%, %episode%, %episodeName%, %year%, %rating%, %genres%, %voteCount%, %runtime%, %videoQuality% + \ No newline at end of file