Readarr/src/Marr.Data/Mapping/RelationshipBuilder.cs
ta264 bb02d73c42 Whole album matching and fingerprinting (#592)
* Cache result of GetAllArtists

* Fixed: Manual import not respecting album import notifications

* Fixed: partial album imports stay in queue, prompting manual import

* Fixed: Allow release if tracks are missing

* Fixed: Be tolerant of missing/extra "The" at start of artist name

* Improve manual import UI

* Omit video tracks from DB entirely

* Revert "faster test packaging in build.sh"

This reverts commit 2723e2a7b8.

-u and -T are not supported on macOS

* Fix tests on linux and macOS

* Actually lint on linux

On linux yarn runs scripts with sh not bash so ** doesn't recursively glob

* Match whole albums

* Option to disable fingerprinting

* Rip out MediaInfo

* Don't split up things that have the same album selected in manual import

* Try to speed up IndentificationService

* More speedups

* Some fixes and increase power of recording id

* Fix NRE when no tags

* Fix NRE when some (but not all) files in a directory have missing tags

* Bump taglib, tidy up tag parsing

* Add a health check

* Remove media info setting

* Tags -> audioTags

* Add some tests where tags are null

* Rename history events

* Add missing method to interface

* Reinstate MediaInfo tags and update info with artist scan

Also adds migration to remove old format media info

* This file no longer exists

* Don't penalise year if missing from tags

* Formatting improvements

* Use correct system newline

* Switch to the netstandard2.0 library to support net 461

* TagLib.File is IDisposable so should be in a using

* Improve filename matching and add tests

* Neater logging of parsed tags

* Fix disk scan tests for new media info update

* Fix quality detection source

* Fix Inexact Artist/Album match

* Add button to clear track mapping

* Fix warning

* Pacify eslint

* Use \ not /

* Fix UI updates

* Fix media covers

Prevent localizing URL propaging back to the metadata object

* Reduce database overhead broadcasting UI updates

* Relax timings a bit to make test pass

* Remove irrelevant tests

* Test framework for identification service

* Fix PreferMissingToBadMatch test case

* Make fingerprinting more robust

* More logging

* Penalize unknown media format and country

* Prefer USA to UK

* Allow Data CD

* Fix exception if fingerprinting fails for all files

* Fix tests

* Fix NRE

* Allow apostrophes and remove accents in filename aggregation

* Address codacy issues

* Cope with old versions of fpcalc and suggest upgrade

* fpcalc health check passes if fingerprinting disabled

* Get the Artist meta with the artist

* Fix the mapper so that lazy loaded lists will be populated on Join

And therefore we can join TrackFiles on Tracks by default and avoid an
extra query

* Rename subtitle -> lyric

* Tidy up MediaInfoFormatter
2019-02-16 09:49:24 -05:00

186 lines
6.4 KiB
C#

using System;
using System.Collections;
using System.Linq;
using System.Linq.Expressions;
using Marr.Data.Mapping.Strategies;
namespace Marr.Data.Mapping
{
/// <summary>
/// This class has fluent methods that are used to easily configure relationship mappings.
/// </summary>
/// <typeparam name="TEntity"></typeparam>
public class RelationshipBuilder<TEntity>
{
private FluentMappings.MappingsFluentEntity<TEntity> _fluentEntity;
private string _currentPropertyName;
public RelationshipBuilder(FluentMappings.MappingsFluentEntity<TEntity> fluentEntity, RelationshipCollection relationships)
{
_fluentEntity = fluentEntity;
Relationships = relationships;
}
/// <summary>
/// Gets the list of relationship mappings that are being configured.
/// </summary>
public RelationshipCollection Relationships { get; private set; }
#region - Fluent Methods -
/// <summary>
/// Initializes the configurator to configure the given property.
/// </summary>
/// <param name="property"></param>
/// <returns></returns>
public RelationshipBuilder<TEntity> For(Expression<Func<TEntity, object>> property)
{
return For(property.GetMemberName());
}
/// <summary>
/// Initializes the configurator to configure the given property or field.
/// </summary>
/// <param name="propertyName"></param>
/// <returns></returns>
public RelationshipBuilder<TEntity> For(string propertyName)
{
_currentPropertyName = propertyName;
// Try to add the relationship if it doesn't exist
if (Relationships[_currentPropertyName] == null)
{
TryAddRelationshipForField(_currentPropertyName);
}
return this;
}
/// <summary>
/// Sets a property to be lazy loaded, with a given query.
/// </summary>
/// <typeparam name="TChild"></typeparam>
/// <param name="query"></param>
/// <param name="condition">condition in which a child could exist. eg. avoid call to db if foreign key is 0 or null</param>
/// <returns></returns>
public RelationshipBuilder<TEntity> LazyLoad<TChild>(Func<IDataMapper, TEntity, TChild> query, Func<TEntity, bool> condition = null)
{
AssertCurrentPropertyIsSet();
var relationship = Relationships[_currentPropertyName];
relationship.LazyLoaded = new LazyLoaded<TEntity, TChild>(query, condition);
// work out if it's one to many or not
if (typeof(ICollection).IsAssignableFrom(typeof(TChild)))
{
relationship.RelationshipInfo.RelationType = RelationshipTypes.Many;
relationship.RelationshipInfo.EntityType = typeof(TChild).GetGenericArguments()[0];
}
else
{
relationship.RelationshipInfo.EntityType = typeof(TChild);
}
return this;
}
public RelationshipBuilder<TEntity> SetOneToOne()
{
AssertCurrentPropertyIsSet();
SetOneToOne(_currentPropertyName);
return this;
}
public RelationshipBuilder<TEntity> SetOneToOne(string propertyName)
{
Relationships[propertyName].RelationshipInfo.RelationType = RelationshipTypes.One;
return this;
}
public RelationshipBuilder<TEntity> SetOneToMany()
{
AssertCurrentPropertyIsSet();
SetOneToMany(_currentPropertyName);
return this;
}
public RelationshipBuilder<TEntity> SetOneToMany(string propertyName)
{
Relationships[propertyName].RelationshipInfo.RelationType = RelationshipTypes.Many;
return this;
}
public RelationshipBuilder<TEntity> Ignore(Expression<Func<TEntity, object>> property)
{
string propertyName = property.GetMemberName();
Relationships.RemoveAll(r => r.Member.Name == propertyName);
return this;
}
public FluentMappings.MappingsFluentTables<TEntity> Tables
{
get
{
if (_fluentEntity == null)
{
throw new Exception("This property is not compatible with the obsolete 'MapBuilder' class.");
}
return _fluentEntity.Table;
}
}
public FluentMappings.MappingsFluentColumns<TEntity> Columns
{
get
{
if (_fluentEntity == null)
{
throw new Exception("This property is not compatible with the obsolete 'MapBuilder' class.");
}
return _fluentEntity.Columns;
}
}
public FluentMappings.MappingsFluentEntity<TNewEntity> Entity<TNewEntity>()
{
return new FluentMappings.MappingsFluentEntity<TNewEntity>(true);
}
/// <summary>
/// Tries to add a Relationship for the given field name.
/// Throws and exception if field cannot be found.
/// </summary>
private void TryAddRelationshipForField(string fieldName)
{
// Set strategy to filter for public or private fields
ConventionMapStrategy strategy = new ConventionMapStrategy(false);
// Find the field that matches the given field name
strategy.RelationshipPredicate = mi => mi.Name == fieldName;
Relationship relationship = strategy.MapRelationships(typeof(TEntity)).FirstOrDefault();
if (relationship == null)
{
throw new DataMappingException(string.Format("Could not find the field '{0}' in '{1}'.",
fieldName,
typeof(TEntity).Name));
}
Relationships.Add(relationship);
}
/// <summary>
/// Throws an exception if the "current" property has not been set.
/// </summary>
private void AssertCurrentPropertyIsSet()
{
if (string.IsNullOrEmpty(_currentPropertyName))
{
throw new DataMappingException("A property must first be specified using the 'For' method.");
}
}
#endregion
}
}