Convert library items to an interface

This commit is contained in:
Zoe Roux 2023-11-18 23:44:40 +01:00
parent c5a2a05af6
commit ca6a4d8ab5
16 changed files with 86 additions and 238 deletions

View File

@ -31,7 +31,7 @@ namespace Kyoo.Abstractions.Controllers
/// <summary>
/// The repository that handle libraries items (a wrapper around shows and collections).
/// </summary>
IRepository<LibraryItem> LibraryItems { get; }
IRepository<ILibraryItem> LibraryItems { get; }
/// <summary>
/// The repository that handle collections.

View File

@ -35,10 +35,10 @@ public interface ISearchManager
/// <param name="pagination">How pagination should be done (where to start and how many to return)</param>
/// <param name="include">The related fields to include.</param>
/// <returns>A list of resources that match every filters</returns>
public Task<SearchPage<LibraryItem>.SearchResult> SearchItems(string? query,
Sort<LibraryItem> sortBy,
public Task<SearchPage<ILibraryItem>.SearchResult> SearchItems(string? query,
Sort<ILibraryItem> sortBy,
SearchPagination pagination,
Include<LibraryItem>? include = default);
Include<ILibraryItem>? include = default);
/// <summary>
/// Search for movies.

View File

@ -30,138 +30,20 @@ namespace Kyoo.Abstractions.Models
public enum ItemKind
{
/// <summary>
/// The <see cref="LibraryItem"/> is a <see cref="Show"/>.
/// The <see cref="ILibraryItem"/> is a <see cref="Show"/>.
/// </summary>
Show,
/// <summary>
/// The <see cref="LibraryItem"/> is a Movie.
/// The <see cref="ILibraryItem"/> is a Movie.
/// </summary>
Movie,
/// <summary>
/// The <see cref="LibraryItem"/> is a <see cref="Collection"/>.
/// The <see cref="ILibraryItem"/> is a <see cref="Collection"/>.
/// </summary>
Collection
}
public class LibraryItem : IResource, IThumbnails, IMetadata, IAddedDate
{
/// <inheritdoc />
public int Id { get; set; }
/// <inheritdoc />
[MaxLength(256)]
public string Slug { get; set; }
/// <summary>
/// The title of this show.
/// </summary>
public string Name { get; set; }
/// <summary>
/// A catchphrase for this movie.
/// </summary>
public string? Tagline { get; set; }
/// <summary>
/// The list of alternative titles of this show.
/// </summary>
public string[] Aliases { get; set; } = Array.Empty<string>();
/// <summary>
/// The path of the movie video file.
/// </summary>
public string? Path { get; set; }
/// <summary>
/// The summary of this show.
/// </summary>
public string? Overview { get; set; }
/// <summary>
/// A list of tags that match this movie.
/// </summary>
public string[] Tags { get; set; } = Array.Empty<string>();
/// <summary>
/// The list of genres (themes) this show has.
/// </summary>
public Genre[] Genres { get; set; } = Array.Empty<Genre>();
/// <summary>
/// Is this show airing, not aired yet or finished?
/// </summary>
public Status Status { get; set; }
/// <summary>
/// How well this item is rated? (from 0 to 100).
/// </summary>
public int Rating { get; set; }
/// <summary>
/// How long is this movie? (in minutes)
/// </summary>
public int? Runtime { get; set; }
/// <summary>
/// The date this show started airing. It can be null if this is unknown.
/// </summary>
public DateTime? StartAir { get; set; }
/// <summary>
/// The date this show finished airing.
/// It can also be null if this is unknown.
/// </summary>
public DateTime? EndAir { get; set; }
/// <summary>
/// The date this movie aired.
/// </summary>
public DateTime? AirDate { get; set; }
/// <inheritdoc />
public DateTime AddedDate { get; set; }
/// <inheritdoc />
public Image? Poster { get; set; }
/// <inheritdoc />
public Image? Thumbnail { get; set; }
/// <inheritdoc />
public Image? Logo { get; set; }
/// <summary>
/// A video of a few minutes that tease the content.
/// </summary>
public string? Trailer { get; set; }
/// <summary>
/// Is the item a collection, a movie or a show?
/// </summary>
public ItemKind Kind { get; set; }
/// <inheritdoc />
public Dictionary<string, MetadataId> ExternalId { get; set; } = new();
/// <summary>
/// Links to watch this movie.
/// </summary>
public VideoLinks? Links => Kind == ItemKind.Movie ? new()
{
Direct = $"/video/movie/{Slug}/direct",
Hls = $"/video/movie/{Slug}/master.m3u8",
}
: null;
public LibraryItem() { }
[JsonConstructor]
public LibraryItem(string name)
{
Slug = Utility.ToSlug(name);
Name = name;
}
}
public interface ILibraryItem : IResource, IThumbnails, IMetadata, IAddedDate { }
}

View File

@ -28,12 +28,12 @@ namespace Kyoo.Abstractions.Models
public enum NewsKind
{
/// <summary>
/// The <see cref="LibraryItem"/> is an <see cref="Episode"/>.
/// The <see cref="ILibraryItem"/> is an <see cref="Episode"/>.
/// </summary>
Episode,
/// <summary>
/// The <see cref="LibraryItem"/> is a Movie.
/// The <see cref="ILibraryItem"/> is a Movie.
/// </summary>
Movie,
}

View File

@ -28,7 +28,7 @@ namespace Kyoo.Abstractions.Models
/// <summary>
/// A class representing collections of <see cref="Show"/>.
/// </summary>
public class Collection : IResource, IMetadata, IThumbnails, IAddedDate
public class Collection : IResource, IMetadata, IThumbnails, IAddedDate, ILibraryItem
{
/// <inheritdoc />
public int Id { get; set; }

View File

@ -28,7 +28,7 @@ namespace Kyoo.Abstractions.Models
/// <summary>
/// A series or a movie.
/// </summary>
public class Movie : IResource, IMetadata, IOnMerge, IThumbnails, IAddedDate
public class Movie : IResource, IMetadata, IOnMerge, IThumbnails, IAddedDate, ILibraryItem
{
/// <inheritdoc />
public int Id { get; set; }

View File

@ -30,7 +30,7 @@ namespace Kyoo.Abstractions.Models
/// <summary>
/// A series or a movie.
/// </summary>
public class Show : IResource, IMetadata, IOnMerge, IThumbnails, IAddedDate
public class Show : IResource, IMetadata, IOnMerge, IThumbnails, IAddedDate, ILibraryItem
{
/// <inheritdoc />
public int Id { get; set; }

View File

@ -30,7 +30,7 @@ namespace Kyoo.Core.Controllers
private readonly IBaseRepository[] _repositories;
public LibraryManager(
IRepository<LibraryItem> libraryItemRepository,
IRepository<ILibraryItem> libraryItemRepository,
IRepository<Collection> collectionRepository,
IRepository<Movie> movieRepository,
IRepository<Show> showRepository,
@ -65,7 +65,7 @@ namespace Kyoo.Core.Controllers
}
/// <inheritdoc />
public IRepository<LibraryItem> LibraryItems { get; }
public IRepository<ILibraryItem> LibraryItems { get; }
/// <inheritdoc />
public IRepository<Collection> Collections { get; }

View File

@ -36,13 +36,11 @@ namespace Kyoo.Core.Controllers
/// <summary>
/// A local repository to handle library items.
/// </summary>
public class LibraryItemRepository : IRepository<LibraryItem>
public class LibraryItemRepository : IRepository<ILibraryItem>
{
private readonly DbConnection _database;
protected Sort<LibraryItem> DefaultSort => new Sort<LibraryItem>.By(x => x.Name);
public Type RepositoryType => typeof(LibraryItem);
public Type RepositoryType => typeof(ILibraryItem);
public LibraryItemRepository(DbConnection database)
{
@ -50,45 +48,45 @@ namespace Kyoo.Core.Controllers
}
/// <inheritdoc/>
public virtual async Task<LibraryItem> Get(int id, Include<LibraryItem>? include = default)
public virtual async Task<ILibraryItem> Get(int id, Include<ILibraryItem>? include = default)
{
LibraryItem? ret = await GetOrDefault(id, include);
ILibraryItem? ret = await GetOrDefault(id, include);
if (ret == null)
throw new ItemNotFoundException($"No {nameof(LibraryItem)} found with the id {id}");
throw new ItemNotFoundException($"No {nameof(ILibraryItem)} found with the id {id}");
return ret;
}
/// <inheritdoc/>
public virtual async Task<LibraryItem> Get(string slug, Include<LibraryItem>? include = default)
public virtual async Task<ILibraryItem> Get(string slug, Include<ILibraryItem>? include = default)
{
LibraryItem? ret = await GetOrDefault(slug, include);
ILibraryItem? ret = await GetOrDefault(slug, include);
if (ret == null)
throw new ItemNotFoundException($"No {nameof(LibraryItem)} found with the slug {slug}");
throw new ItemNotFoundException($"No {nameof(ILibraryItem)} found with the slug {slug}");
return ret;
}
/// <inheritdoc/>
public virtual async Task<LibraryItem> Get(
Expression<Func<LibraryItem, bool>> where,
Include<LibraryItem>? include = default)
public virtual async Task<ILibraryItem> Get(
Expression<Func<ILibraryItem, bool>> where,
Include<ILibraryItem>? include = default)
{
LibraryItem? ret = await GetOrDefault(where, include: include);
ILibraryItem? ret = await GetOrDefault(where, include: include);
if (ret == null)
throw new ItemNotFoundException($"No {nameof(LibraryItem)} found with the given predicate.");
throw new ItemNotFoundException($"No {nameof(ILibraryItem)} found with the given predicate.");
return ret;
}
public Task<LibraryItem?> GetOrDefault(int id, Include<LibraryItem>? include = null)
public Task<ILibraryItem?> GetOrDefault(int id, Include<ILibraryItem>? include = null)
{
throw new NotImplementedException();
}
public Task<LibraryItem?> GetOrDefault(string slug, Include<LibraryItem>? include = null)
public Task<ILibraryItem?> GetOrDefault(string slug, Include<ILibraryItem>? include = null)
{
throw new NotImplementedException();
}
public Task<LibraryItem?> GetOrDefault(Expression<Func<LibraryItem, bool>> where, Include<LibraryItem>? include = null, Sort<LibraryItem>? sortBy = null)
public Task<ILibraryItem?> GetOrDefault(Expression<Func<ILibraryItem, bool>> where, Include<ILibraryItem>? include = null, Sort<ILibraryItem>? sortBy = null)
{
throw new NotImplementedException();
}
@ -106,11 +104,11 @@ namespace Kyoo.Core.Controllers
};
}
public async Task<ICollection<LibraryItem>> GetAll(
Expression<Func<LibraryItem, bool>>? where = null,
Sort<LibraryItem>? sort = null,
public async Task<ICollection<ILibraryItem>> GetAll(
Expression<Func<ILibraryItem, bool>>? where = null,
Sort<ILibraryItem>? sort = null,
Pagination? limit = null,
Include<LibraryItem>? include = null)
Include<ILibraryItem>? include = null)
{
// language=PostgreSQL
IDapperSqlCommand query = _database.SqlBuilder($"""
@ -136,7 +134,8 @@ namespace Kyoo.Core.Controllers
limit {limit.Limit}
""").Build();
var data = await query.QueryAsync<IResource>(new[] { typeof(Show), typeof(Movie), typeof(Collection), typeof(Studio) }, items =>
Type[] types = new[] { typeof(Show), typeof(Movie), typeof(Collection), typeof(Studio) };
IEnumerable<ILibraryItem> data = await query.QueryAsync<ILibraryItem>(types, items =>
{
var studio = items[3] as Studio;
if (items[0] is Show show && show.Id != 0)
@ -147,44 +146,26 @@ namespace Kyoo.Core.Controllers
return collection;
throw new InvalidDataException();
});
// await using DbDataReader reader = await _database.ExecuteReaderAsync(sql);
// int kindOrdinal = reader.GetOrdinal("kind");
// var showParser = reader.GetRowParser<IResource>(typeof(Show));
// var movieParser = reader.GetRowParser<IResource>(typeof(Movie));
// var collectionParser = reader.GetRowParser<IResource>(typeof(Collection));
//
// while (await reader.ReadAsync())
// {
// ItemKind type = await reader.GetFieldValueAsync<ItemKind>(kindOrdinal);
// ret.Add(type switch
// {
// ItemKind.Show => showParser(reader),
// ItemKind.Movie => movieParser(reader),
// ItemKind.Collection => collectionParser(reader),
// _ => throw new InvalidDataException(),
// });
// }
throw new NotImplementedException();
// return ret;
return data.ToList();
}
public Task<int> GetCount(Expression<Func<LibraryItem, bool>>? where = null)
public Task<int> GetCount(Expression<Func<ILibraryItem, bool>>? where = null)
{
throw new NotImplementedException();
}
public Task<ICollection<LibraryItem>> FromIds(IList<int> ids, Include<LibraryItem>? include = null)
public Task<ICollection<ILibraryItem>> FromIds(IList<int> ids, Include<ILibraryItem>? include = null)
{
throw new NotImplementedException();
}
public Task DeleteAll(Expression<Func<LibraryItem, bool>> where)
public Task DeleteAll(Expression<Func<ILibraryItem, bool>> where)
{
throw new NotImplementedException();
}
/// <inheritdoc />
public async Task<ICollection<LibraryItem>> Search(string query, Include<LibraryItem>? include = default)
public async Task<ICollection<ILibraryItem>> Search(string query, Include<ILibraryItem>? include = default)
{
throw new NotImplementedException();
// return await Sort(
@ -195,12 +176,12 @@ namespace Kyoo.Core.Controllers
// .ToListAsync();
}
public async Task<ICollection<LibraryItem>> GetAllOfCollection(
public async Task<ICollection<ILibraryItem>> GetAllOfCollection(
Expression<Func<Collection, bool>> selector,
Expression<Func<LibraryItem, bool>>? where = null,
Sort<LibraryItem>? sort = default,
Expression<Func<ILibraryItem, bool>>? where = null,
Sort<ILibraryItem>? sort = default,
Pagination? limit = default,
Include<LibraryItem>? include = default)
Include<ILibraryItem>? include = default)
{
throw new NotImplementedException();
// return await ApplyFilters(
@ -220,19 +201,19 @@ namespace Kyoo.Core.Controllers
}
/// <inheritdoc />
public Task<LibraryItem> Create(LibraryItem obj)
public Task<ILibraryItem> Create(ILibraryItem obj)
=> throw new InvalidOperationException();
/// <inheritdoc />
public Task<LibraryItem> CreateIfNotExists(LibraryItem obj)
public Task<ILibraryItem> CreateIfNotExists(ILibraryItem obj)
=> throw new InvalidOperationException();
/// <inheritdoc />
public Task<LibraryItem> Edit(LibraryItem edited)
public Task<ILibraryItem> Edit(ILibraryItem edited)
=> throw new InvalidOperationException();
/// <inheritdoc />
public Task<LibraryItem> Patch(int id, Func<LibraryItem, Task<bool>> patch)
public Task<ILibraryItem> Patch(int id, Func<ILibraryItem, Task<bool>> patch)
=> throw new InvalidOperationException();
/// <inheritdoc />
@ -244,7 +225,7 @@ namespace Kyoo.Core.Controllers
=> throw new InvalidOperationException();
/// <inheritdoc />
public Task Delete(LibraryItem obj)
public Task Delete(ILibraryItem obj)
=> throw new InvalidOperationException();
}
}

View File

@ -119,7 +119,6 @@ namespace Kyoo.Core.Controllers
{
string directory = item switch
{
LibraryItem litem => Path.Combine("./metadata", litem.Kind.ToString().ToLowerInvariant(), litem.Slug),
IResource res => Path.Combine("./metadata", item.GetType().Name.ToLowerInvariant(), res.Slug),
_ => Path.Combine("./metadata", typeof(T).Name.ToLowerInvariant())
};

View File

@ -138,16 +138,16 @@ namespace Kyoo.Core.Api
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(RequestError))]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult<Page<LibraryItem>>> GetItems(Identifier identifier,
[FromQuery] Sort<LibraryItem> sortBy,
public async Task<ActionResult<Page<ILibraryItem>>> GetItems(Identifier identifier,
[FromQuery] Sort<ILibraryItem> sortBy,
[FromQuery] Dictionary<string, string> where,
[FromQuery] Pagination pagination,
[FromQuery] Include<LibraryItem>? fields)
[FromQuery] Include<ILibraryItem>? fields)
{
ICollection<LibraryItem> resources = await _items.GetAllOfCollection(
ICollection<ILibraryItem> resources = await _items.GetAllOfCollection(
identifier.IsSame<Collection>(),
ApiHelper.ParseWhere<LibraryItem>(where),
sortBy == new Sort<LibraryItem>.Default() ? new Sort<LibraryItem>.By(x => x.AirDate) : sortBy,
ApiHelper.ParseWhere<ILibraryItem>(where),
sortBy == new Sort<ILibraryItem>.Default() ? new Sort<ILibraryItem>.By(nameof(Movie.AirDate)) : sortBy,
pagination,
fields
);

View File

@ -35,12 +35,12 @@ namespace Kyoo.Core.Api
[ResourceView]
[PartialPermission("LibraryItem")]
[ApiDefinition("Items", Group = ResourcesGroup)]
public class LibraryItemApi : CrudThumbsApi<LibraryItem>
public class LibraryItemApi : CrudThumbsApi<ILibraryItem>
{
/// <summary>
/// The library item repository used to modify or retrieve information in the data store.
/// </summary>
private readonly IRepository<LibraryItem> _libraryItems;
private readonly IRepository<ILibraryItem> _libraryItems;
/// <summary>
/// Create a new <see cref="LibraryItemApi"/>.
@ -49,7 +49,7 @@ namespace Kyoo.Core.Api
/// The library item repository used to modify or retrieve information in the data store.
/// </param>
/// <param name="thumbs">Thumbnail manager to retrieve images.</param>
public LibraryItemApi(IRepository<LibraryItem> libraryItems, IThumbnailsManager thumbs)
public LibraryItemApi(IRepository<ILibraryItem> libraryItems, IThumbnailsManager thumbs)
: base(libraryItems, thumbs)
{
_libraryItems = libraryItems;

View File

@ -135,14 +135,14 @@ namespace Kyoo.Core.Api
/// <returns>A list of items found for the specified query.</returns>
[HttpGet("items")]
[HttpGet("item", Order = AlternativeRoute)]
[Permission(nameof(LibraryItem), Kind.Read)]
[Permission(nameof(ILibraryItem), Kind.Read)]
[ApiDefinition("Item")]
[ProducesResponseType(StatusCodes.Status200OK)]
public async Task<SearchPage<LibraryItem>> SearchItems(
public async Task<SearchPage<ILibraryItem>> SearchItems(
[FromQuery] string? q,
[FromQuery] Sort<LibraryItem> sortBy,
[FromQuery] Sort<ILibraryItem> sortBy,
[FromQuery] SearchPagination pagination,
[FromQuery] Include<LibraryItem> fields)
[FromQuery] Include<ILibraryItem> fields)
{
return SearchPage(await _searchManager.SearchItems(q, sortBy, pagination, fields));
}

View File

@ -41,32 +41,32 @@ namespace Kyoo.Meiliseach
{
SearchableAttributes = new[]
{
CamelCase.ConvertName(nameof(LibraryItem.Name)),
CamelCase.ConvertName(nameof(LibraryItem.Slug)),
CamelCase.ConvertName(nameof(LibraryItem.Aliases)),
CamelCase.ConvertName(nameof(LibraryItem.Path)),
CamelCase.ConvertName(nameof(LibraryItem.Tags)),
CamelCase.ConvertName(nameof(LibraryItem.Overview)),
CamelCase.ConvertName(nameof(Movie.Name)),
CamelCase.ConvertName(nameof(Movie.Slug)),
CamelCase.ConvertName(nameof(Movie.Aliases)),
CamelCase.ConvertName(nameof(Movie.Path)),
CamelCase.ConvertName(nameof(Movie.Tags)),
CamelCase.ConvertName(nameof(Movie.Overview)),
},
FilterableAttributes = new[]
{
CamelCase.ConvertName(nameof(LibraryItem.Genres)),
CamelCase.ConvertName(nameof(LibraryItem.Status)),
CamelCase.ConvertName(nameof(LibraryItem.AirDate)),
CamelCase.ConvertName(nameof(Movie.Genres)),
CamelCase.ConvertName(nameof(Movie.Status)),
CamelCase.ConvertName(nameof(Movie.AirDate)),
CamelCase.ConvertName(nameof(Movie.StudioId)),
CamelCase.ConvertName(nameof(LibraryItem.Kind)),
"kind"
},
SortableAttributes = new[]
{
CamelCase.ConvertName(nameof(LibraryItem.AirDate)),
CamelCase.ConvertName(nameof(LibraryItem.AddedDate)),
CamelCase.ConvertName(nameof(LibraryItem.Rating)),
CamelCase.ConvertName(nameof(LibraryItem.Runtime)),
CamelCase.ConvertName(nameof(Movie.AirDate)),
CamelCase.ConvertName(nameof(Movie.AddedDate)),
CamelCase.ConvertName(nameof(Movie.Rating)),
CamelCase.ConvertName(nameof(Movie.Runtime)),
},
DisplayedAttributes = new[]
{
CamelCase.ConvertName(nameof(LibraryItem.Id)),
CamelCase.ConvertName(nameof(LibraryItem.Kind)),
CamelCase.ConvertName(nameof(Movie.Id)),
"kind"
},
RankingRules = new[]
{
@ -76,10 +76,9 @@ namespace Kyoo.Meiliseach
"attribute",
"sort",
"exactness",
$"{CamelCase.ConvertName(nameof(LibraryItem.Rating))}:desc",
$"{CamelCase.ConvertName(nameof(Movie.Rating))}:desc",
}
// TODO: Add stopwords
// TODO: Extend default ranking to add ratings.
}
},
{

View File

@ -73,10 +73,10 @@ public class SearchManager : ISearchManager
};
}
public async Task<SearchPage<LibraryItem>.SearchResult> SearchItems(string? query,
Sort<LibraryItem> sortBy,
public async Task<SearchPage<ILibraryItem>.SearchResult> SearchItems(string? query,
Sort<ILibraryItem> sortBy,
SearchPagination pagination,
Include<LibraryItem>? include = default)
Include<ILibraryItem>? include = default)
{
// TODO: add filters and facets
ISearchable<IdResource> res = await _client.Index("items").SearchAsync<IdResource>(query, new SearchQuery()
@ -96,7 +96,7 @@ public class SearchManager : ISearchManager
_ => throw new InvalidOperationException("An unknown item kind was found in meilisearch"),
}).ToList();
return new SearchPage<LibraryItem>.SearchResult
return new SearchPage<ILibraryItem>.SearchResult
{
Query = query,
Items = await _libraryManager.LibraryItems

View File

@ -92,14 +92,6 @@ namespace Kyoo.Postgresql
/// </summary>
public DbSet<PeopleRole> PeopleRoles { get; set; }
/// <summary>
/// The list of library items (shows and collections that are part of a library - or the global one).
/// </summary>
/// <remarks>
/// This set is ready only, on most database this will be a view.
/// </remarks>
public DbSet<LibraryItem> LibraryItems { get; set; }
/// <summary>
/// The list of new items (episodes and movies).
/// </summary>
@ -291,7 +283,6 @@ namespace Kyoo.Postgresql
.WithMany("Users")
.UsingEntity(x => x.ToTable(LinkName<User, Show>()));
_HasMetadata<LibraryItem>(modelBuilder);
_HasMetadata<News>(modelBuilder);
_HasMetadata<Collection>(modelBuilder);
_HasMetadata<Movie>(modelBuilder);
@ -301,7 +292,6 @@ namespace Kyoo.Postgresql
_HasMetadata<People>(modelBuilder);
_HasMetadata<Studio>(modelBuilder);
_HasImages<LibraryItem>(modelBuilder);
_HasImages<News>(modelBuilder);
_HasImages<Collection>(modelBuilder);
_HasImages<Movie>(modelBuilder);
@ -310,7 +300,6 @@ namespace Kyoo.Postgresql
_HasImages<Episode>(modelBuilder);
_HasImages<People>(modelBuilder);
_HasAddedDate<LibraryItem>(modelBuilder);
_HasAddedDate<News>(modelBuilder);
_HasAddedDate<Collection>(modelBuilder);
_HasAddedDate<Movie>(modelBuilder);
@ -357,8 +346,6 @@ namespace Kyoo.Postgresql
modelBuilder.Entity<Movie>()
.Ignore(x => x.Links);
modelBuilder.Entity<LibraryItem>()
.Ignore(x => x.Links);
modelBuilder.Entity<News>()
.Ignore(x => x.Links);