using MediaBrowser.Common.Extensions; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.IO; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Localization; using MediaBrowser.Controller.Persistence; using MediaBrowser.Controller.Providers; using MediaBrowser.Controller.Resolvers; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Logging; using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Runtime.Serialization; using System.Text; using System.Threading; using System.Threading.Tasks; namespace MediaBrowser.Controller.Entities { /// /// Class BaseItem /// public abstract class BaseItem : IHasProviderIds { protected BaseItem() { Genres = new List(); TrailerUrls = new List(); Studios = new List(); People = new List(); CriticReviews = new List(); } /// /// The trailer folder name /// public const string TrailerFolderName = "trailers"; public const string ThemeSongsFolderName = "theme-music"; /// /// Gets or sets the name. /// /// The name. public virtual string Name { get; set; } /// /// Gets or sets the id. /// /// The id. public virtual Guid Id { get; set; } /// /// Gets or sets the path. /// /// The path. public virtual string Path { get; set; } /// /// Gets or sets the type of the location. /// /// The type of the location. public virtual LocationType LocationType { get { if (string.IsNullOrEmpty(Path)) { return LocationType.Virtual; } return System.IO.Path.IsPathRooted(Path) ? LocationType.FileSystem : LocationType.Remote; } } /// /// This is just a helper for convenience /// /// The primary image path. [IgnoreDataMember] public string PrimaryImagePath { get { return GetImage(ImageType.Primary); } set { SetImage(ImageType.Primary, value); } } /// /// Gets or sets the images. /// /// The images. public virtual Dictionary Images { get; set; } /// /// Gets or sets the date created. /// /// The date created. public DateTime DateCreated { get; set; } /// /// Gets or sets the date modified. /// /// The date modified. public DateTime DateModified { get; set; } /// /// The logger /// public static ILogger Logger { get; set; } public static ILibraryManager LibraryManager { get; set; } public static IServerConfigurationManager ConfigurationManager { get; set; } public static IProviderManager ProviderManager { get; set; } /// /// Returns a that represents this instance. /// /// A that represents this instance. public override string ToString() { return Name; } /// /// Returns true if this item should not attempt to fetch metadata /// /// true if [dont fetch meta]; otherwise, false. [IgnoreDataMember] public virtual bool DontFetchMeta { get { if (Path != null) { return Path.IndexOf("[dontfetchmeta]", StringComparison.OrdinalIgnoreCase) != -1; } return false; } } /// /// Determines whether the item has a saved local image of the specified name (jpg or png). /// /// The name. /// true if [has local image] [the specified item]; otherwise, false. /// name public bool HasLocalImage(string name) { if (string.IsNullOrEmpty(name)) { throw new ArgumentNullException("name"); } return ResolveArgs.ContainsMetaFileByName(name + ".jpg") || ResolveArgs.ContainsMetaFileByName(name + ".png"); } /// /// Should be overridden to return the proper folder where metadata lives /// /// The meta location. [IgnoreDataMember] public virtual string MetaLocation { get { return Path ?? ""; } } /// /// The _provider data /// private Dictionary _providerData; /// /// Holds persistent data for providers like last refresh date. /// Providers can use this to determine if they need to refresh. /// The BaseProviderInfo class can be extended to hold anything a provider may need. /// Keyed by a unique provider ID. /// /// The provider data. public Dictionary ProviderData { get { return _providerData ?? (_providerData = new Dictionary()); } set { _providerData = value; } } /// /// The _file system stamp /// private Guid? _fileSystemStamp; /// /// Gets a directory stamp, in the form of a string, that can be used for /// comparison purposes to determine if the file system entries for this item have changed. /// /// The file system stamp. [IgnoreDataMember] public Guid FileSystemStamp { get { if (!_fileSystemStamp.HasValue) { _fileSystemStamp = GetFileSystemStamp(); } return _fileSystemStamp.Value; } } /// /// Gets the type of the media. /// /// The type of the media. [IgnoreDataMember] public virtual string MediaType { get { return null; } } /// /// Gets a directory stamp, in the form of a string, that can be used for /// comparison purposes to determine if the file system entries for this item have changed. /// /// Guid. private Guid GetFileSystemStamp() { // If there's no path or the item is a file, there's nothing to do if (LocationType != LocationType.FileSystem || !ResolveArgs.IsDirectory) { return Guid.Empty; } var sb = new StringBuilder(); // Record the name of each file // Need to sort these because accoring to msdn docs, our i/o methods are not guaranteed in any order foreach (var file in ResolveArgs.FileSystemChildren.OrderBy(f => f.Name)) { sb.Append(file.Name); } foreach (var file in ResolveArgs.MetadataFiles.OrderBy(f => f.Name)) { sb.Append(file.Name); } return sb.ToString().GetMD5(); } /// /// The _resolve args /// private ItemResolveArgs _resolveArgs; /// /// The _resolve args initialized /// private bool _resolveArgsInitialized; /// /// The _resolve args sync lock /// private object _resolveArgsSyncLock = new object(); /// /// We attach these to the item so that we only ever have to hit the file system once /// (this includes the children of the containing folder) /// Use ResolveArgs.FileSystemDictionary to check for the existence of files instead of File.Exists /// /// The resolve args. [IgnoreDataMember] public ItemResolveArgs ResolveArgs { get { try { LazyInitializer.EnsureInitialized(ref _resolveArgs, ref _resolveArgsInitialized, ref _resolveArgsSyncLock, () => CreateResolveArgs()); } catch (IOException ex) { Logger.ErrorException("Error creating resolve args for ", ex, Path); throw; } return _resolveArgs; } set { _resolveArgs = value; _resolveArgsInitialized = value != null; // Null this out so that it can be lazy loaded again _fileSystemStamp = null; } } /// /// Resets the resolve args. /// /// The path info. public void ResetResolveArgs(FileSystemInfo pathInfo) { ResolveArgs = CreateResolveArgs(pathInfo); } /// /// Creates ResolveArgs on demand /// /// The path info. /// ItemResolveArgs. /// Unable to retrieve file system info for + path protected internal virtual ItemResolveArgs CreateResolveArgs(FileSystemInfo pathInfo = null) { var path = Path; // non file-system entries will not have a path if (LocationType != LocationType.FileSystem || string.IsNullOrEmpty(path)) { return new ItemResolveArgs(ConfigurationManager.ApplicationPaths); } if (UseParentPathToCreateResolveArgs) { path = System.IO.Path.GetDirectoryName(path); } pathInfo = pathInfo ?? FileSystem.GetFileSystemInfo(path); if (pathInfo == null || !pathInfo.Exists) { throw new IOException("Unable to retrieve file system info for " + path); } var args = new ItemResolveArgs(ConfigurationManager.ApplicationPaths) { FileInfo = pathInfo, Path = path, Parent = Parent }; // Gather child folder and files if (args.IsDirectory) { var isPhysicalRoot = args.IsPhysicalRoot; // When resolving the root, we need it's grandchildren (children of user views) var flattenFolderDepth = isPhysicalRoot ? 2 : 0; args.FileSystemDictionary = FileData.GetFilteredFileSystemEntries(args.Path, Logger, flattenFolderDepth: flattenFolderDepth, args: args, resolveShortcuts: isPhysicalRoot || args.IsVf); } //update our dates EntityResolutionHelper.EnsureDates(this, args); return args; } /// /// Some subclasses will stop resolving at a directory and point their Path to a file within. This will help ensure the on-demand resolve args are identical to the /// original ones. /// /// true if [use parent path to create resolve args]; otherwise, false. [IgnoreDataMember] protected virtual bool UseParentPathToCreateResolveArgs { get { return false; } } /// /// Gets or sets the name of the forced sort. /// /// The name of the forced sort. public string ForcedSortName { get; set; } private string _sortName; /// /// Gets or sets the name of the sort. /// /// The name of the sort. [IgnoreDataMember] public string SortName { get { return ForcedSortName ?? _sortName ?? (_sortName = CreateSortName()); } } /// /// Creates the name of the sort. /// /// System.String. protected virtual string CreateSortName() { if (Name == null) return null; //some items may not have name filled in properly var sortable = Name.Trim().ToLower(); sortable = ConfigurationManager.Configuration.SortRemoveCharacters.Aggregate(sortable, (current, search) => current.Replace(search.ToLower(), string.Empty)); sortable = ConfigurationManager.Configuration.SortReplaceCharacters.Aggregate(sortable, (current, search) => current.Replace(search.ToLower(), " ")); foreach (var search in ConfigurationManager.Configuration.SortRemoveWords) { var searchLower = search.ToLower(); // Remove from beginning if a space follows if (sortable.StartsWith(searchLower + " ")) { sortable = sortable.Remove(0, searchLower.Length + 1); } // Remove from middle if surrounded by spaces sortable = sortable.Replace(" " + searchLower + " ", " "); // Remove from end if followed by a space if (sortable.EndsWith(" " + searchLower)) { sortable = sortable.Remove(sortable.Length - (searchLower.Length + 1)); } } return sortable; } /// /// Gets or sets the parent. /// /// The parent. [IgnoreDataMember] public Folder Parent { get; set; } /// /// Gets the collection folder parent. /// /// The collection folder parent. [IgnoreDataMember] public Folder CollectionFolder { get { if (this is AggregateFolder) { return null; } if (IsFolder) { var iCollectionFolder = this as ICollectionFolder; if (iCollectionFolder != null) { return (Folder)this; } } var parent = Parent; while (parent != null) { var iCollectionFolder = parent as ICollectionFolder; if (iCollectionFolder != null) { return parent; } parent = parent.Parent; } return null; } } /// /// When the item first debuted. For movies this could be premiere date, episodes would be first aired /// /// The premiere date. public DateTime? PremiereDate { get; set; } /// /// Gets or sets the end date. /// /// The end date. public DateTime? EndDate { get; set; } /// /// Gets or sets the display type of the media. /// /// The display type of the media. public virtual string DisplayMediaType { get; set; } /// /// Gets or sets the backdrop image paths. /// /// The backdrop image paths. public List BackdropImagePaths { get; set; } /// /// Gets or sets the screenshot image paths. /// /// The screenshot image paths. public List ScreenshotImagePaths { get; set; } /// /// Gets or sets the official rating. /// /// The official rating. public virtual string OfficialRating { get; set; } /// /// Gets or sets the custom rating. /// /// The custom rating. public virtual string CustomRating { get; set; } /// /// Gets or sets the language. /// /// The language. public string Language { get; set; } /// /// Gets or sets the overview. /// /// The overview. public string Overview { get; set; } /// /// Gets or sets the taglines. /// /// The taglines. public List Taglines { get; set; } /// /// Gets or sets the people. /// /// The people. public List People { get; set; } /// /// Override this if you need to combine/collapse person information /// /// All people. [IgnoreDataMember] public virtual IEnumerable AllPeople { get { return People; } } /// /// Gets or sets the studios. /// /// The studios. public virtual List Studios { get; set; } /// /// Gets or sets the genres. /// /// The genres. public virtual List Genres { get; set; } /// /// Gets or sets the home page URL. /// /// The home page URL. public string HomePageUrl { get; set; } /// /// Gets or sets the budget. /// /// The budget. public double? Budget { get; set; } /// /// Gets or sets the revenue. /// /// The revenue. public double? Revenue { get; set; } /// /// Gets or sets the production locations. /// /// The production locations. public List ProductionLocations { get; set; } /// /// Gets or sets the critic rating. /// /// The critic rating. public float? CriticRating { get; set; } /// /// Gets or sets the critic rating summary. /// /// The critic rating summary. public string CriticRatingSummary { get; set; } /// /// Gets or sets the community rating. /// /// The community rating. public float? CommunityRating { get; set; } /// /// Gets or sets the run time ticks. /// /// The run time ticks. public long? RunTimeTicks { get; set; } /// /// Gets or sets the aspect ratio. /// /// The aspect ratio. public string AspectRatio { get; set; } /// /// Gets or sets the production year. /// /// The production year. public virtual int? ProductionYear { get; set; } /// /// If the item is part of a series, this is it's number in the series. /// This could be episode number, album track number, etc. /// /// The index number. public int? IndexNumber { get; set; } /// /// For an episode this could be the season number, or for a song this could be the disc number. /// /// The parent index number. public int? ParentIndexNumber { get; set; } /// /// Gets or sets the critic reviews. /// /// The critic reviews. public List CriticReviews { get; set; } /// /// The _local trailers /// private List _localTrailers; /// /// The _local trailers initialized /// private bool _localTrailersInitialized; /// /// The _local trailers sync lock /// private object _localTrailersSyncLock = new object(); /// /// Gets the local trailers. /// /// The local trailers. [IgnoreDataMember] public List LocalTrailers { get { LazyInitializer.EnsureInitialized(ref _localTrailers, ref _localTrailersInitialized, ref _localTrailersSyncLock, LoadLocalTrailers); return _localTrailers; } private set { _localTrailers = value; if (value == null) { _localTrailersInitialized = false; } } } private List _themeSongs; private bool _themeSongsInitialized; private object _themeSongsSyncLock = new object(); [IgnoreDataMember] public List ThemeSongs { get { LazyInitializer.EnsureInitialized(ref _themeSongs, ref _themeSongsInitialized, ref _themeSongsSyncLock, LoadThemeSongs); return _themeSongs; } private set { _themeSongs = value; if (value == null) { _themeSongsInitialized = false; } } } /// /// Loads local trailers from the file system /// /// List{Video}. private List LoadLocalTrailers() { ItemResolveArgs resolveArgs; try { resolveArgs = ResolveArgs; } catch (IOException ex) { Logger.ErrorException("Error getting ResolveArgs for {0}", ex, Path); return new List(); } if (!resolveArgs.IsDirectory) { return new List(); } var folder = resolveArgs.GetFileSystemEntryByName(TrailerFolderName); // Path doesn't exist. No biggie if (folder == null) { return new List(); } IEnumerable files; try { files = new DirectoryInfo(folder.FullName).EnumerateFiles(); } catch (IOException ex) { Logger.ErrorException("Error loading trailers for {0}", ex, Name); return new List(); } return LibraryManager.ResolvePaths(files, null).Select(video => { // Try to retrieve it from the db. If we don't find it, use the resolved version var dbItem = LibraryManager.RetrieveItem(video.Id) as Trailer; if (dbItem != null) { dbItem.ResolveArgs = video.ResolveArgs; video = dbItem; } return video; }).ToList(); } /// /// Loads the theme songs. /// /// List{Audio.Audio}. private List LoadThemeSongs() { ItemResolveArgs resolveArgs; try { resolveArgs = ResolveArgs; } catch (IOException ex) { Logger.ErrorException("Error getting ResolveArgs for {0}", ex, Path); return new List(); } if (!resolveArgs.IsDirectory) { return new List(); } var folder = resolveArgs.GetFileSystemEntryByName(ThemeSongsFolderName); // Path doesn't exist. No biggie if (folder == null) { return new List(); } IEnumerable files; try { files = new DirectoryInfo(folder.FullName).EnumerateFiles(); } catch (IOException ex) { Logger.ErrorException("Error loading theme songs for {0}", ex, Name); return new List(); } return LibraryManager.ResolvePaths(files, null).Select(audio => { // Try to retrieve it from the db. If we don't find it, use the resolved version var dbItem = LibraryManager.RetrieveItem(audio.Id) as Audio.Audio; if (dbItem != null) { dbItem.ResolveArgs = audio.ResolveArgs; audio = dbItem; } return audio; }).ToList(); } /// /// Overrides the base implementation to refresh metadata for local trailers /// /// The cancellation token. /// if set to true [is new item]. /// if set to true [force]. /// if set to true [allow slow providers]. /// if set to true [reset resolve args]. /// true if a provider reports we changed public virtual async Task RefreshMetadata(CancellationToken cancellationToken, bool forceSave = false, bool forceRefresh = false, bool allowSlowProviders = true, bool resetResolveArgs = true) { if (resetResolveArgs) { ResolveArgs = null; } // Lazy load these again LocalTrailers = null; ThemeSongs = null; // Refresh for the item var itemRefreshTask = ProviderManager.ExecuteMetadataProviders(this, cancellationToken, forceRefresh, allowSlowProviders); cancellationToken.ThrowIfCancellationRequested(); // Refresh metadata for local trailers var trailerTasks = LocalTrailers.Select(i => i.RefreshMetadata(cancellationToken, forceSave, forceRefresh, allowSlowProviders)); var themeSongTasks = ThemeSongs.Select(i => i.RefreshMetadata(cancellationToken, forceSave, forceRefresh, allowSlowProviders)); cancellationToken.ThrowIfCancellationRequested(); // Await the trailer tasks await Task.WhenAll(trailerTasks).ConfigureAwait(false); await Task.WhenAll(themeSongTasks).ConfigureAwait(false); cancellationToken.ThrowIfCancellationRequested(); // Get the result from the item task var changed = await itemRefreshTask.ConfigureAwait(false); if (changed || forceSave) { cancellationToken.ThrowIfCancellationRequested(); await LibraryManager.SaveItem(this, cancellationToken).ConfigureAwait(false); } return changed; } /// /// Clear out all metadata properties. Extend for sub-classes. /// public virtual void ClearMetaValues() { Images = null; ForcedSortName = null; PremiereDate = null; BackdropImagePaths = null; OfficialRating = null; CustomRating = null; Overview = null; Taglines = null; Language = null; Studios = null; Genres = null; CommunityRating = null; RunTimeTicks = null; AspectRatio = null; ProductionYear = null; ProviderIds = null; DisplayMediaType = GetType().Name; ResolveArgs = null; } /// /// Gets or sets the trailer URL. /// /// The trailer URL. public List TrailerUrls { get; set; } /// /// Gets or sets the provider ids. /// /// The provider ids. public Dictionary ProviderIds { get; set; } /// /// Override this to false if class should be ignored for indexing purposes /// /// true if [include in index]; otherwise, false. [IgnoreDataMember] public virtual bool IncludeInIndex { get { return true; } } /// /// Override this to true if class should be grouped under a container in indicies /// The container class should be defined via IndexContainer /// /// true if [group in index]; otherwise, false. [IgnoreDataMember] public virtual bool GroupInIndex { get { return false; } } /// /// Override this to return the folder that should be used to construct a container /// for this item in an index. GroupInIndex should be true as well. /// /// The index container. [IgnoreDataMember] public virtual Folder IndexContainer { get { return null; } } /// /// Gets the user data key. /// /// System.String. public virtual string GetUserDataKey() { return Id.ToString(); } /// /// Determines if a given user has access to this item /// /// The user. /// true if [is parental allowed] [the specified user]; otherwise, false. /// public bool IsParentalAllowed(User user) { if (user == null) { throw new ArgumentNullException("user"); } if (user.Configuration.MaxParentalRating == null) { return true; } return Ratings.Level(CustomRating ?? OfficialRating) <= user.Configuration.MaxParentalRating.Value; } /// /// Determines if this folder should be visible to a given user. /// Default is just parental allowed. Can be overridden for more functionality. /// /// The user. /// true if the specified user is visible; otherwise, false. /// user public virtual bool IsVisible(User user) { if (user == null) { throw new ArgumentNullException("user"); } return IsParentalAllowed(user); } /// /// Finds the particular item by searching through our parents and, if not found there, loading from repo /// /// The id. /// BaseItem. /// protected BaseItem FindParentItem(Guid id) { if (id == Guid.Empty) { throw new ArgumentException(); } var parent = Parent; while (parent != null && !parent.IsRoot) { if (parent.Id == id) return parent; parent = parent.Parent; } //not found - load from repo return LibraryManager.RetrieveItem(id); } /// /// Gets a value indicating whether this instance is folder. /// /// true if this instance is folder; otherwise, false. [IgnoreDataMember] public virtual bool IsFolder { get { return false; } } /// /// Determine if we have changed vs the passed in copy /// /// The copy. /// true if the specified copy has changed; otherwise, false. /// public virtual bool HasChanged(BaseItem copy) { if (copy == null) { throw new ArgumentNullException(); } var changed = copy.DateModified != DateModified; if (changed) { Logger.Debug(Name + " changed - original creation: " + DateCreated + " new creation: " + copy.DateCreated + " original modified: " + DateModified + " new modified: " + copy.DateModified); } return changed; } /// /// Determines if the item is considered new based on user settings /// /// true if [is recently added] [the specified user]; otherwise, false. /// public bool IsRecentlyAdded() { return (DateTime.UtcNow - DateCreated).TotalDays < ConfigurationManager.Configuration.RecentItemDays; } /// /// Adds people to the item /// /// The people. /// public void AddPeople(IEnumerable people) { if (people == null) { throw new ArgumentNullException(); } foreach (var person in people) { AddPerson(person); } } /// /// Adds a person to the item /// /// The person. /// public void AddPerson(PersonInfo person) { if (person == null) { throw new ArgumentNullException("person"); } if (string.IsNullOrWhiteSpace(person.Name)) { throw new ArgumentNullException(); } if (People == null) { People = new List { person }; return; } // If the type is GuestStar and there's already an Actor entry, then update it to avoid dupes if (string.Equals(person.Type, PersonType.GuestStar, StringComparison.OrdinalIgnoreCase)) { var existing = People.FirstOrDefault(p => p.Name.Equals(person.Name, StringComparison.OrdinalIgnoreCase) && p.Type.Equals(PersonType.Actor, StringComparison.OrdinalIgnoreCase)); if (existing != null) { existing.Type = PersonType.GuestStar; return; } } if (string.Equals(person.Type, PersonType.Actor, StringComparison.OrdinalIgnoreCase)) { // Only add actors if there isn't an existing one of type Actor or GuestStar if (!People.Any(p => p.Name.Equals(person.Name, StringComparison.OrdinalIgnoreCase) && (p.Type.Equals(PersonType.Actor, StringComparison.OrdinalIgnoreCase) || p.Type.Equals(PersonType.GuestStar, StringComparison.OrdinalIgnoreCase)))) { People.Add(person); } } else { // Check for dupes based on the combination of Name and Type if (!People.Any(p => p.Name.Equals(person.Name, StringComparison.OrdinalIgnoreCase) && p.Type.Equals(person.Type, StringComparison.OrdinalIgnoreCase))) { People.Add(person); } } } /// /// Adds studios to the item /// /// The studios. /// public void AddStudios(IEnumerable studios) { if (studios == null) { throw new ArgumentNullException(); } foreach (var name in studios) { AddStudio(name); } } /// /// Adds a studio to the item /// /// The name. /// public void AddStudio(string name) { if (string.IsNullOrWhiteSpace(name)) { throw new ArgumentNullException("name"); } if (Studios == null) { Studios = new List(); } if (!Studios.Contains(name, StringComparer.OrdinalIgnoreCase)) { Studios.Add(name); } } /// /// Adds a tagline to the item /// /// The name. /// public void AddTagline(string name) { if (string.IsNullOrWhiteSpace(name)) { throw new ArgumentNullException("name"); } if (Taglines == null) { Taglines = new List(); } if (!Taglines.Contains(name, StringComparer.OrdinalIgnoreCase)) { Taglines.Add(name); } } /// /// Adds a TrailerUrl to the item /// /// The URL. /// public void AddTrailerUrl(string url) { if (string.IsNullOrWhiteSpace(url)) { throw new ArgumentNullException("url"); } if (TrailerUrls == null) { TrailerUrls = new List(); } if (!TrailerUrls.Contains(url, StringComparer.OrdinalIgnoreCase)) { TrailerUrls.Add(url); } } /// /// Adds a genre to the item /// /// The name. /// public void AddGenre(string name) { if (string.IsNullOrWhiteSpace(name)) { throw new ArgumentNullException("name"); } if (Genres == null) { Genres = new List(); } if (!Genres.Contains(name, StringComparer.OrdinalIgnoreCase)) { Genres.Add(name); } } /// /// Adds the production location. /// /// The location. /// location public void AddProductionLocation(string location) { if (string.IsNullOrWhiteSpace(location)) { throw new ArgumentNullException("location"); } if (ProductionLocations == null) { ProductionLocations = new List(); } if (!ProductionLocations.Contains(location, StringComparer.OrdinalIgnoreCase)) { ProductionLocations.Add(location); } } /// /// Adds genres to the item /// /// The genres. /// public void AddGenres(IEnumerable genres) { if (genres == null) { throw new ArgumentNullException(); } foreach (var name in genres) { AddGenre(name); } } /// /// Marks the item as either played or unplayed /// /// The user. /// if set to true [was played]. /// The user manager. /// Task. /// public virtual async Task SetPlayedStatus(User user, bool wasPlayed, IUserDataRepository userManager) { if (user == null) { throw new ArgumentNullException(); } var key = GetUserDataKey(); var data = await userManager.GetUserData(user.Id, key).ConfigureAwait(false); if (wasPlayed) { data.PlayCount = Math.Max(data.PlayCount, 1); if (!data.LastPlayedDate.HasValue) { data.LastPlayedDate = DateTime.UtcNow; } } else { //I think it is okay to do this here. // if this is only called when a user is manually forcing something to un-played // then it probably is what we want to do... data.PlayCount = 0; data.PlaybackPositionTicks = 0; data.LastPlayedDate = null; } data.Played = wasPlayed; await userManager.SaveUserData(user.Id, key, data, CancellationToken.None).ConfigureAwait(false); } /// /// Do whatever refreshing is necessary when the filesystem pertaining to this item has changed. /// /// Task. public virtual Task ChangedExternally() { return RefreshMetadata(CancellationToken.None); } /// /// Finds a parent of a given type /// /// /// ``0. public T FindParent() where T : Folder { var parent = Parent; while (parent != null) { var result = parent as T; if (result != null) { return result; } parent = parent.Parent; } return null; } /// /// Gets an image /// /// The type. /// System.String. /// Backdrops should be accessed using Item.Backdrops public string GetImage(ImageType type) { if (type == ImageType.Backdrop) { throw new ArgumentException("Backdrops should be accessed using Item.Backdrops"); } if (type == ImageType.Screenshot) { throw new ArgumentException("Screenshots should be accessed using Item.Screenshots"); } if (Images == null) { return null; } string val; Images.TryGetValue(type, out val); return val; } /// /// Gets an image /// /// The type. /// true if the specified type has image; otherwise, false. /// Backdrops should be accessed using Item.Backdrops public bool HasImage(ImageType type) { if (type == ImageType.Backdrop) { throw new ArgumentException("Backdrops should be accessed using Item.Backdrops"); } if (type == ImageType.Screenshot) { throw new ArgumentException("Screenshots should be accessed using Item.Screenshots"); } return !string.IsNullOrEmpty(GetImage(type)); } /// /// Sets an image /// /// The type. /// The path. /// Backdrops should be accessed using Item.Backdrops public void SetImage(ImageType type, string path) { if (type == ImageType.Backdrop) { throw new ArgumentException("Backdrops should be accessed using Item.Backdrops"); } if (type == ImageType.Screenshot) { throw new ArgumentException("Screenshots should be accessed using Item.Screenshots"); } var typeKey = type; // If it's null remove the key from the dictionary if (string.IsNullOrEmpty(path)) { if (Images != null) { if (Images.ContainsKey(typeKey)) { Images.Remove(typeKey); } } } else { // Ensure it exists if (Images == null) { Images = new Dictionary(); } Images[typeKey] = path; } } /// /// Deletes the image. /// /// The type. /// Task. public async Task DeleteImage(ImageType type) { if (!HasImage(type)) { return; } // Delete the source file File.Delete(GetImage(type)); // Remove it from the item SetImage(type, null); // Refresh metadata await RefreshMetadata(CancellationToken.None).ConfigureAwait(false); } } }