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