/// <summary> /// Recursively gets known files (files that are in a content folder) from a movie root folder and its children. /// </summary> /// <param name="folder">Root folders to look for files in</param> /// <param name="folderPath">Current folder path</param> /// <param name="files">List of files to add to</param> /// <param name="depth">Current depth from root folder</param> /// <param name="movie">Movie current path belongs to</param> private void GetKnownFiles(ContentRootFolder folder, string folderPath, List<OrgPath> files, int depth, Movie movie) { // Match to movie if (depth == 1) { foreach (Movie mov in Organization.Movies) if (mov.Path == folderPath) { movie = mov; break; } } // Only get files from folders that allow organization if (folder.AllowOrganizing) { // Get files only from non-content sub-folders if (depth > 0) { string[] fileList = Directory.GetFiles(folderPath); foreach (string file in fileList) files.Add(new OrgPath(file, true, folder.AllowOrganizing, folder, movie)); } // Recursion on sub folders string[] subDirs = Directory.GetDirectories(folderPath); foreach (string subDir in subDirs) { ContentRootFolder subDirContent = null; foreach (ContentRootFolder subfolder in folder.ChildFolders) if (subfolder.FullPath == subDir) { subDirContent = subfolder; break; } if (subDirContent != null) GetKnownFiles(subDirContent, subDirContent.FullPath, files, 0, movie); else GetKnownFiles(folder, subDir, files, depth + 1, movie); } } }
/// <summary> /// Tries to create a movie action item for a scan from a file. /// </summary> /// <param name="file">The file to create movie action from</param> /// <param name="item">The resulting movie action</param> /// <returns>Whether the file was matched to a movie</returns> private bool CreateMovieAction(OrgPath file, string matchString, out OrgItem item, bool threaded, bool fast, bool skipMatching, Movie knownMovie) { // Initialize item item = new OrgItem(OrgAction.None, file.Path, FileCategory.MovieVideo, file.OrgFolder); // Check if sample! if (matchString.ToLower().Contains("sample")) { item.Action = OrgAction.Delete; return false; } // Try to match file to movie string search = Path.GetFileNameWithoutExtension(matchString); // Search for match to movie Movie searchResult = null; bool searchSucess = false; if (!skipMatching) searchSucess = SearchHelper.MovieSearch.ContentMatch(search, string.Empty, string.Empty, fast, threaded, out searchResult, knownMovie); // Add closest match item if (searchSucess) { // Get root folder ContentRootFolder rootFolder; string path; if (Settings.GetMovieFolderForContent(searchResult, out rootFolder)) searchResult.RootFolder = rootFolder.FullPath; else { searchResult.RootFolder = NO_MOVIE_FOLDER; item.Action = OrgAction.NoRootFolder; } if (item.Action != OrgAction.NoRootFolder) item.Action = file.Copy ? OrgAction.Copy : OrgAction.Move; item.DestinationPath = searchResult.BuildFilePath(file.Path); if (File.Exists(item.DestinationPath)) item.Action = OrgAction.AlreadyExists; searchResult.Path = searchResult.BuildFolderPath(); item.Movie = searchResult; if (item.Action == OrgAction.AlreadyExists || item.Action == OrgAction.NoRootFolder) item.Enable = false; else item.Enable = true; return true; } return false; }
/// <summary> /// Tries to create a movie action item for a scan from a file. /// </summary> /// <param name="file">The file to create movie action from</param> /// <param name="item">The resulting movie action</param> /// <returns>Whether the file was matched to a movie</returns> private bool CreateMovieAction(OrgPath file, string matchString, out OrgItem item, bool threaded, bool fast, bool skipMatching, Movie knownMovie) { // Initialize item item = new OrgItem(OrgAction.None, file.Path, FileCategory.MovieVideo, file.OrgFolder); // Check if sample! if (matchString.ToLower().Contains("sample")) { item.Action = OrgAction.Delete; return(false); } // Try to match file to movie string search = Path.GetFileNameWithoutExtension(matchString); // Search for match to movie Movie searchResult = null; bool searchSucess = false; if (!skipMatching) { searchSucess = SearchHelper.MovieSearch.ContentMatch(search, string.Empty, string.Empty, fast, threaded, out searchResult, knownMovie); } // Add closest match item if (searchSucess) { // Get root folder ContentRootFolder rootFolder; string path; if (Settings.GetMovieFolderForContent(searchResult, out rootFolder)) { searchResult.RootFolder = rootFolder.FullPath; } else { searchResult.RootFolder = NO_MOVIE_FOLDER; item.Action = OrgAction.NoRootFolder; } if (item.Action != OrgAction.NoRootFolder) { item.Action = file.Copy ? OrgAction.Copy : OrgAction.Move; } item.DestinationPath = searchResult.BuildFilePath(file.Path); if (File.Exists(item.DestinationPath)) { item.Action = OrgAction.AlreadyExists; } searchResult.Path = searchResult.BuildFolderPath(); item.Movie = searchResult; if (item.Action == OrgAction.AlreadyExists || item.Action == OrgAction.NoRootFolder) { item.Enable = false; } else { item.Enable = true; } return(true); } return(false); }
/// <summary> /// Attempts to match string to content from the online database. /// </summary> /// <param name="search">Search string to match against</param> /// <param name="rootFolder">The root folder the content will belong to</param> /// <param name="folderPath">Folder path where the content should be moved to</param> /// <returns>Match content item, null if no match</returns> protected bool ContentMatch(string search, string rootFolder, string folderPath, bool fast, bool threaded, out Content match, Content knownContent) { // Create empty content Content emptyContent; switch (this.ContentType) { case ContentType.Movie: emptyContent = new Movie(); break; case ContentType.TvShow: emptyContent = new TvShow(); break; default: throw new Exception("Unknown content type"); } emptyContent.Path = folderPath; emptyContent.RootFolder = rootFolder; emptyContent.Found = true; // Check for empty search condition if (string.IsNullOrEmpty(search)) { match = emptyContent; return false; } // Get year from search string int dirYear = FileHelper.GetYear(search); // Get list of simplified strings List<FileHelper.SimplifyStringResults> searches = new List<FileHelper.SimplifyStringResults>(); // Get list of search bases List<string> searchBases = GetModifiedSearches(search); // Fast search: use first search base only if (fast) { FileHelper.SimplifyStringResults result = FileHelper.BuildSimplifyResults(searchBases[0], false, false, FileHelper.OptionalSimplifyRemoves.YearAndFollowing, true, false, true, false); searches.Add(result); } // Full search: Go through each search base and get simplified search options else foreach (string searchBase in searchBases) { // Get results from current base List<FileHelper.SimplifyStringResults> currSearches = FileHelper.SimplifyString(searchBase); currSearches.Add(new FileHelper.SimplifyStringResults(searchBase, new Dictionary<FileWordType, List<string>>(), ContentSearchMod.None)); // Add each result to full list of searches foreach (FileHelper.SimplifyStringResults results in currSearches) { // Check if search already exist bool exists = false; foreach (FileHelper.SimplifyStringResults s in searches) if (s.SimplifiedString == results.SimplifiedString) { exists = true; break; } // If doesn't exist add it to searches if (!exists && !string.IsNullOrWhiteSpace(results.SimplifiedString)) searches.Add(results); } } searches.Sort(); // Create new status int currSeachCnt; MatchStatus status; lock (searchLock) { currSeachCnt = ++searchCount; status = new MatchStatus(searches.Count, this.ContentType); searchStatus.Add(currSeachCnt, status); } ContentSearchMod lowMods; Content lowestModsMatch; // Add thread to pool for each search that need to be performed int searchNum = 0; while (searchNum < searches.Count) { // Check for any search results so far if (status.GetSearchResultWithLowestMods(out lowMods, out lowestModsMatch)) { // If search results have no mods or just year removed use them as final results if (lowMods == ContentSearchMod.None || lowMods == ContentSearchMod.YearRemoved) { match = lowestModsMatch; return true; } } // Limit number of search threads created if (status.NumStarted - status.NumCompleted >= Settings.General.NumSimultaneousSearches) { Thread.Sleep(100); continue; } // Build search arguments object[] args = { currSeachCnt, searchNum, searches[searchNum].SimplifiedString, folderPath, rootFolder, dirYear, searches[searchNum].Modifications, knownContent }; // Threaded: add a search to thread pool if (threaded) { ThreadPool.QueueUserWorkItem(new WaitCallback(SearchThread), args); lock (searchLock) status.SetSearchStarted(searchNum); } // Synchronized: call search method else SearchThread(args); searchNum++; } // Wait for all search to complete while (status.NumCompleted < searches.Count) { // Check for any search results so far if (status.GetSearchResultWithLowestMods(out lowMods, out lowestModsMatch)) { // If search results have no mods or just year removed use them as final results if (lowMods == ContentSearchMod.None || lowMods == ContentSearchMod.YearRemoved) { match = lowestModsMatch; return true; } } Thread.Sleep(100); } // Clear status lock (searchLock) searchStatus.Remove(currSeachCnt); // Return result with lowest mods to search string if (status.GetSearchResultWithLowestMods(out lowMods, out lowestModsMatch)) { match = lowestModsMatch; return true; } else { match = emptyContent; return false; } }
/// <summary> /// Get search match with lowest modification to search string /// </summary> /// <param name="status">Search status instance</param> /// <param name="lowestModsMatchStrLen">Length of best result's content name</param> /// <param name="results">Best resulting content</param> /// <returns>Whether a valid content match result was found</returns> public bool GetSearchResultWithLowestMods(out ContentSearchMod modsOnResultsSearch, out Content results) { int lowestModsMatchStrLen = 0; modsOnResultsSearch = ContentSearchMod.All; switch (this.ContentType) { case ContentType.Movie: results = new Movie(); break; case ContentType.TvShow: results = new TvShow(); break; default: throw new Exception("Unknown content type"); } // Use match with lowest amount of modification made to search string and longest length (I think this is the most likely to the content we actually want to match to) for (int i = 0; i < this.matches.Length; i++) if (this.matches[i] != null) for (int j = 0; j < this.matches[i].Count; j++) { if (this.matches[i][j].Mods < modsOnResultsSearch || (this.matches[i][j].Mods == modsOnResultsSearch && this.matches[i][j].MatchedString.Length > lowestModsMatchStrLen)) { results = this.matches[i][j].Content; modsOnResultsSearch = this.matches[i][j].Mods; lowestModsMatchStrLen = this.matches[i][j].MatchedString.Length; } } return !string.IsNullOrWhiteSpace(results.DatabaseName); }
private void UpdatePreview() { // Build preview switch (this.ContentType) { case ContentType.Movie: Movie movie = new Movie("Donnie Darko"); movie.DatabaseYear = 2001; this.FileNamePreview1Title = "Example 1: 'Donnie Darko', 720p, bluray rip - x264, 5.1 audio - english, extended cut, file part 1"; this.FileNamePreview1 = System.IO.Path.GetFileNameWithoutExtension(this.FileNameFormat.BuildMovieFileName(movie, "Donnie Darko 720p blurayrip x264 5.1 en extended cut cd1.avi")); movie = new Movie("The Matrix"); movie.DatabaseYear = 1999; this.FileNamePreview2Title = "Example 2: 'The Matrix', video/audio information unknown, no parts"; this.FileNamePreview2 = System.IO.Path.GetFileNameWithoutExtension(this.FileNameFormat.BuildMovieFileName(movie, "The Matrix.avi")); break; case ContentType.TvShow: this.FileNamePreview1Title = "Example 1: Single Episode: Episode 5 of season 1 of the show 'Arrested Development'"; ; this.FileNamePreview1 = this.FileNameFormat.BuildTvFileName(new TvEpisode("Charity Drive", new TvShow("Arrested Development"), 1, 5, "", ""), null, string.Empty); this.FileNamePreview2Title = "Example 2: Double Episode: Episode 23 and 24 of season 9 of the show 'Seinfeld'"; this.FileNamePreview2 = this.FileNameFormat.BuildTvFileName(new TvEpisode("The Finale (Part 1)", new TvShow("Seinfeld"), 9, 23, "", ""), new TvEpisode("The Finale (Part 2)", new TvShow("Seinfeld"), 9, 24, "", ""), string.Empty); break; } }
/// <summary> /// Constructor for movie item. /// </summary> /// <param name="action">action to be performed</param> /// <param name="sourceFile">the source path</param> /// <param name="category">file's category</param> /// <param name="movie">Movie object related to file</param> /// <param name="destination">destination path</param> /// <param name="scanDir">path to content folder of movie</param> public OrgItem(OrgAction action, string sourceFile, FileCategory category, Movie movie, string destination, OrgFolder scanDir) : this() { this.Progress = 0; this.Action = action; this.SourcePath = sourceFile; this.Movie = movie; this.ScanDirectory = scanDir; this.DestinationPath = destination; this.Category = category; this.Enable = false; this.Number = 0; }
/// <summary> /// Converts HTTP get result node into movie. /// </summary> /// <param name="baseMovie">Movie instance to start with (to keep path/org info from)</param> /// <param name="resultNode">Result node from get request containing movie info</param> /// <returns>Movie with properties from results node</returns> private void ParseMovieResult(Movie baseMovie, JsonNode resultNode) { baseMovie.DatabaseSelection = (int)MovieDatabaseSelection.TheMovieDb; // Go through result child nodes and get properties for movie foreach (JsonNode resultPropNode in resultNode.ChildNodes) switch (resultPropNode.Name) { case "id": int id2; int.TryParse(resultPropNode.Value, out id2); baseMovie.Id = id2; break; case "title": baseMovie.DatabaseName = resultPropNode.Value; break; case "original_title": if (string.IsNullOrEmpty(baseMovie.DatabaseName)) baseMovie.DatabaseName = resultPropNode.Value; break; case "release_date": DateTime date; DateTime.TryParse(resultPropNode.Value, out date); baseMovie.DatabaseYear = date.Year; break; case "genres": baseMovie.DatabaseGenres = new GenreCollection(GenreCollection.CollectionType.Movie); foreach (JsonNode genreNode in resultPropNode.ChildNodes) { foreach (JsonNode genrePropNode in genreNode.ChildNodes) if (genrePropNode.Name == "name") baseMovie.DatabaseGenres.Add(genrePropNode.Value); } break; case "overview": baseMovie.Overview = resultPropNode.Value; break; } }
/// <summary> /// Searches for movie from database. /// </summary> /// <param name="search">Search string for movie</param> /// <returns>Search results as list of movies</returns> protected override List<Content> DoSearch(string mirror, string searchString, bool includeSummaries) { // Perform request from database List<HttpGetParameter> parameters = new List<HttpGetParameter>(); parameters.Add(new HttpGetParameter("query", searchString)); JsonNode searchNode = GetJsonRequest(mirror, parameters, "search/movie"); // Go through result nodes, convert them to movie objects, and add to list List<Content> searchResults = new List<Content>(); foreach (JsonNode pageNode in searchNode.ChildNodes) foreach (JsonNode node in pageNode.ChildNodes) if (node.Name == "results") foreach (JsonNode resultNode in node.ChildNodes) { Movie movieResult = new Movie(); ParseMovieResult(movieResult, resultNode); if (movieResult.Id > 0) searchResults.Add(movieResult); } // Return results list return searchResults; }
/// <summary> /// Update processing method (thread) for single content folder in root. /// </summary> /// <param name="orgPath">Organization path instance to be processed</param> /// <param name="pathNum">The path's number out of total being processed</param> /// <param name="totalPaths">Total number of paths being processed</param> /// <param name="processNumber">The identifier for the OrgProcessing instance</param> /// <param name="numItemsProcessed">Number of paths that have been processed - used for progress updates</param> /// <param name="numItemsStarted">Number of paths that have had been added to thread pool for processing</param> /// <param name="processSpecificArgs">Arguments specific to this process</param> private void UpdateProcess(OrgPath orgPath, int pathNum, int totalPaths, int processNumber, ref int numItemsProcessed, ref int numItemsStarted, object processSpecificArgs) { // Check for cancellation - this method is called from thread pool, so cancellation could have occured by the time this is run if (updateCancelled || this.updateNumber != processNumber) return; // First pass run does quick folder update by skipping online database searching bool firstPass = (bool)processSpecificArgs; string passString = firstPass ? " (First Pass)" : " (Second Pass)"; // Set processing messge string progressMsg = "Updating of '" + orgPath.RootFolder.FullPath + "'" + passString + " - '" + Path.GetFileName(orgPath.Path) + "' started"; OnUpdateProgressChange(this, false, CalcProgress(numItemsProcessed, numItemsStarted, totalPaths), progressMsg); // Get content collection to add content to ContentCollection content = GetContentCollection(); // Check if folder already has a match to existing content bool contentExists = false; bool contentComplete = false; Content newContent = null; int index = 0; for (int j = 0; j < content.Count; j++) if (Path.Equals(orgPath.Path, content[j].Path)) { contentExists = true; content[j].Found = true; if (!string.IsNullOrEmpty(content[j].DatabaseName)) contentComplete = true; newContent = content[j]; index = j; break; } // Set completed progess message progressMsg = "Updating of '" + orgPath.RootFolder.FullPath + "'" + passString + " - '" + Path.GetFileName(orgPath.Path) + "' complete"; // Check if content found if (contentExists && contentComplete) { // Check if content needs updating if ((DateTime.Now - newContent.LastUpdated).TotalDays > 7 && this.ContentType == ContentType.TvShow) newContent.UpdateInfoFromDatabase(); // Update progress if (this.updateNumber == processNumber) OnUpdateProgressChange(this, false, CalcProgress(numItemsProcessed, numItemsStarted, totalPaths), progressMsg); return; } // Folder wasn't matched to an existing content instance, try tmatch folder to content from online database Content match; bool matchSucess; switch (this.ContentType) { case ContentType.TvShow: TvShow showMatch; matchSucess = SearchHelper.TvShowSearch.PathMatch(orgPath.RootFolder.FullPath, orgPath.Path, firstPass, true, out showMatch); match = showMatch; break; case ContentType.Movie: Movie movieMatch; matchSucess = SearchHelper.MovieSearch.PathMatch(orgPath.RootFolder.FullPath, orgPath.Path, firstPass, true, out movieMatch); match = movieMatch; break; default: throw new Exception("unknown content type"); } // Check that current process hasn't been replaced - search can be slow, so update may have been cancelled by the time it gets here if (updateCancelled || this.updateNumber != processNumber) return; // Folder already existed, but wasn't previously match to valid content if (contentExists && matchSucess) { switch (this.ContentType) { case ContentType.TvShow: ((TvShow)newContent).CloneAndHandlePath((TvShow)match, true); ((TvShow)newContent).UpdateMissing(); break; case ContentType.Movie: ((Movie)newContent).CloneAndHandlePath((Movie)match, true); break; default: throw new Exception("unknown content type"); } newContent.LastUpdated = DateTime.Now; } else if (matchSucess) newContent = match; else switch (this.ContentType) { case ContentType.TvShow: newContent = new TvShow(string.Empty, 0, 0, orgPath.Path, orgPath.RootFolder.FullPath); break; case ContentType.Movie: newContent = new Movie(string.Empty, 0, 0, orgPath.Path, orgPath.RootFolder.FullPath); break; default: throw new Exception("unknown content type"); } // Set found flag newContent.Found = true; // Add content to list if new if (!contentExists) content.Add(newContent); // Update progress OnUpdateProgressChange(this, true, CalcProgress(numItemsProcessed, numItemsStarted, totalPaths), progressMsg); }
/// <summary> /// Converts HTTP get result node into movie. /// </summary> /// <param name="baseMovie">Movie instance to start with (to keep path/org info from)</param> /// <param name="resultNode">Result node from get request containing movie info</param> /// <returns>Movie with properties from results node</returns> private void ParseMovieResult(Movie baseMovie, JsonNode resultNode) { baseMovie.DatabaseSelection = (int)MovieDatabaseSelection.RottenTomotoes; // Go through result child nodes and get properties for movie foreach (JsonNode resultPropNode in resultNode.ChildNodes) switch (resultPropNode.Name) { case "id": int id2; int.TryParse(resultPropNode.Value, out id2); baseMovie.Id = id2; break; case "title": baseMovie.DatabaseName = resultPropNode.Value; break; case "original_title": if (string.IsNullOrEmpty(baseMovie.DatabaseName)) baseMovie.DatabaseName = resultPropNode.Value; break; case "year": if (baseMovie.DatabaseYear < 1900) { int year; if (int.TryParse(resultPropNode.Value, out year)) baseMovie.DatabaseYear = year; } break; //case "release_dates": // foreach(JsonNode node in resultPropNode.ChildNodes) // if (node.Name == "theater") // { // DateTime date; // DateTime.TryParse(node.Value, out date); // baseMovie.Date = date; // } // break; case "genres": baseMovie.DatabaseGenres = new GenreCollection(GenreCollection.CollectionType.Movie); string[] genres = resultPropNode.Value.Split(','); foreach (string genre in genres) { string noQuotesGenre = genre.Replace("\"", ""); string[] multiGenre = noQuotesGenre.Split('&'); foreach (string g in multiGenre) baseMovie.DatabaseGenres.Add(g.Trim()); } break; case "synopsis": baseMovie.Overview = resultPropNode.Value; break; } }
/// <summary> /// Updates movie infor from database /// </summary> /// <param name="movie"></param> public static void UpdateMovieInfo(Movie movie) { GetDataBaseAccess(movie.Database).Update(movie); }
/// <summary> /// Constructor for cloning a Movie. /// </summary> /// <param name="movie"></param> public Movie(Movie movie) : this() { Clone(movie); }
/// <summary> /// Builds formatted file name for movie from an existing file path. /// </summary> /// <param name="movie">The movie associated with file</param> /// <param name="fullPath">The path of current file to be renamed</param> /// <returns>Resulting formatted file name string</returns> public string BuildMovieFileName(Movie movie, string fullPath) { string fileName = Path.GetFileNameWithoutExtension(fullPath); string folderPath = Path.GetDirectoryName(fullPath); string fileExt = Path.GetExtension(fullPath).ToLower(); // Get info from file using simplify function (pulls out file words and categorizes them) FileHelper.OptionalSimplifyRemoves simplifyOptions = FileHelper.OptionalSimplifyRemoves.Year; FileHelper.SimplifyStringResults simpleResult = FileHelper.BuildSimplifyResults(fileName, false, false, simplifyOptions, true, false, true, false); // Get all video files from folder that don't match current path string differentiator = string.Empty; if (Directory.Exists(folderPath)) // Check in case path was empty { string[] dirFiles = Directory.GetFiles(folderPath); List<string> diffFiles = dirFiles.ToList().FindAll(f => Path.GetFileName(f) != Path.GetFileName(fileName) && Settings.VideoFileTypes.Contains(Path.GetExtension(f))); // Get differentiating part of string for other files with similar name foreach (string diffFile in diffFiles) { // Get file name string diffFileName = Path.GetFileNameWithoutExtension(diffFile); // Init differentiating string string diff = string.Empty; // Count number of characters in that match 2 file names and build string of different characters int matchCnt = 0; for (int i = 0; i < fileName.Length; i++) { if (i < diffFileName.Length && diffFileName[i].Equals(fileName[i])) matchCnt++; else diff += fileName[i]; } // If enough characters are common between the two strings it likely means there the same movie if (matchCnt > 10 || matchCnt > (fileName.Length * 3) / 4) // Save longest file name difference if (diff.Length > differentiator.Length) differentiator = diff; } // Simplify differentiator differentiator = Regex.Replace(differentiator, @"']+", " "); differentiator = Regex.Replace(differentiator, @"[!\?\u0028\u0029\:\]\[]+", " "); differentiator = Regex.Replace(differentiator, @"\W+|_", " "); differentiator = differentiator.Trim(); } // Build file name string buildName = BuildFileName(movie.DisplayName, movie.DisplayYear, simpleResult, differentiator); // Remove unsafe file characters and add extension return FileHelper.GetSafeFileName(buildName) + fileExt; }