WIP
|
@ -4,7 +4,7 @@ using Microsoft.AspNetCore.Authorization;
|
|||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using roadie.Library.Setttings;
|
||||
using Roadie.Library.Setttings;
|
||||
using Roadie.Library.Caching;
|
||||
using Roadie.Library.Data;
|
||||
using System;
|
||||
|
@ -20,8 +20,8 @@ namespace Roadie.Api.Controllers
|
|||
[Authorize]
|
||||
public class ArtistController : EntityControllerBase
|
||||
{
|
||||
public ArtistController(IRoadieDbContext roadieDbContext, ILoggerFactory logger, ICacheManager cacheManager, IConfiguration configuration, IRoadieSettings roadieSettings)
|
||||
: base(roadieDbContext, cacheManager, configuration, roadieSettings)
|
||||
public ArtistController(IRoadieDbContext RoadieDbContext, ILoggerFactory logger, ICacheManager cacheManager, IConfiguration configuration, IRoadieSettings RoadieSettings)
|
||||
: base(RoadieDbContext, cacheManager, configuration, RoadieSettings)
|
||||
{
|
||||
this._logger = logger.CreateLogger("RoadieApi.Controllers.ArtistController"); ;
|
||||
}
|
||||
|
@ -29,7 +29,7 @@ namespace Roadie.Api.Controllers
|
|||
[EnableQuery]
|
||||
public IActionResult Get()
|
||||
{
|
||||
return Ok(this._roadieDbContext.Artists.ProjectToType<models.Artist>());
|
||||
return Ok(this._RoadieDbContext.Artists.ProjectToType<models.Artist>());
|
||||
}
|
||||
|
||||
[HttpGet("{id}")]
|
||||
|
@ -40,7 +40,7 @@ namespace Roadie.Api.Controllers
|
|||
var key = id.ToString();
|
||||
var result = this._cacheManager.Get<models.Artist>(key, () =>
|
||||
{
|
||||
var d = this._roadieDbContext.Artists.FirstOrDefault(x => x.RoadieId == id);
|
||||
var d = this._RoadieDbContext.Artists.FirstOrDefault(x => x.RoadieId == id);
|
||||
if (d != null)
|
||||
{
|
||||
return d.Adapt<models.Artist>();
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using roadie.Library.Setttings;
|
||||
using Roadie.Library.Setttings;
|
||||
using Roadie.Library.Caching;
|
||||
using Roadie.Library.Data;
|
||||
|
||||
|
@ -12,17 +12,17 @@ namespace Roadie.Api.Controllers
|
|||
{
|
||||
protected readonly ICacheManager _cacheManager;
|
||||
protected readonly IConfiguration _configuration;
|
||||
protected readonly IRoadieDbContext _roadieDbContext;
|
||||
protected readonly IRoadieSettings _roadieSettings;
|
||||
protected readonly IRoadieDbContext _RoadieDbContext;
|
||||
protected readonly IRoadieSettings _RoadieSettings;
|
||||
|
||||
protected ILogger _logger;
|
||||
|
||||
public EntityControllerBase(IRoadieDbContext roadieDbContext, ICacheManager cacheManager, IConfiguration configuration, IRoadieSettings roadieSettings)
|
||||
public EntityControllerBase(IRoadieDbContext RoadieDbContext, ICacheManager cacheManager, IConfiguration configuration, IRoadieSettings RoadieSettings)
|
||||
{
|
||||
this._roadieDbContext = roadieDbContext;
|
||||
this._RoadieDbContext = RoadieDbContext;
|
||||
this._cacheManager = cacheManager;
|
||||
this._configuration = configuration;
|
||||
this._roadieSettings = roadieSettings;
|
||||
this._RoadieSettings = RoadieSettings;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -4,7 +4,7 @@ using Microsoft.AspNetCore.Authorization;
|
|||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using roadie.Library.Setttings;
|
||||
using Roadie.Library.Setttings;
|
||||
using Roadie.Library.Caching;
|
||||
using Roadie.Library.Data;
|
||||
using System;
|
||||
|
@ -19,8 +19,8 @@ namespace Roadie.Api.Controllers
|
|||
[Authorize]
|
||||
public class LabelController : EntityControllerBase
|
||||
{
|
||||
public LabelController(IRoadieDbContext roadieDbContext, ILoggerFactory logger, ICacheManager cacheManager, IConfiguration configuration, IRoadieSettings roadieSettings)
|
||||
: base(roadieDbContext, cacheManager, configuration, roadieSettings)
|
||||
public LabelController(IRoadieDbContext RoadieDbContext, ILoggerFactory logger, ICacheManager cacheManager, IConfiguration configuration, IRoadieSettings RoadieSettings)
|
||||
: base(RoadieDbContext, cacheManager, configuration, RoadieSettings)
|
||||
{
|
||||
this._logger = logger.CreateLogger("RoadieApi.Controllers.LabelController"); ;
|
||||
}
|
||||
|
@ -28,7 +28,7 @@ namespace Roadie.Api.Controllers
|
|||
[EnableQuery]
|
||||
public IActionResult Get()
|
||||
{
|
||||
return Ok(this._roadieDbContext.Labels.ProjectToType<models.Label>());
|
||||
return Ok(this._RoadieDbContext.Labels.ProjectToType<models.Label>());
|
||||
}
|
||||
|
||||
[HttpGet("{id}")]
|
||||
|
@ -39,7 +39,7 @@ namespace Roadie.Api.Controllers
|
|||
var key = id.ToString();
|
||||
var result = this._cacheManager.Get<models.Label>(key, () =>
|
||||
{
|
||||
var d = this._roadieDbContext.Labels.FirstOrDefault(x => x.RoadieId == id);
|
||||
var d = this._RoadieDbContext.Labels.FirstOrDefault(x => x.RoadieId == id);
|
||||
if (d != null)
|
||||
{
|
||||
return d.Adapt<models.Label>();
|
||||
|
|
|
@ -5,7 +5,7 @@ using Microsoft.AspNetCore.Mvc;
|
|||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using roadie.Library.Setttings;
|
||||
using Roadie.Library.Setttings;
|
||||
using Roadie.Library.Caching;
|
||||
using Roadie.Library.Data;
|
||||
using System;
|
||||
|
@ -20,8 +20,8 @@ namespace Roadie.Api.Controllers
|
|||
[Authorize]
|
||||
public class ReleaseController : EntityControllerBase
|
||||
{
|
||||
public ReleaseController(IRoadieDbContext roadieDbContext, ILoggerFactory logger, ICacheManager cacheManager, IConfiguration configuration, IRoadieSettings roadieSettings)
|
||||
: base(roadieDbContext, cacheManager, configuration, roadieSettings)
|
||||
public ReleaseController(IRoadieDbContext RoadieDbContext, ILoggerFactory logger, ICacheManager cacheManager, IConfiguration configuration, IRoadieSettings RoadieSettings)
|
||||
: base(RoadieDbContext, cacheManager, configuration, RoadieSettings)
|
||||
{
|
||||
this._logger = logger.CreateLogger("RoadieApi.Controllers.ReleaseController"); ;
|
||||
}
|
||||
|
@ -29,7 +29,7 @@ namespace Roadie.Api.Controllers
|
|||
[EnableQuery]
|
||||
public IActionResult Get()
|
||||
{
|
||||
return Ok(this._roadieDbContext.Releases.ProjectToType<models.Release>());
|
||||
return Ok(this._RoadieDbContext.Releases.ProjectToType<models.Release>());
|
||||
}
|
||||
|
||||
[HttpGet("{id}")]
|
||||
|
@ -40,7 +40,7 @@ namespace Roadie.Api.Controllers
|
|||
var key = id.ToString();
|
||||
var result = this._cacheManager.Get<models.Release>(key, () =>
|
||||
{
|
||||
var d = this._roadieDbContext
|
||||
var d = this._RoadieDbContext
|
||||
.Releases
|
||||
.Include(x => x.Artist)
|
||||
.Include(x => x.Labels).Include("Labels.Label")
|
||||
|
|
|
@ -4,7 +4,7 @@ using Microsoft.AspNetCore.Authorization;
|
|||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using roadie.Library.Setttings;
|
||||
using Roadie.Library.Setttings;
|
||||
using Roadie.Library.Caching;
|
||||
using Roadie.Library.Data;
|
||||
using System;
|
||||
|
@ -19,8 +19,8 @@ namespace Roadie.Api.Controllers
|
|||
[Authorize]
|
||||
public class TrackController : EntityControllerBase
|
||||
{
|
||||
public TrackController(IRoadieDbContext roadieDbContext, ILoggerFactory logger, ICacheManager cacheManager, IConfiguration configuration, IRoadieSettings roadieSettings)
|
||||
: base(roadieDbContext, cacheManager, configuration, roadieSettings)
|
||||
public TrackController(IRoadieDbContext RoadieDbContext, ILoggerFactory logger, ICacheManager cacheManager, IConfiguration configuration, IRoadieSettings RoadieSettings)
|
||||
: base(RoadieDbContext, cacheManager, configuration, RoadieSettings)
|
||||
{
|
||||
this._logger = logger.CreateLogger("RoadieApi.Controllers.TrackController"); ;
|
||||
}
|
||||
|
@ -28,7 +28,7 @@ namespace Roadie.Api.Controllers
|
|||
[EnableQuery]
|
||||
public IActionResult Get()
|
||||
{
|
||||
return Ok(this._roadieDbContext.Tracks.ProjectToType<models.Track>());
|
||||
return Ok(this._RoadieDbContext.Tracks.ProjectToType<models.Track>());
|
||||
}
|
||||
|
||||
[HttpGet("{id}")]
|
||||
|
@ -39,7 +39,7 @@ namespace Roadie.Api.Controllers
|
|||
var key = id.ToString();
|
||||
var result = this._cacheManager.Get<models.Track>(key, () =>
|
||||
{
|
||||
var d = this._roadieDbContext.Tracks.FirstOrDefault(x => x.RoadieId == id);
|
||||
var d = this._RoadieDbContext.Tracks.FirstOrDefault(x => x.RoadieId == id);
|
||||
if (d != null)
|
||||
{
|
||||
return d.Adapt<models.Track>();
|
||||
|
|
BIN
RoadieApi/Images/artist.gif
Normal file
After Width: | Height: | Size: 1.6 KiB |
BIN
RoadieApi/Images/collection.gif
Normal file
After Width: | Height: | Size: 7.6 KiB |
BIN
RoadieApi/Images/image-not-found.gif
Normal file
After Width: | Height: | Size: 16 KiB |
BIN
RoadieApi/Images/label.gif
Normal file
After Width: | Height: | Size: 3.6 KiB |
BIN
RoadieApi/Images/release.gif
Normal file
After Width: | Height: | Size: 3.2 KiB |
BIN
RoadieApi/Images/track.gif
Normal file
After Width: | Height: | Size: 1.5 KiB |
BIN
RoadieApi/Images/user.gif
Normal file
After Width: | Height: | Size: 48 KiB |
|
@ -6,6 +6,7 @@
|
|||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="Images\" />
|
||||
<Folder Include="wwwroot\" />
|
||||
</ItemGroup>
|
||||
|
||||
|
|
22
RoadieApi/Services/HttpEncoder.cs
Normal file
|
@ -0,0 +1,22 @@
|
|||
using Roadie.Library.Encoding;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using System.Web;
|
||||
|
||||
namespace Roadie.Api.Services
|
||||
{
|
||||
public class HttpEncoder : IHttpEncoder
|
||||
{
|
||||
public string UrlDecode(string s)
|
||||
{
|
||||
return HttpUtility.UrlDecode(s);
|
||||
}
|
||||
|
||||
public string UrlEncode(string s)
|
||||
{
|
||||
return HttpUtility.UrlEncode(s);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -13,7 +13,7 @@ using Microsoft.Extensions.Logging;
|
|||
using Microsoft.IdentityModel.Tokens;
|
||||
using Microsoft.OData.Edm;
|
||||
using Newtonsoft.Json;
|
||||
using roadie.Library.Setttings;
|
||||
using Roadie.Library.Setttings;
|
||||
using Roadie.Api.Services;
|
||||
using Roadie.Library.Caching;
|
||||
using Roadie.Library.Data;
|
||||
|
@ -22,6 +22,7 @@ using System;
|
|||
using System.IO;
|
||||
using System.Reflection;
|
||||
using models = Roadie.Api.Data.Models;
|
||||
using Roadie.Library.Encoding;
|
||||
|
||||
namespace Roadie.Api
|
||||
{
|
||||
|
@ -86,6 +87,8 @@ namespace Roadie.Api
|
|||
|
||||
services.AddSingleton<ITokenService, TokenService>();
|
||||
|
||||
services.AddSingleton<IHttpEncoder, HttpEncoder>();
|
||||
|
||||
services.AddSingleton<IRoadieSettings, RoadieSettings>(options =>
|
||||
{
|
||||
var settingsPath = Path.Combine(AssemblyDirectory, "settings.json");
|
||||
|
|
|
@ -22,7 +22,7 @@
|
|||
"Audience": "http://localhost:5500"
|
||||
},
|
||||
"ConnectionStrings": {
|
||||
"RoadieDatabaseConnection": "server=voyager;userid=roadie;password=MenAtW0rk668;persistsecurityinfo=True;database=roadie;ConvertZeroDateTime=true"
|
||||
"RoadieDatabaseConnection": "server=voyager;userid=Roadie;password=MenAtW0rk668;persistsecurityinfo=True;database=Roadie;ConvertZeroDateTime=true"
|
||||
},
|
||||
"Settings": {
|
||||
"SecretKey": "a9kf^!y@rd-ci0&7l#da6ko$(l@_$9(y^r^a@2j+8!7zpk!zw88wi069",
|
||||
|
|
|
@ -4,7 +4,7 @@ using System.Linq;
|
|||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace roadie.Library.Setttings
|
||||
namespace Roadie.Library.Setttings
|
||||
{
|
||||
/// <summary>
|
||||
/// This is a Api Key used by Roadie to interact with an API (ie KeyName is "BingImageSearch" and its key is the BingImageSearch Key)
|
||||
|
|
|
@ -4,7 +4,7 @@ using System.Linq;
|
|||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace roadie.Library.Setttings
|
||||
namespace Roadie.Library.Setttings
|
||||
{
|
||||
[Serializable]
|
||||
public class Converting
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
using System.Collections.Generic;
|
||||
|
||||
namespace roadie.Library.Setttings
|
||||
namespace Roadie.Library.Setttings
|
||||
{
|
||||
public interface IRoadieSettings
|
||||
{
|
||||
|
|
|
@ -5,7 +5,7 @@ using System.Linq;
|
|||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace roadie.Library.Setttings
|
||||
namespace Roadie.Library.Setttings
|
||||
{
|
||||
public class Integrations
|
||||
{
|
||||
|
|
|
@ -4,7 +4,7 @@ using System.Linq;
|
|||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace roadie.Library.Setttings
|
||||
namespace Roadie.Library.Setttings
|
||||
{
|
||||
[Serializable]
|
||||
public class Processing
|
||||
|
|
|
@ -4,7 +4,7 @@ using System.Linq;
|
|||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace roadie.Library.Setttings
|
||||
namespace Roadie.Library.Setttings
|
||||
{
|
||||
[Serializable]
|
||||
public class RedisCache
|
||||
|
|
|
@ -4,7 +4,7 @@ using System.Linq;
|
|||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace roadie.Library.Setttings
|
||||
namespace Roadie.Library.Setttings
|
||||
{
|
||||
[Serializable]
|
||||
public sealed class RoadieSettings : IRoadieSettings
|
||||
|
|
|
@ -4,7 +4,7 @@ using System.Linq;
|
|||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace roadie.Library.Setttings
|
||||
namespace Roadie.Library.Setttings
|
||||
{
|
||||
[Serializable]
|
||||
public class Thumbnails
|
||||
|
|
|
@ -16,7 +16,7 @@ namespace Roadie.Library.Data
|
|||
|
||||
[Column("genreId")]
|
||||
[Required]
|
||||
public int GenreId { get; set; }
|
||||
public int? GenreId { get; set; }
|
||||
|
||||
[Column("id")]
|
||||
[Key]
|
||||
|
|
65
RoadieLibrary/Data/ArtistPartial.cs
Normal file
|
@ -0,0 +1,65 @@
|
|||
using Microsoft.Extensions.Configuration;
|
||||
using Roadie.Library.Utility;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace Roadie.Library.Data
|
||||
{
|
||||
public partial class Artist
|
||||
{
|
||||
public string CacheRegion
|
||||
{
|
||||
get
|
||||
{
|
||||
return string.Format("urn:artist:{0}", this.RoadieId);
|
||||
}
|
||||
}
|
||||
|
||||
public string Etag
|
||||
{
|
||||
get
|
||||
{
|
||||
using (var md5 = System.Security.Cryptography.MD5.Create())
|
||||
{
|
||||
return String.Concat(md5.ComputeHash(Encoding.Default.GetBytes(string.Format("{0}{1}", this.RoadieId, this.LastUpdated))).Select(x => x.ToString("D2")));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsValid
|
||||
{
|
||||
get
|
||||
{
|
||||
return !string.IsNullOrEmpty(this.Name);
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsNew
|
||||
{
|
||||
get
|
||||
{
|
||||
return this.Id < 1;
|
||||
}
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return string.Format("Id [{0}], Name [{1}], SortName [{2}], RoadieId [{3}]", this.Id, this.Name, this.SortNameValue, this.RoadieId);
|
||||
}
|
||||
|
||||
public string SortNameValue
|
||||
{
|
||||
get
|
||||
{
|
||||
return string.IsNullOrEmpty(this.SortName) ? this.Name : this.SortName;
|
||||
}
|
||||
}
|
||||
|
||||
public string ArtistFileFolder(IConfiguration configuration, string destinationRoot)
|
||||
{
|
||||
return FolderPathHelper.ArtistPath(configuration, this.SortNameValue, destinationRoot);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -21,11 +21,18 @@ namespace Roadie.Library.Data
|
|||
[Required]
|
||||
public DateTime? LastUpdated { get; set; }
|
||||
|
||||
[Column("roadieId")]
|
||||
[Column("RoadieId")]
|
||||
[Required]
|
||||
public Guid RoadieId { get; set; }
|
||||
|
||||
[Column("status", TypeName = "enum")]
|
||||
public Statuses Status { get; set; }
|
||||
|
||||
public EntityBase()
|
||||
{
|
||||
this.RoadieId = Guid.NewGuid();
|
||||
this.Status = Statuses.Incomplete;
|
||||
this.CreatedDate = DateTime.UtcNow;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,9 +1,16 @@
|
|||
using Microsoft.EntityFrameworkCore;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.ChangeTracking;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Internal;
|
||||
using Roadie.Library.Identity;
|
||||
|
||||
namespace Roadie.Library.Data
|
||||
{
|
||||
public interface IRoadieDbContext
|
||||
public interface IRoadieDbContext : IDisposable, IInfrastructure<IServiceProvider>, IDbContextDependencies, IDbSetCache, IDbQueryCache, IDbContextPoolable
|
||||
{
|
||||
DbSet<ArtistAssociation> ArtistAssociations { get; set; }
|
||||
DbSet<ArtistGenre> ArtistGenres { get; set; }
|
||||
|
@ -27,5 +34,45 @@ namespace Roadie.Library.Data
|
|||
DbSet<UserRelease> UserReleases { get; set; }
|
||||
DbSet<ApplicationUser> Users { get; set; }
|
||||
DbSet<UserTrack> UserTracks { get; set; }
|
||||
|
||||
DatabaseFacade Database { get; }
|
||||
ChangeTracker ChangeTracker { get; }
|
||||
EntityEntry Add(object entity);
|
||||
EntityEntry<TEntity> Add<TEntity>(TEntity entity) where TEntity : class;
|
||||
Task<EntityEntry> AddAsync(object entity, CancellationToken cancellationToken = default(CancellationToken));
|
||||
Task<EntityEntry<TEntity>> AddAsync<TEntity>(TEntity entity, CancellationToken cancellationToken = default(CancellationToken)) where TEntity : class;
|
||||
void AddRange(IEnumerable<object> entities);
|
||||
void AddRange(params object[] entities);
|
||||
Task AddRangeAsync(IEnumerable<object> entities, CancellationToken cancellationToken = default(CancellationToken));
|
||||
Task AddRangeAsync(params object[] entities);
|
||||
EntityEntry<TEntity> Attach<TEntity>(TEntity entity) where TEntity : class;
|
||||
EntityEntry Attach(object entity);
|
||||
void AttachRange(params object[] entities);
|
||||
void AttachRange(IEnumerable<object> entities);
|
||||
EntityEntry<TEntity> Entry<TEntity>(TEntity entity) where TEntity : class;
|
||||
EntityEntry Entry(object entity);
|
||||
bool Equals(object obj);
|
||||
object Find(Type entityType, params object[] keyValues);
|
||||
TEntity Find<TEntity>(params object[] keyValues) where TEntity : class;
|
||||
Task<TEntity> FindAsync<TEntity>(params object[] keyValues) where TEntity : class;
|
||||
Task<object> FindAsync(Type entityType, object[] keyValues, CancellationToken cancellationToken);
|
||||
Task<TEntity> FindAsync<TEntity>(object[] keyValues, CancellationToken cancellationToken) where TEntity : class;
|
||||
Task<object> FindAsync(Type entityType, params object[] keyValues);
|
||||
int GetHashCode();
|
||||
DbQuery<TQuery> Query<TQuery>() where TQuery : class;
|
||||
EntityEntry Remove(object entity);
|
||||
EntityEntry<TEntity> Remove<TEntity>(TEntity entity) where TEntity : class;
|
||||
void RemoveRange(IEnumerable<object> entities);
|
||||
void RemoveRange(params object[] entities);
|
||||
int SaveChanges(bool acceptAllChangesOnSuccess);
|
||||
int SaveChanges();
|
||||
Task<int> SaveChangesAsync(bool acceptAllChangesOnSuccess, CancellationToken cancellationToken = default(CancellationToken));
|
||||
Task<int> SaveChangesAsync(CancellationToken cancellationToken = default(CancellationToken));
|
||||
DbSet<TEntity> Set<TEntity>() where TEntity : class;
|
||||
string ToString();
|
||||
EntityEntry Update(object entity);
|
||||
EntityEntry<TEntity> Update<TEntity>(TEntity entity) where TEntity : class;
|
||||
void UpdateRange(params object[] entities);
|
||||
void UpdateRange(IEnumerable<object> entities);
|
||||
}
|
||||
}
|
30
RoadieLibrary/Data/ImagePartial.cs
Normal file
|
@ -0,0 +1,30 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace Roadie.Library.Data
|
||||
{
|
||||
public partial class Image
|
||||
{
|
||||
public string Etag
|
||||
{
|
||||
get
|
||||
{
|
||||
using (var md5 = System.Security.Cryptography.MD5.Create())
|
||||
{
|
||||
return String.Concat(md5.ComputeHash(Encoding.Default.GetBytes(string.Format("{0}{1}", this.RoadieId, this.LastUpdated))).Select(x => x.ToString("D2")));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public string GenerateSignature()
|
||||
{
|
||||
if (this.Bytes == null || !this.Bytes.Any())
|
||||
{
|
||||
return null;
|
||||
}
|
||||
return ImageHashing.AverageHash(this.image1).ToString();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -19,6 +19,10 @@ namespace Roadie.Library.Data
|
|||
[MaxLength(65535)]
|
||||
public string Profile { get; set; }
|
||||
|
||||
[Column("imageUrl")]
|
||||
[MaxLength(500)]
|
||||
public string ImageUrl { get; set; }
|
||||
|
||||
public List<ReleaseLabel> ReleaseLabels { get; set; }
|
||||
}
|
||||
}
|
34
RoadieLibrary/Data/LabelPartial.cs
Normal file
|
@ -0,0 +1,34 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace Roadie.Library.Data
|
||||
{
|
||||
public partial class Label
|
||||
{
|
||||
public string CacheRegion
|
||||
{
|
||||
get
|
||||
{
|
||||
return string.Format("urn:label:{0}", this.RoadieId);
|
||||
}
|
||||
}
|
||||
public string Etag
|
||||
{
|
||||
get
|
||||
{
|
||||
using (var md5 = System.Security.Cryptography.MD5.Create())
|
||||
{
|
||||
return String.Concat(md5.ComputeHash(Encoding.Default.GetBytes(string.Format("{0}{1}", this.RoadieId, this.LastUpdated))).Select(x => x.ToString("D2")));
|
||||
}
|
||||
}
|
||||
}
|
||||
public bool IsValid
|
||||
{
|
||||
get
|
||||
{
|
||||
return !string.IsNullOrEmpty(this.Name);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -21,6 +21,9 @@ namespace Roadie.Library.Data
|
|||
[MaxLength(65535)]
|
||||
public string Tags { get; set; }
|
||||
|
||||
[Column("thumbnail", TypeName = "blob")]
|
||||
public byte[] Thumbnail { get; set; }
|
||||
|
||||
[Column("urls", TypeName = "text")]
|
||||
[MaxLength(65535)]
|
||||
public string URLs { get; set; }
|
||||
|
|
|
@ -7,7 +7,7 @@ using System.ComponentModel.DataAnnotations.Schema;
|
|||
namespace Roadie.Library.Data
|
||||
{
|
||||
[Table("release")]
|
||||
public partial class Release : EntityBase
|
||||
public partial class Release : NamedEntityBase
|
||||
{
|
||||
[Column("amgId")]
|
||||
[MaxLength(50)]
|
||||
|
@ -63,8 +63,7 @@ namespace Roadie.Library.Data
|
|||
public string Profile { get; set; }
|
||||
|
||||
[Column("releaseDate")]
|
||||
[Required]
|
||||
public DateTime ReleaseDate { get; set; }
|
||||
public DateTime? ReleaseDate { get; set; }
|
||||
|
||||
[Column("releaseType")]
|
||||
public ReleaseType? ReleaseType { get; set; }
|
||||
|
@ -76,9 +75,6 @@ namespace Roadie.Library.Data
|
|||
[Column("submissionId")]
|
||||
public int? SubmissionId { get; set; }
|
||||
|
||||
[Column("thumbnail", TypeName = "blob")]
|
||||
public byte[] Thumbnail { get; set; }
|
||||
|
||||
[MaxLength(250)]
|
||||
[Column("title")]
|
||||
[Required]
|
||||
|
|
|
@ -10,7 +10,7 @@ namespace Roadie.Library.Data
|
|||
|
||||
[Column("genreId")]
|
||||
[Required]
|
||||
public int GenreId { get; set; }
|
||||
public int? GenreId { get; set; }
|
||||
|
||||
[Column("id")]
|
||||
[Key]
|
||||
|
|
140
RoadieLibrary/Data/ReleasePartial.cs
Normal file
|
@ -0,0 +1,140 @@
|
|||
using Microsoft.Extensions.Configuration;
|
||||
using Roadie.Library.Extensions;
|
||||
using Roadie.Library.Utility;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace Roadie.Library.Data
|
||||
{
|
||||
public partial class Release
|
||||
{
|
||||
public string CacheRegion
|
||||
{
|
||||
get
|
||||
{
|
||||
return string.Format("urn:release:{0}", this.RoadieId);
|
||||
}
|
||||
}
|
||||
public string Etag
|
||||
{
|
||||
get
|
||||
{
|
||||
using (var md5 = System.Security.Cryptography.MD5.Create())
|
||||
{
|
||||
return String.Concat(md5.ComputeHash(System.Text.Encoding.Default.GetBytes(string.Format("{0}{1}", this.RoadieId, this.LastUpdated))).Select(x => x.ToString("D2")));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsValid
|
||||
{
|
||||
get
|
||||
{
|
||||
return !string.IsNullOrEmpty(this.Title) && this.ReleaseDate > DateTime.MinValue;
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsSoundTrack
|
||||
{
|
||||
get
|
||||
{
|
||||
if (string.IsNullOrEmpty(this.Title))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
foreach (var soundTrackTrigger in new List<string> { "soundtrack", " ost", "(ost)" })
|
||||
{
|
||||
if (this.IsReleaseTypeOf(soundTrackTrigger))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsCastRecording
|
||||
{
|
||||
get
|
||||
{
|
||||
if (string.IsNullOrEmpty(this.Title))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return this.IsReleaseTypeOf("Original Broadway Cast") || this.IsReleaseTypeOf("Original Cast");
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsReleaseTypeOf(string type, bool doCheckTitles = false)
|
||||
{
|
||||
if (string.IsNullOrEmpty(type))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
try
|
||||
{
|
||||
if (doCheckTitles)
|
||||
{
|
||||
if (this.Artist != null && !string.IsNullOrEmpty(this.Artist.Name))
|
||||
{
|
||||
if (this.Artist.Name.IndexOf(type, 0, StringComparison.OrdinalIgnoreCase) > -1)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if (!string.IsNullOrEmpty(this.Title))
|
||||
{
|
||||
if (this.Title.IndexOf(type, 0, StringComparison.OrdinalIgnoreCase) > -1)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if (this.AlternateNames != null)
|
||||
{
|
||||
if (this.AlternateNames.IsValueInDelimitedList(type))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (this.Tags != null)
|
||||
{
|
||||
if (this.Tags.IsValueInDelimitedList(type))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if (this.Genres != null)
|
||||
{
|
||||
if (this.Genres.Any(x => x.Genre.Name.ToLower().Equals(type)))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
return false;
|
||||
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return string.Format("Id [{0}], Title [{1}], Release Date [{2}]", this.Id, this.Title, this.ReleaseDate);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Return this releases file folder for the given artist folder
|
||||
/// </summary>
|
||||
/// <param name="artistFolder"></param>
|
||||
/// <returns></returns>
|
||||
public string ReleaseFileFolder(string artistFolder)
|
||||
{
|
||||
return FolderPathHelper.ReleasePath(artistFolder, this.Title, this.ReleaseDate);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
|
@ -21,7 +21,7 @@ namespace Roadie.Library.Data
|
|||
public int? ArtistId { get; set; }
|
||||
|
||||
[Column("duration")]
|
||||
public int Duration { get; set; }
|
||||
public int? Duration { get; set; }
|
||||
|
||||
[Column("fileName")]
|
||||
[MaxLength(500)]
|
||||
|
@ -32,7 +32,7 @@ namespace Roadie.Library.Data
|
|||
public string FilePath { get; set; }
|
||||
|
||||
[Column("fileSize")]
|
||||
public int FileSize { get; set; }
|
||||
public int? FileSize { get; set; }
|
||||
|
||||
[Column("hash")]
|
||||
[MaxLength(32)]
|
||||
|
@ -58,7 +58,7 @@ namespace Roadie.Library.Data
|
|||
public string PartTitles { get; set; }
|
||||
|
||||
[Column("playedCount")]
|
||||
public int PlayedCount { get; set; }
|
||||
public int? PlayedCount { get; set; }
|
||||
|
||||
[Column("rating")]
|
||||
public short Rating { get; set; }
|
||||
|
@ -76,6 +76,9 @@ namespace Roadie.Library.Data
|
|||
[MaxLength(65535)]
|
||||
public string Tags { get; set; }
|
||||
|
||||
[Column("thumbnail", TypeName = "blob")]
|
||||
public byte[] Thumbnail { get; set; }
|
||||
|
||||
[MaxLength(250)]
|
||||
[Column("title")]
|
||||
[Required]
|
||||
|
|
74
RoadieLibrary/Data/TrackPartial.cs
Normal file
|
@ -0,0 +1,74 @@
|
|||
using Microsoft.Extensions.Configuration;
|
||||
using Roadie.Library.Enums;
|
||||
using Roadie.Library.Utility;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace Roadie.Library.Data
|
||||
{
|
||||
public partial class Track
|
||||
{
|
||||
[NotMapped]
|
||||
public IEnumerable<string> TrackArtists { get; set; }
|
||||
|
||||
[NotMapped]
|
||||
public Artist TrackArtist { get; set; }
|
||||
|
||||
public bool IsValid
|
||||
{
|
||||
get
|
||||
{
|
||||
return !string.IsNullOrEmpty(this.Hash);
|
||||
}
|
||||
}
|
||||
|
||||
public string CacheRegion
|
||||
{
|
||||
get
|
||||
{
|
||||
return string.Format("urn:track:{0}", this.RoadieId);
|
||||
}
|
||||
}
|
||||
public string Etag
|
||||
{
|
||||
get
|
||||
{
|
||||
using (var md5 = System.Security.Cryptography.MD5.Create())
|
||||
{
|
||||
return String.Concat(md5.ComputeHash(System.Text.Encoding.Default.GetBytes(string.Format("{0}{1}", this.RoadieId, this.LastUpdated))).Select(x => x.ToString("D2")));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a full file path to the current track
|
||||
/// </summary>
|
||||
public string PathToTrack(IConfiguration configuration, string libraryFolder)
|
||||
{
|
||||
return FolderPathHelper.PathForTrack(configuration, this, libraryFolder);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Update any file related columns to indicate this track file is missing
|
||||
/// </summary>
|
||||
/// <param name="now">Optional datetime to mark for Updated, if missing defaults to UtcNow</param>
|
||||
public void UpdateTrackMissingFile(DateTime? now = null)
|
||||
{
|
||||
this.Hash = null;
|
||||
this.Status = Statuses.Missing;
|
||||
this.FileName = null;
|
||||
this.FileSize = null;
|
||||
this.FilePath = null;
|
||||
this.LastUpdated = now ?? DateTime.UtcNow;
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return string.Format("Id [{0}], TrackNumber [{1}], Title [{2}]", this.Id, this.TrackNumber, this.Title);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
12
RoadieLibrary/Encoding/IHttpEncoder.cs
Normal file
|
@ -0,0 +1,12 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace Roadie.Library.Encoding
|
||||
{
|
||||
public interface IHttpEncoder
|
||||
{
|
||||
string UrlEncode(string s);
|
||||
string UrlDecode(string s);
|
||||
}
|
||||
}
|
33
RoadieLibrary/Extensions/ByteExt.cs
Normal file
|
@ -0,0 +1,33 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace Roadie.Library.Extensions
|
||||
{
|
||||
public static class ByteExt
|
||||
{
|
||||
public static int ComputeHash(this byte[] data)
|
||||
{
|
||||
if (data == null || data.Length == 0)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
unchecked
|
||||
{
|
||||
const int p = 16777619;
|
||||
int hash = (int)2166136261;
|
||||
|
||||
for (int i = 0; i < data.Length; i++)
|
||||
{
|
||||
hash = (hash ^ data[i]) * p;
|
||||
}
|
||||
hash += hash << 13;
|
||||
hash ^= hash >> 7;
|
||||
hash += hash << 3;
|
||||
hash ^= hash >> 17;
|
||||
hash += hash << 5;
|
||||
return hash;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
36
RoadieLibrary/Extensions/DateTimeExt.cs
Normal file
|
@ -0,0 +1,36 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Roadie.Library.Extensions
|
||||
{
|
||||
public static class DateTimeExt
|
||||
{
|
||||
public static DateTime? FormatDateTime(this DateTime? value)
|
||||
{
|
||||
if(!value.HasValue)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
if(value.Value.Year == 0 || value.Value.Year == 1)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
public static DateTime FromUnixTime(this long unixTime)
|
||||
{
|
||||
var epoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
|
||||
return epoch.AddSeconds(unixTime);
|
||||
}
|
||||
|
||||
public static long ToUnixTime(this DateTime date)
|
||||
{
|
||||
var epoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
|
||||
return Convert.ToInt64((date - epoch).TotalSeconds);
|
||||
}
|
||||
}
|
||||
}
|
24
RoadieLibrary/Extensions/ExceptionExt.cs
Normal file
|
@ -0,0 +1,24 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Roadie.Library.Extensions
|
||||
{
|
||||
public static class ExceptionExt
|
||||
{
|
||||
public static string Serialize(this Exception input)
|
||||
{
|
||||
if(input == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
var settings = new Newtonsoft.Json.JsonSerializerSettings
|
||||
{
|
||||
NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore
|
||||
};
|
||||
return Newtonsoft.Json.JsonConvert.SerializeObject(input, Newtonsoft.Json.Formatting.Indented, settings);
|
||||
}
|
||||
}
|
||||
}
|
27
RoadieLibrary/Extensions/GenericExt.cs
Normal file
|
@ -0,0 +1,27 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Roadie.Library.Extensions
|
||||
{
|
||||
public static class GenericExt
|
||||
{
|
||||
public static TEntity CopyTo<TEntity>(this TEntity OriginalEntity, TEntity NewEntity)
|
||||
{
|
||||
PropertyInfo[] oProperties = OriginalEntity.GetType().GetProperties();
|
||||
|
||||
foreach (PropertyInfo CurrentProperty in oProperties.Where(p => p.CanWrite))
|
||||
{
|
||||
if (CurrentProperty.GetValue(NewEntity, null) != null)
|
||||
{
|
||||
CurrentProperty.SetValue(OriginalEntity, CurrentProperty.GetValue(NewEntity, null), null);
|
||||
}
|
||||
}
|
||||
|
||||
return OriginalEntity;
|
||||
}
|
||||
}
|
||||
}
|
20
RoadieLibrary/Extensions/IntEx.cs
Normal file
|
@ -0,0 +1,20 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Roadie.Library.Extensions
|
||||
{
|
||||
public static class IntEx
|
||||
{
|
||||
public static int? Or(this int? value, int? alternative)
|
||||
{
|
||||
if (!value.HasValue && !alternative.HasValue)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
return value.HasValue ? value : alternative;
|
||||
}
|
||||
}
|
||||
}
|
39
RoadieLibrary/Extensions/ListExt.cs
Normal file
|
@ -0,0 +1,39 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Roadie.Library.Extensions
|
||||
{
|
||||
public static class ListExt
|
||||
{
|
||||
public static void Shuffle<T>(this IList<T> list)
|
||||
{
|
||||
int n = list.Count;
|
||||
Random rnd = new Random();
|
||||
while (n > 1)
|
||||
{
|
||||
int k = (rnd.Next(0, n) % n);
|
||||
n--;
|
||||
T value = list[k];
|
||||
list[k] = list[n];
|
||||
list[n] = value;
|
||||
}
|
||||
}
|
||||
|
||||
public static string ToDelimitedList<T>(this IList<T> list, char delimiter = '|')
|
||||
{
|
||||
return ((ICollection<T>)list).ToDelimitedList(delimiter);
|
||||
}
|
||||
|
||||
public static string ToDelimitedList<T>(this ICollection<T> list, char delimiter = '|')
|
||||
{
|
||||
if (list == null || !list.Any())
|
||||
{
|
||||
return null;
|
||||
}
|
||||
return string.Join(delimiter.ToString(), list);
|
||||
}
|
||||
}
|
||||
}
|
21
RoadieLibrary/Extensions/LongExt.cs
Normal file
|
@ -0,0 +1,21 @@
|
|||
using Roadie.Library.Utility;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Roadie.Library.Extensions
|
||||
{
|
||||
public static class LongExt
|
||||
{
|
||||
public static string ToFileSize(this long? l)
|
||||
{
|
||||
if(!l.HasValue)
|
||||
{
|
||||
return "0";
|
||||
}
|
||||
return String.Format(new FileSizeFormatProvider(), "{0:fs}", l);
|
||||
}
|
||||
}
|
||||
}
|
33
RoadieLibrary/Extensions/ShortExt.cs
Normal file
|
@ -0,0 +1,33 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Roadie.Library.Extensions
|
||||
{
|
||||
public static class ShortExt
|
||||
{
|
||||
public static short? Or(this short? value, short? alternative)
|
||||
{
|
||||
if (!value.HasValue && !alternative.HasValue)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
return value.HasValue ? value : alternative;
|
||||
}
|
||||
|
||||
public static short? TakeLarger(this short? value, short? alternative)
|
||||
{
|
||||
if (!value.HasValue && !alternative.HasValue)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
if(!value.HasValue && alternative.HasValue)
|
||||
{
|
||||
return alternative.Value;
|
||||
}
|
||||
return value.Value > alternative.Value ? value.Value : alternative.Value;
|
||||
}
|
||||
}
|
||||
}
|
366
RoadieLibrary/Extensions/StringExt.cs
Normal file
|
@ -0,0 +1,366 @@
|
|||
using Microsoft.Extensions.Configuration;
|
||||
using Roadie.Library.Utility;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
|
||||
namespace Roadie.Library.Extensions
|
||||
{
|
||||
public static class StringExt
|
||||
{
|
||||
public static string TrimEnd(this string input, string suffixToRemove)
|
||||
{
|
||||
if (input != null && suffixToRemove != null && input.EndsWith(suffixToRemove))
|
||||
{
|
||||
return input.Substring(0, input.Length - suffixToRemove.Length);
|
||||
}
|
||||
return input;
|
||||
}
|
||||
|
||||
public static string FormatWith(this string format, object source)
|
||||
{
|
||||
return FormatWith(format, null, source);
|
||||
}
|
||||
|
||||
public static string FormatWith(this string format, IFormatProvider provider, object source)
|
||||
{
|
||||
if (format == null)
|
||||
{
|
||||
throw new ArgumentNullException("format");
|
||||
}
|
||||
|
||||
Regex r = new Regex(@"(?<start>\{)+(?<property>[\w\.\[\]]+)(?<format>:[^}]+)?(?<end>\})+",
|
||||
RegexOptions.Compiled | RegexOptions.CultureInvariant | RegexOptions.IgnoreCase);
|
||||
|
||||
List<object> values = new List<object>();
|
||||
string rewrittenFormat = r.Replace(format, delegate (Match m)
|
||||
{
|
||||
Group startGroup = m.Groups["start"];
|
||||
Group propertyGroup = m.Groups["property"];
|
||||
Group formatGroup = m.Groups["format"];
|
||||
Group endGroup = m.Groups["end"];
|
||||
|
||||
values.Add((propertyGroup.Value == "0")
|
||||
? source
|
||||
: DataBinder.Eval(source, propertyGroup.Value));
|
||||
|
||||
return new string('{', startGroup.Captures.Count) + (values.Count - 1) + formatGroup.Value
|
||||
+ new string('}', endGroup.Captures.Count);
|
||||
});
|
||||
|
||||
return string.Format(provider, rewrittenFormat, values.ToArray());
|
||||
}
|
||||
|
||||
public static int? ToTrackDuration(this string input)
|
||||
{
|
||||
if (string.IsNullOrEmpty(input) || string.IsNullOrEmpty(input.Replace(":","")))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
try
|
||||
{
|
||||
var parts = input.Contains(":") ? input.Split(':').ToList() : new List<string> { input };
|
||||
while (parts.Count() < 3)
|
||||
{
|
||||
parts.Insert(0, "00:");
|
||||
}
|
||||
var tsRaw = string.Empty;
|
||||
foreach (var part in parts)
|
||||
{
|
||||
if (tsRaw.Length > 0)
|
||||
{
|
||||
tsRaw += ":";
|
||||
}
|
||||
tsRaw += part.PadLeft(2, '0').Substring(0, 2);
|
||||
}
|
||||
TimeSpan ts = TimeSpan.MinValue;
|
||||
var success = TimeSpan.TryParse(tsRaw, out ts);
|
||||
if (success)
|
||||
{
|
||||
return (int?)ts.TotalMilliseconds;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static string NormalizeName(this string input)
|
||||
{
|
||||
if(string.IsNullOrEmpty(input))
|
||||
{
|
||||
return input;
|
||||
}
|
||||
input = input.ToLower();
|
||||
var removeParts = new List<string> { " ft. ", " ft ", " feat ", " feat. " };
|
||||
foreach(var removePart in removeParts)
|
||||
{
|
||||
input = input.Replace(removePart, "");
|
||||
}
|
||||
TextInfo cultInfo = new CultureInfo("en-US", false).TextInfo;
|
||||
return cultInfo.ToTitleCase(input).Trim();
|
||||
}
|
||||
|
||||
public static string Or(this string input, string alternative)
|
||||
{
|
||||
return string.IsNullOrEmpty(input) ? alternative : input;
|
||||
}
|
||||
|
||||
public static string CleanString(this string input, IConfiguration configuration)
|
||||
{
|
||||
if (string.IsNullOrEmpty(input))
|
||||
{
|
||||
return input;
|
||||
}
|
||||
var result = input;
|
||||
foreach(var kvp in configuration.GetValue<Dictionary<string, string>>("Processing:ReplaceStrings", new Dictionary<string, string>()))
|
||||
{
|
||||
result = result.Replace(kvp.Key, kvp.Value);
|
||||
}
|
||||
result = result.Trim().ToTitleCase(false);
|
||||
var removeStringsRegex = configuration.GetValue<string>("Processing:RemoveStringsRegex");
|
||||
if (!string.IsNullOrEmpty(removeStringsRegex))
|
||||
{
|
||||
var regexParts = removeStringsRegex.Split('|');
|
||||
foreach (var regexPart in regexParts)
|
||||
{
|
||||
result = Regex.Replace(result, regexPart, "");
|
||||
}
|
||||
}
|
||||
if (result.Length > 5)
|
||||
{
|
||||
var extensionStart = result.Substring(result.Length - 5, 2);
|
||||
if (extensionStart == "_." || extensionStart == " .")
|
||||
{
|
||||
var inputSb = new StringBuilder(result);
|
||||
inputSb.Remove(result.Length - 5, 2);
|
||||
inputSb.Insert(result.Length - 5, ".");
|
||||
result = inputSb.ToString();
|
||||
}
|
||||
}
|
||||
// Strip out any more than single space and remove any blanks before and after
|
||||
return Regex.Replace(result, @"\s+", " ").Trim();
|
||||
}
|
||||
|
||||
public static string ReplaceLastOccurrence(this string input, string find, string replace = "")
|
||||
{
|
||||
if (string.IsNullOrEmpty(input))
|
||||
{
|
||||
return input;
|
||||
}
|
||||
int Place = input.LastIndexOf(find);
|
||||
return input.Remove(Place, find.Length).Insert(Place, replace);
|
||||
}
|
||||
|
||||
public static string SafeReplace(this string input, string replace, string replaceWith = " ")
|
||||
{
|
||||
if (string.IsNullOrEmpty(input))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
return input.Replace(replace, replaceWith);
|
||||
}
|
||||
|
||||
|
||||
public static string ToTitleCase(this string input, bool doPutTheAtEnd = true)
|
||||
{
|
||||
if (string.IsNullOrEmpty(input))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
TextInfo textInfo = new CultureInfo("en-US", false).TextInfo;
|
||||
var r = textInfo.ToTitleCase(input.Trim().ToLower());
|
||||
r = Regex.Replace(r, @"\s+", " ");
|
||||
if (doPutTheAtEnd)
|
||||
{
|
||||
if (r.StartsWith("The "))
|
||||
{
|
||||
return r.Replace("The ", "") + ", The";
|
||||
}
|
||||
}
|
||||
return r;
|
||||
}
|
||||
|
||||
public static string ToAlphanumericName(this string input)
|
||||
{
|
||||
if (string.IsNullOrEmpty(input))
|
||||
{
|
||||
return input;
|
||||
}
|
||||
input = input.ToLower().Trim().Replace("&", "and");
|
||||
char[] arr = input.ToCharArray();
|
||||
arr = Array.FindAll<char>(arr, (c => (char.IsLetterOrDigit(c))));
|
||||
return new string(arr);
|
||||
}
|
||||
|
||||
public static string ToFolderNameFriendly(this string input)
|
||||
{
|
||||
if (string.IsNullOrEmpty(input))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
return Regex.Replace(PathSanitizer.SanitizeFilename(input, ' '), @"\s+", " ").Trim().TrimEnd('.');
|
||||
}
|
||||
|
||||
public static string ToFileNameFriendly(this string input)
|
||||
{
|
||||
if (string.IsNullOrEmpty(input))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
return Regex.Replace(PathSanitizer.SanitizeFilename(input, ' '), @"\s+", " ").Trim();
|
||||
}
|
||||
|
||||
public static string ToContentDispositionFriendly(this string input)
|
||||
{
|
||||
if (string.IsNullOrEmpty(input))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
return input.Replace(',', ' ');
|
||||
}
|
||||
|
||||
|
||||
public static bool IsValidFilename(this string input)
|
||||
{
|
||||
Regex containsABadCharacter = new Regex("[" + Regex.Escape(new string(System.IO.Path.GetInvalidPathChars())) + "]");
|
||||
if (containsABadCharacter.IsMatch(input))
|
||||
{
|
||||
return false;
|
||||
};
|
||||
return true;
|
||||
}
|
||||
|
||||
public static string RemoveFirst(this string input, string remove = "")
|
||||
{
|
||||
if (string.IsNullOrEmpty(input))
|
||||
{
|
||||
return input;
|
||||
}
|
||||
int index = input.IndexOf(remove);
|
||||
return (index < 0)
|
||||
? input
|
||||
: input.Remove(index, remove.Length).Trim();
|
||||
}
|
||||
|
||||
public static string RemoveStartsWith(this string input, string remove = "")
|
||||
{
|
||||
if (string.IsNullOrEmpty(input))
|
||||
{
|
||||
return input;
|
||||
}
|
||||
int index = input.IndexOf(remove);
|
||||
string result = input;
|
||||
while (index == 0)
|
||||
{
|
||||
result = result.Remove(index, remove.Length).Trim();
|
||||
index = result.IndexOf(remove);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public static bool DoesStartWithNumber(this string input)
|
||||
{
|
||||
if (string.IsNullOrEmpty(input))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
var firstPart = input.Split(' ').First().SafeReplace("[").SafeReplace("]");
|
||||
return SafeParser.ToNumber<long>(firstPart) > 0;
|
||||
}
|
||||
|
||||
public static string StripStartingNumber(this string input)
|
||||
{
|
||||
if (string.IsNullOrEmpty(input))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
if (input.DoesStartWithNumber())
|
||||
{
|
||||
return string.Join(" ", input.Split(' ').Skip(1));
|
||||
}
|
||||
return input;
|
||||
}
|
||||
|
||||
public static bool IsValueInDelimitedList(this string input, string value, char delimiter = '|')
|
||||
{
|
||||
if(string.IsNullOrEmpty(input))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
var p = input.Split(delimiter);
|
||||
return !p.Any() ? false : p.Any(x => x.Trim().Equals(value, StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
public static IEnumerable<string> ToListFromDelimited(this string input, char delimiter = '|')
|
||||
{
|
||||
if(string.IsNullOrEmpty(input))
|
||||
{
|
||||
return new string[0];
|
||||
}
|
||||
return input.Split(delimiter);
|
||||
}
|
||||
|
||||
public static string AddToDelimitedList(this string input, IEnumerable<string> values, char delimiter = '|')
|
||||
{
|
||||
if(string.IsNullOrEmpty(input) && (values == null || !values.Any()))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
if(string.IsNullOrEmpty(input))
|
||||
{
|
||||
return string.Join(delimiter.ToString(), values);
|
||||
}
|
||||
if(values == null || !values.Any())
|
||||
{
|
||||
return input;
|
||||
}
|
||||
foreach(var value in values)
|
||||
{
|
||||
if(string.IsNullOrEmpty(value))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if(!input.IsValueInDelimitedList(value, delimiter))
|
||||
{
|
||||
if (!input.EndsWith(delimiter.ToString()))
|
||||
{
|
||||
input = input + delimiter;
|
||||
}
|
||||
input = input + value;
|
||||
}
|
||||
}
|
||||
return input;
|
||||
}
|
||||
|
||||
public static string ToHexString(this string str)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
|
||||
var bytes = Encoding.UTF8.GetBytes(str);
|
||||
foreach (var t in bytes)
|
||||
{
|
||||
sb.Append(t.ToString("X2"));
|
||||
}
|
||||
|
||||
return sb.ToString(); // returns: "48656C6C6F20776F726C64" for "Hello world"
|
||||
}
|
||||
|
||||
public static string FromHexString(this string hexString)
|
||||
{
|
||||
var bytes = new byte[hexString.Length / 2];
|
||||
for (var i = 0; i < bytes.Length; i++)
|
||||
{
|
||||
bytes[i] = Convert.ToByte(hexString.Substring(i * 2, 2), 16);
|
||||
}
|
||||
|
||||
return Encoding.UTF8.GetString(bytes); // returns: "Hello world" for "48656C6C6F20776F726C64"
|
||||
}
|
||||
}
|
||||
}
|
1067
RoadieLibrary/Factories/ArtistFactory.cs
Normal file
181
RoadieLibrary/Factories/FactoryBase.cs
Normal file
|
@ -0,0 +1,181 @@
|
|||
using Roadie.Library.Caching;
|
||||
using Roadie.Library.SearchEngines.Imaging;
|
||||
using Roadie.Library.SearchEngines.MetaData;
|
||||
using Roadie.Library.SearchEngines.MetaData.Discogs;
|
||||
using Roadie.Library.SearchEngines.MetaData.Spotify;
|
||||
using Roadie.Library.SearchEngines.MetaData.Wikipedia;
|
||||
using Roadie.Library.Logging;
|
||||
using Roadie.Library.Data;
|
||||
using Roadie.Library.MetaData.MusicBrainz;
|
||||
using Roadie.Library.MetaData.LastFm;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
|
||||
namespace Roadie.Library.Factories
|
||||
{
|
||||
public abstract class FactoryBase
|
||||
{
|
||||
protected readonly IArtistSearchEngine _itunesArtistSearchEngine = null;
|
||||
protected readonly IArtistSearchEngine _musicBrainzyArtistSearchEngine = null;
|
||||
protected readonly IArtistSearchEngine _lastFmArtistSearchEngine = null;
|
||||
protected readonly IArtistSearchEngine _spotifyArtistSearchEngine = null;
|
||||
protected readonly IArtistSearchEngine _wikipediaArtistSearchEngine = null;
|
||||
protected readonly IArtistSearchEngine _discogsArtistSearchEngine = null;
|
||||
|
||||
protected readonly IConfiguration _configuration = null;
|
||||
protected readonly ICacheManager _cacheManager = null;
|
||||
protected readonly ILogger _logger = null;
|
||||
protected readonly IRoadieDbContext _dbContext = null;
|
||||
|
||||
protected ICacheManager CacheManager
|
||||
{
|
||||
get
|
||||
{
|
||||
return this._cacheManager;
|
||||
}
|
||||
}
|
||||
|
||||
protected ILogger Logger
|
||||
{
|
||||
get
|
||||
{
|
||||
return this._logger;
|
||||
}
|
||||
}
|
||||
|
||||
protected IRoadieDbContext DbContext
|
||||
{
|
||||
get
|
||||
{
|
||||
return this._dbContext;
|
||||
}
|
||||
}
|
||||
|
||||
protected IConfiguration Configuration
|
||||
{
|
||||
get
|
||||
{
|
||||
return this._configuration;
|
||||
}
|
||||
}
|
||||
|
||||
protected IArtistSearchEngine ITunesArtistSearchEngine
|
||||
{
|
||||
get
|
||||
{
|
||||
return this._itunesArtistSearchEngine;
|
||||
}
|
||||
}
|
||||
|
||||
protected IArtistSearchEngine MusicBrainzArtistSearchEngine
|
||||
{
|
||||
get
|
||||
{
|
||||
return this._musicBrainzyArtistSearchEngine;
|
||||
}
|
||||
}
|
||||
|
||||
protected IArtistSearchEngine LastFmArtistSearchEngine
|
||||
{
|
||||
get
|
||||
{
|
||||
return this._lastFmArtistSearchEngine;
|
||||
}
|
||||
}
|
||||
|
||||
protected IArtistSearchEngine SpotifyArtistSearchEngine
|
||||
{
|
||||
get
|
||||
{
|
||||
return this._spotifyArtistSearchEngine;
|
||||
}
|
||||
}
|
||||
|
||||
protected IArtistSearchEngine WikipediaArtistSearchEngine
|
||||
{
|
||||
get
|
||||
{
|
||||
return this._wikipediaArtistSearchEngine;
|
||||
}
|
||||
}
|
||||
|
||||
protected IArtistSearchEngine DiscogsArtistSearchEngine
|
||||
{
|
||||
get
|
||||
{
|
||||
return this._discogsArtistSearchEngine;
|
||||
}
|
||||
}
|
||||
|
||||
protected IReleaseSearchEngine ITunesReleaseSearchEngine
|
||||
{
|
||||
get
|
||||
{
|
||||
return (IReleaseSearchEngine)this._itunesArtistSearchEngine;
|
||||
}
|
||||
}
|
||||
|
||||
protected IReleaseSearchEngine MusicBrainzReleaseSearchEngine
|
||||
{
|
||||
get
|
||||
{
|
||||
return (IReleaseSearchEngine)this._musicBrainzyArtistSearchEngine;
|
||||
}
|
||||
}
|
||||
|
||||
protected IReleaseSearchEngine LastFmReleaseSearchEngine
|
||||
{
|
||||
get
|
||||
{
|
||||
return (IReleaseSearchEngine)this._lastFmArtistSearchEngine;
|
||||
}
|
||||
}
|
||||
|
||||
protected IReleaseSearchEngine SpotifyReleaseSearchEngine
|
||||
{
|
||||
get
|
||||
{
|
||||
return (IReleaseSearchEngine)this._spotifyArtistSearchEngine;
|
||||
}
|
||||
}
|
||||
|
||||
protected IReleaseSearchEngine WikipediaReleaseSearchEngine
|
||||
{
|
||||
get
|
||||
{
|
||||
return (IReleaseSearchEngine)this._wikipediaArtistSearchEngine;
|
||||
}
|
||||
}
|
||||
|
||||
protected IReleaseSearchEngine DiscogsReleaseSearchEngine
|
||||
{
|
||||
get
|
||||
{
|
||||
return (IReleaseSearchEngine)this._discogsArtistSearchEngine;
|
||||
}
|
||||
}
|
||||
|
||||
protected ILabelSearchEngine DiscogsLabelSearchEngine
|
||||
{
|
||||
get
|
||||
{
|
||||
return (ILabelSearchEngine)this._discogsArtistSearchEngine;
|
||||
}
|
||||
}
|
||||
|
||||
public FactoryBase(IConfiguration configuration, IRoadieDbContext context, ICacheManager cacheManager, ILogger logger
|
||||
)
|
||||
{
|
||||
this._configuration = configuration;
|
||||
this._dbContext = context;
|
||||
this._cacheManager = cacheManager;
|
||||
this._logger = logger;
|
||||
|
||||
this._itunesArtistSearchEngine = new ITunesSearchEngine(configuration, this.CacheManager, this.Logger);
|
||||
this._musicBrainzyArtistSearchEngine = new MusicBrainzProvider(configuration, this.CacheManager, this.Logger);
|
||||
this._lastFmArtistSearchEngine = new LastFmHelper(configuration, this.CacheManager, this.Logger);
|
||||
this._spotifyArtistSearchEngine = new SpotifyHelper(configuration, this.CacheManager, this.Logger);
|
||||
this._wikipediaArtistSearchEngine = new WikipediaHelper(configuration, this.CacheManager, this.Logger);
|
||||
this._discogsArtistSearchEngine = new DiscogsHelper(configuration, this.CacheManager, this.Logger);
|
||||
}
|
||||
}
|
||||
}
|
12
RoadieLibrary/Factories/FactoryResult.cs
Normal file
|
@ -0,0 +1,12 @@
|
|||
using System.Collections.Generic;
|
||||
|
||||
namespace Roadie.Library.Factories
|
||||
{
|
||||
public class FactoryResult<T>
|
||||
{
|
||||
public bool IsSuccess { get; set; }
|
||||
public T Data { get; set; }
|
||||
public IEnumerable<string> Errors { get; set; }
|
||||
public long OperationTime { get; set; }
|
||||
}
|
||||
}
|
124
RoadieLibrary/Factories/ImageFactory.cs
Normal file
|
@ -0,0 +1,124 @@
|
|||
using Roadie.Library.Caching;
|
||||
using Roadie.Library.Imaging;
|
||||
using Roadie.Library.Extensions;
|
||||
using Roadie.Library.Processors;
|
||||
using Roadie.Library.Utility;
|
||||
using Roadie.Library.Logging;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Roadie.Library.Data;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Roadie.Library.MetaData.Audio;
|
||||
|
||||
namespace Roadie.Library.Factories
|
||||
{
|
||||
public sealed class ImageFactory : FactoryBase
|
||||
{
|
||||
private readonly ImageProcessor _imageProcessor = null;
|
||||
|
||||
private ImageProcessor ImageProcessor
|
||||
{
|
||||
get
|
||||
{
|
||||
return this._imageProcessor;
|
||||
}
|
||||
}
|
||||
|
||||
public ImageFactory(IConfiguration configuration, IRoadieDbContext context, ICacheManager cacheManager, ILogger logger) : base(configuration, context, cacheManager, logger)
|
||||
{
|
||||
this._imageProcessor = new ImageProcessor();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get image data from all sources for either fileanme or MetaData
|
||||
/// </summary>
|
||||
/// <param name="filename">Name of the File (ie a CUE file)</param>
|
||||
/// <param name="metaData">Populated MetaData</param>
|
||||
/// <returns></returns>
|
||||
public AudioMetaDataImage GetPictureForMetaData(string filename, AudioMetaData metaData)
|
||||
{
|
||||
SimpleContract.Requires<ArgumentException>(!string.IsNullOrEmpty(filename), "Invalid Filename");
|
||||
SimpleContract.Requires<ArgumentException>(metaData != null, "Invalid MetaData");
|
||||
|
||||
return this.ImageForFilename(filename);
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Does image exist with the same filename
|
||||
/// </summary>
|
||||
/// <param name="filename">Name of the File (ie a CUE file)</param>
|
||||
/// <returns>Null if not found else populated image</returns>
|
||||
public AudioMetaDataImage ImageForFilename(string filename)
|
||||
{
|
||||
AudioMetaDataImage imageMetaData = null;
|
||||
|
||||
if(string.IsNullOrEmpty(filename))
|
||||
{
|
||||
return imageMetaData;
|
||||
}
|
||||
try
|
||||
{
|
||||
var fileInfo = new FileInfo(filename);
|
||||
var ReleaseCover = Path.ChangeExtension(filename, "jpg");
|
||||
if (File.Exists(ReleaseCover))
|
||||
{
|
||||
using (var processor = new ImageProcessor())
|
||||
{
|
||||
imageMetaData = new AudioMetaDataImage
|
||||
{
|
||||
Data = processor.Process(File.ReadAllBytes(ReleaseCover)),
|
||||
Type = AudioMetaDataImageType.FrontCover,
|
||||
MimeType = FileProcessor.DetermineFileType(fileInfo)
|
||||
};
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Is there a picture in filename folder (for the Release)
|
||||
var pictures = fileInfo.Directory.GetFiles("*.jpg");
|
||||
var tagImages = new List<AudioMetaDataImage>();
|
||||
if (pictures != null && pictures.Any())
|
||||
{
|
||||
FileInfo picture = null;
|
||||
// See if there is a "cover" or "front" jpg file if so use it
|
||||
picture = pictures.FirstOrDefault(x => x.Name.Equals("cover", StringComparison.OrdinalIgnoreCase));
|
||||
if(picture == null)
|
||||
{
|
||||
picture = pictures.FirstOrDefault(x => x.Name.Equals("front", StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
if (picture == null)
|
||||
{
|
||||
picture = pictures.First();
|
||||
}
|
||||
if (picture != null)
|
||||
{
|
||||
using (var processor = new ImageProcessor())
|
||||
{
|
||||
imageMetaData = new AudioMetaDataImage
|
||||
{
|
||||
Data = processor.Process(File.ReadAllBytes(picture.FullName)),
|
||||
Type = AudioMetaDataImageType.FrontCover,
|
||||
MimeType = FileProcessor.DetermineFileType(picture)
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
catch(System.IO.FileNotFoundException)
|
||||
{
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this.Logger.Error(ex, ex.Serialize());
|
||||
}
|
||||
return imageMetaData;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
203
RoadieLibrary/Factories/LabelFactory.cs
Normal file
|
@ -0,0 +1,203 @@
|
|||
using Roadie.Library.Caching;
|
||||
using MySql.Data.MySqlClient;
|
||||
using Roadie.Library.Extensions;
|
||||
using Roadie.Library.Utility;
|
||||
using Roadie.Library.Logging;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Roadie.Library.Data;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
|
||||
namespace Roadie.Library.Factories
|
||||
{
|
||||
public sealed class LabelFactory : FactoryBase
|
||||
{
|
||||
public LabelFactory(IConfiguration configuration, IRoadieDbContext context, ICacheManager cacheManager, ILogger logger) : base(configuration, context, cacheManager, logger)
|
||||
{
|
||||
}
|
||||
|
||||
public async Task<FactoryResult<Label>> Add(Label label)
|
||||
{
|
||||
SimpleContract.Requires<ArgumentNullException>(label != null, "Invalid Label");
|
||||
|
||||
try
|
||||
{
|
||||
var now = DateTime.UtcNow;
|
||||
label.AlternateNames = label.AlternateNames.AddToDelimitedList(new string[] { label.Name.ToAlphanumericName() });
|
||||
if (!label.IsValid)
|
||||
{
|
||||
return new FactoryResult<Label>
|
||||
{
|
||||
Errors = new List<string> { "Label is Invalid" }
|
||||
};
|
||||
}
|
||||
this.DbContext.Labels.Add(label);
|
||||
int inserted = 0;
|
||||
try
|
||||
{
|
||||
inserted = await this.DbContext.SaveChangesAsync();
|
||||
}
|
||||
catch (System.Data.Entity.Validation.DbEntityValidationException ex)
|
||||
{
|
||||
foreach (var v in ex.EntityValidationErrors.SelectMany(x => x.ValidationErrors))
|
||||
{
|
||||
this.Logger.Error(string.Format("Property [{0}], Error [{0}]", v.ErrorMessage, v.PropertyName));
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this.Logger.Error(ex);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this.Logger.Error(ex);
|
||||
}
|
||||
return new FactoryResult<Label>
|
||||
{
|
||||
IsSuccess = label.Id > 0,
|
||||
Data = label
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<FactoryResult<Label>> GetByName(string LabelName, bool doFindIfNotInDatabase = false)
|
||||
{
|
||||
try
|
||||
{
|
||||
var sw = new Stopwatch();
|
||||
sw.Start();
|
||||
var cacheRegion = (new Label { Name = LabelName }).CacheRegion;
|
||||
var cacheKey = string.Format("urn:Label_by_name:{0}", LabelName);
|
||||
var resultInCache = this.CacheManager.Get<Label>(cacheKey, cacheRegion);
|
||||
if (resultInCache != null)
|
||||
{
|
||||
sw.Stop();
|
||||
return new FactoryResult<Label>
|
||||
{
|
||||
IsSuccess = true,
|
||||
OperationTime = sw.ElapsedMilliseconds,
|
||||
Data = resultInCache
|
||||
};
|
||||
}
|
||||
var getParams = new List<object>();
|
||||
var searchName = LabelName.NormalizeName().ToLower();
|
||||
getParams.Add(new MySqlParameter("@isName", searchName));
|
||||
getParams.Add(new MySqlParameter("@startAlt", string.Format("{0}|%", searchName)));
|
||||
getParams.Add(new MySqlParameter("@inAlt", string.Format("%|{0}|%", searchName)));
|
||||
getParams.Add(new MySqlParameter("@endAlt", string.Format("%|{0}", searchName)));
|
||||
var Label = this.DbContext.Labels.SqlQuery(@"SELECT *
|
||||
FROM `Label`
|
||||
WHERE LCASE(name) = @isName
|
||||
OR LCASE(sortName) = @isName
|
||||
OR LCASE(alternatenames) = @isName
|
||||
OR alternatenames like @startAlt
|
||||
OR alternatenames like @inAlt
|
||||
OR alternatenames like @endAlt
|
||||
LIMIT 1;", getParams.ToArray()).FirstOrDefault();
|
||||
sw.Stop();
|
||||
if (Label == null || !Label.IsValid)
|
||||
{
|
||||
this._logger.Info("LabelFactory: Label Not Found By Name [{0}]", LabelName);
|
||||
if (doFindIfNotInDatabase)
|
||||
{
|
||||
OperationResult<Label> LabelSearch = null;
|
||||
try
|
||||
{
|
||||
LabelSearch = await this.PerformMetaDataProvidersLabelSearch(LabelName);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this.Logger.Error(ex);
|
||||
}
|
||||
if (LabelSearch.IsSuccess)
|
||||
{
|
||||
Label = LabelSearch.Data;
|
||||
var addResult = await this.Add(Label);
|
||||
if (!addResult.IsSuccess)
|
||||
{
|
||||
sw.Stop();
|
||||
return new FactoryResult<Label>
|
||||
{
|
||||
OperationTime = sw.ElapsedMilliseconds,
|
||||
Errors = addResult.Errors
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
this.CacheManager.Add(cacheKey, Label);
|
||||
}
|
||||
return new FactoryResult<Label>
|
||||
{
|
||||
IsSuccess = Label != null,
|
||||
OperationTime = sw.ElapsedMilliseconds,
|
||||
Data = Label
|
||||
};
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this.Logger.Error(ex);
|
||||
}
|
||||
return new FactoryResult<Label>();
|
||||
}
|
||||
|
||||
public async Task<OperationResult<Label>> PerformMetaDataProvidersLabelSearch(string LabelName)
|
||||
{
|
||||
SimpleContract.Requires<ArgumentNullException>(LabelName != null, "Invalid Label Name");
|
||||
|
||||
var sw = new Stopwatch();
|
||||
sw.Start();
|
||||
var result = new Label
|
||||
{
|
||||
Name = LabelName.ToTitleCase()
|
||||
};
|
||||
var resultsExceptions = new List<Exception>();
|
||||
|
||||
if (this.DiscogsLabelSearchEngine.IsEnabled)
|
||||
{
|
||||
var discogsResult = await this.DiscogsLabelSearchEngine.PerformLabelSearch(result.Name, 1);
|
||||
if (discogsResult.IsSuccess)
|
||||
{
|
||||
var d = discogsResult.Data.First();
|
||||
if (d.Urls != null)
|
||||
{
|
||||
result.URLs = result.URLs.AddToDelimitedList(d.Urls);
|
||||
}
|
||||
if (d.AlternateNames != null)
|
||||
{
|
||||
result.AlternateNames = result.AlternateNames.AddToDelimitedList(d.AlternateNames);
|
||||
}
|
||||
if (!string.IsNullOrEmpty(d.LabelName) && !d.LabelName.Equals(result.name, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
result.AlternateNames.AddToDelimitedList(new string[] { d.LabelName });
|
||||
}
|
||||
result.CopyTo(new Label
|
||||
{
|
||||
Profile = HttpUtility.HtmlEncode(d.Profile),
|
||||
DiscogsId = d.DiscogsId,
|
||||
Name = result.Name ?? d.LabelName.ToTitleCase(),
|
||||
Thumbnail = d.LabelImageUrl != null ? WebHelper.BytesForImageUrl(d.LabelImageUrl) : null
|
||||
});
|
||||
}
|
||||
if (discogsResult.Errors != null)
|
||||
{
|
||||
resultsExceptions.AddRange(discogsResult.Errors);
|
||||
}
|
||||
}
|
||||
|
||||
sw.Stop();
|
||||
return new OperationResult<Label>
|
||||
{
|
||||
Data = result,
|
||||
IsSuccess = result != null,
|
||||
Errors = resultsExceptions,
|
||||
OperationTime = sw.ElapsedMilliseconds
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
97
RoadieLibrary/Factories/PlaylistFactory.cs
Normal file
|
@ -0,0 +1,97 @@
|
|||
using Roadie.Library.Caching;
|
||||
using MySql.Data.MySqlClient;
|
||||
using Roadie.Library.Enums;
|
||||
using Roadie.Library.Extensions;
|
||||
using Roadie.Library.Imaging;
|
||||
using Roadie.Library.Processors;
|
||||
using Roadie.Library.Utility;
|
||||
using Roadie.Library.Logging;
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Roadie.Library.Data;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
|
||||
namespace Roadie.Library.Factories
|
||||
{
|
||||
public class PlaylistFactory : FactoryBase
|
||||
{
|
||||
public PlaylistFactory(IConfiguration configuration, IRoadieDbContext context, ICacheManager cacheManager, ILogger logger) : base(configuration, context, cacheManager, logger)
|
||||
{
|
||||
}
|
||||
|
||||
public async Task<FactoryResult<bool>> AddTracksToPlaylist(Playlist playlist, IEnumerable<string> trackIds)
|
||||
{
|
||||
var sw = new Stopwatch();
|
||||
sw.Start();
|
||||
|
||||
var result = false;
|
||||
var now = DateTime.UtcNow;
|
||||
|
||||
var existingTracksForPlaylist = (from plt in this.DbContext.PlaylistTracks
|
||||
join t in this.DbContext.Tracks on plt.TrackId equals t.Id
|
||||
where plt.PlayListId == playlist.Id
|
||||
select t);
|
||||
var newTracksForPlaylist = (from t in this.DbContext.Tracks
|
||||
where (from x in trackIds select x).Contains(t.RoadieId)
|
||||
where !(from x in existingTracksForPlaylist select x.RoadieId).Contains(t.RoadieId)
|
||||
select t).ToArray();
|
||||
foreach (var newTrackForPlaylist in newTracksForPlaylist)
|
||||
{
|
||||
this.DbContext.PlaylistTracks.Add(new PlaylistTrack
|
||||
{
|
||||
TrackId = newTrackForPlaylist.id,
|
||||
PlayListId = playlist.Id,
|
||||
CreatedDate = now,
|
||||
RoadieId = Guid.NewGuid()
|
||||
});
|
||||
}
|
||||
playlist.LastUpdated = now;
|
||||
await this.DbContext.SaveChangesAsync();
|
||||
result = true;
|
||||
|
||||
var r = await this.ReorderPlaylist(playlist);
|
||||
result = result && r.IsSuccess;
|
||||
|
||||
return new FactoryResult<bool>
|
||||
{
|
||||
Data = result
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<FactoryResult<bool>> ReorderPlaylist(Playlist playlist)
|
||||
{
|
||||
var sw = new Stopwatch();
|
||||
sw.Start();
|
||||
|
||||
var result = false;
|
||||
var now = DateTime.UtcNow;
|
||||
|
||||
if (playlist != null)
|
||||
{
|
||||
var looper = 0;
|
||||
foreach(var playlistTrack in this.DbContext.PlaylistTracks.Where(x => x.PlayListId == playlist.Id).OrderBy(x => x.createdDate))
|
||||
{
|
||||
looper++;
|
||||
playlistTrack.ListNumber = looper;
|
||||
playlistTrack.LastUpdated = now;
|
||||
}
|
||||
await this.DbContext.SaveChangesAsync();
|
||||
result = true;
|
||||
}
|
||||
|
||||
return new FactoryResult<bool>
|
||||
{
|
||||
IsSuccess = result,
|
||||
Data = result
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
1743
RoadieLibrary/Factories/ReleaseFactory.cs
Normal file
274
RoadieLibrary/FilePlugins/Audio.cs
Normal file
|
@ -0,0 +1,274 @@
|
|||
using Roadie.Library.Caching;
|
||||
using Roadie.Library.Extensions;
|
||||
using Roadie.Library.Factories;
|
||||
using Roadie.Library.Utility;
|
||||
using Roadie.Library.Logging;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Roadie.Library.MetaData.MusicBrainz;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Roadie.Library.MetaData.Audio;
|
||||
using Roadie.Library.MetaData.LastFm;
|
||||
using Roadie.Library.Imaging;
|
||||
|
||||
namespace Roadie.Library.FilePlugins
|
||||
{
|
||||
public class Audio : PluginBase
|
||||
{
|
||||
private Guid _releaseId = Guid.Empty;
|
||||
private Guid _artistId = Guid.Empty;
|
||||
|
||||
private MusicBrainzProvider _musicBrainzProvider = null;
|
||||
|
||||
public MusicBrainzProvider MusicBrainzProvider
|
||||
{
|
||||
get
|
||||
{
|
||||
return this._musicBrainzProvider ?? (this._musicBrainzProvider = new MusicBrainzProvider(this.Configuration, this.CacheManager, this.Logger));
|
||||
}
|
||||
set
|
||||
{
|
||||
this._musicBrainzProvider = value;
|
||||
}
|
||||
}
|
||||
|
||||
private LastFmHelper _lastFmHelper = null;
|
||||
|
||||
public LastFmHelper LastFmHelper
|
||||
{
|
||||
get
|
||||
{
|
||||
return this._lastFmHelper ?? (this._lastFmHelper = new LastFmHelper(this.Configuration, this.CacheManager, this.Logger));
|
||||
}
|
||||
set
|
||||
{
|
||||
this._lastFmHelper = value;
|
||||
}
|
||||
}
|
||||
|
||||
private AudioMetaDataHelper _audioMetaDataHelper = null;
|
||||
|
||||
public AudioMetaDataHelper AudioMetaDataHelper
|
||||
{
|
||||
get
|
||||
{
|
||||
return this._audioMetaDataHelper ?? (this._audioMetaDataHelper = new AudioMetaDataHelper(this.Configuration, null, this.MusicBrainzProvider, this.LastFmHelper, this.CacheManager, this.Logger, this.ImageFactory));
|
||||
}
|
||||
set
|
||||
{
|
||||
this._audioMetaDataHelper = value;
|
||||
}
|
||||
}
|
||||
|
||||
public override string[] HandlesTypes
|
||||
{
|
||||
get
|
||||
{
|
||||
return new string[2] { "audio/mpeg", "text/json" };
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public Audio(IConfiguration configuration,
|
||||
ArtistFactory artistFactory,
|
||||
ReleaseFactory releaseFactory,
|
||||
ImageFactory imageFactory,
|
||||
ICacheManager cacheManager,
|
||||
ILogger logger) : base(configuration, artistFactory, releaseFactory, imageFactory, cacheManager, logger)
|
||||
{
|
||||
}
|
||||
|
||||
public override async Task<OperationResult<bool>> Process(string destinationRoot, FileInfo fileInfo, bool doJustInfo, int? submissionId)
|
||||
{
|
||||
var dr = destinationRoot ?? fileInfo.DirectoryName;
|
||||
var result = new OperationResult<bool>();
|
||||
|
||||
string destinationName = null;
|
||||
|
||||
var metaData = await this.AudioMetaDataHelper.GetInfo(fileInfo, doJustInfo);
|
||||
if (!metaData.IsValid)
|
||||
{
|
||||
var minWeight = this.MinWeightToDelete;
|
||||
if (metaData.ValidWeight < minWeight && minWeight > 0)
|
||||
{
|
||||
this.Logger.Trace("Invalid File{3}: ValidWeight [{0}], Under MinWeightToDelete [{1}]. Deleting File [{2}]", metaData.ValidWeight, minWeight, fileInfo.FullName, doJustInfo ? " [Read Only Mode] " : string.Empty);
|
||||
if (!doJustInfo)
|
||||
{
|
||||
fileInfo.Delete();
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
var artist = metaData.Artist.CleanString(this.Configuration);
|
||||
var album = metaData.Release.CleanString(this.Configuration);
|
||||
var title = metaData.Title.CleanString(this.Configuration).ToTitleCase(false);
|
||||
var year = metaData.Year;
|
||||
var trackNumber = metaData.TrackNumber ?? 0;
|
||||
var diskNumber = metaData.Disk ?? 0;
|
||||
|
||||
SimpleContract.Requires(metaData.IsValid, "Track MetaData Invalid");
|
||||
SimpleContract.Requires<ArgumentException>(!string.IsNullOrEmpty(artist), "Missing Track Artist");
|
||||
SimpleContract.Requires<ArgumentException>(!string.IsNullOrEmpty(album), "Missing Track Album");
|
||||
SimpleContract.Requires<ArgumentException>(!string.IsNullOrEmpty(title), "Missing Track Title");
|
||||
SimpleContract.Requires<ArgumentException>(year > 0, string.Format("Invalid Track Year [{0}]", year));
|
||||
SimpleContract.Requires<ArgumentException>(trackNumber > 0, "Missing Track Number");
|
||||
|
||||
var artistFolder = await this.DetermineArtistFolder(dr, metaData, doJustInfo);
|
||||
if (string.IsNullOrEmpty(artistFolder))
|
||||
{
|
||||
this.Logger.Warning("Unable To Find ArtistFolder [{0}] For MetaData [{1}]", artistFolder, metaData.ToString());
|
||||
return new OperationResult<bool>
|
||||
{
|
||||
Messages = new List<string> { "Unable To Find Artist Folder" }
|
||||
};
|
||||
}
|
||||
var releaseFolder = await this.DetermineReleaseFolder(artistFolder, metaData, doJustInfo, submissionId);
|
||||
if (string.IsNullOrEmpty(releaseFolder))
|
||||
{
|
||||
this.Logger.Warning("Unable To Find ReleaseFolder For MetaData [{0}]", metaData.ToString());
|
||||
return new OperationResult<bool>
|
||||
{
|
||||
Messages = new List<string> { "Unable To Find Release Folder" }
|
||||
};
|
||||
}
|
||||
destinationName = FolderPathHelper.TrackFullPath(metaData, dr, artistFolder);
|
||||
this.Logger.Trace("Info: FileInfo [{0}], Artist Folder [{1}], Destination Name [{2}]", fileInfo.FullName, artistFolder, destinationName);
|
||||
|
||||
if (doJustInfo)
|
||||
{
|
||||
result.IsSuccess = metaData.IsValid;
|
||||
return result;
|
||||
}
|
||||
|
||||
PluginBase.CheckMakeFolder(artistFolder);
|
||||
PluginBase.CheckMakeFolder(releaseFolder);
|
||||
|
||||
// See if folder has "cover" image if so then move to release folder for metadata
|
||||
var imageFiles = ImageHelper.ImageFilesInFolder(fileInfo.DirectoryName);
|
||||
if (imageFiles != null && imageFiles.Any())
|
||||
{
|
||||
foreach (var imageFile in imageFiles)
|
||||
{
|
||||
var i = new FileInfo(imageFile);
|
||||
var iName = i.Name.ToLower().Trim();
|
||||
this.Logger.Debug("Found Image File [{0}] [{1}]", imageFile, iName);
|
||||
var isCoverArtType = iName.StartsWith("cover") || iName.StartsWith("folder") || iName.StartsWith("front") || iName.StartsWith("release") || iName.StartsWith("album");
|
||||
if (isCoverArtType)
|
||||
{
|
||||
var coverFileName = Path.Combine(releaseFolder, ReleaseFactory.CoverFilename);
|
||||
if (coverFileName != i.FullName)
|
||||
{
|
||||
// Read image and convert to jpeg
|
||||
var imageBytes = File.ReadAllBytes(i.FullName);
|
||||
imageBytes = ImageHelper.ConvertToJpegFormat(imageBytes);
|
||||
|
||||
// Move cover to release folder
|
||||
if (!doJustInfo)
|
||||
{
|
||||
File.WriteAllBytes(coverFileName, imageBytes);
|
||||
i.Delete();
|
||||
}
|
||||
this.Logger.Debug("Found Image File [{0}], Moved to release folder", i.Name);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var doesFileExistsForTrack = File.Exists(destinationName);
|
||||
if (doesFileExistsForTrack)
|
||||
{
|
||||
var existing = new FileInfo(destinationName);
|
||||
|
||||
// If Exists determine which is better - if same do nothing
|
||||
var existingMetaData = await this.AudioMetaDataHelper.GetInfo(existing, doJustInfo);
|
||||
|
||||
var areSameFile = existing.FullName.Replace("\\", "").Replace("/", "").Equals(fileInfo.FullName.Replace("\\", "").Replace("/", ""), StringComparison.OrdinalIgnoreCase);
|
||||
var currentBitRate = metaData.AudioBitrate;
|
||||
var existingBitRate = existingMetaData.AudioBitrate;
|
||||
|
||||
if (!areSameFile)
|
||||
{
|
||||
if (!existingMetaData.IsValid || (currentBitRate > existingBitRate))
|
||||
{
|
||||
this.Logger.Trace("Newer Is Better: Deleting Existing File [{0}]", existing);
|
||||
if (!doJustInfo)
|
||||
{
|
||||
existing.Delete();
|
||||
fileInfo.MoveTo(destinationName);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
this.Logger.Trace("Existing [{0}] Is Better or Equal: Deleting Found File [{1}]", existing, fileInfo.FullName);
|
||||
if (!doJustInfo)
|
||||
{
|
||||
fileInfo.Delete();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
this.Logger.Trace("Moving File To [{0}]", destinationName);
|
||||
if (!doJustInfo)
|
||||
{
|
||||
fileInfo.MoveTo(destinationName);
|
||||
}
|
||||
}
|
||||
|
||||
result.IsSuccess = true;
|
||||
result.AdditionalData.Add(PluginResultInfo.AdditionalDataKeyPluginResultInfo, new PluginResultInfo
|
||||
{
|
||||
ArtistFolder = artistFolder,
|
||||
ArtistId = this._artistId,
|
||||
ReleaseFolder = releaseFolder,
|
||||
ReleaseId = this._releaseId,
|
||||
Filename = fileInfo.FullName,
|
||||
TrackNumber = metaData.TrackNumber,
|
||||
TrackTitle = metaData.Title
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
private async Task<string> DetermineReleaseFolder(string artistFolder, AudioMetaData metaData, bool doJustInfo, int? submissionId)
|
||||
{
|
||||
var artist = await this.ArtistFactory.GetByName(metaData, !doJustInfo);
|
||||
if (!artist.IsSuccess)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
this._artistId = artist.Data.RoadieId;
|
||||
var release = await this.ReleaseFactory.GetByName(artist.Data, metaData, !doJustInfo, submissionId: submissionId);
|
||||
if (!release.IsSuccess)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
this._releaseId = release.Data.RoadieId;
|
||||
release.Data.releaseDate = SafeParser.ToDateTime(metaData.Year);
|
||||
return release.Data.ReleaseFileFolder(artistFolder);
|
||||
}
|
||||
|
||||
private async Task<string> DetermineArtistFolder(string destinationRoot, AudioMetaData metaData, bool doJustInfo)
|
||||
{
|
||||
var artist = await this.ArtistFactory.GetByName(metaData, !doJustInfo);
|
||||
if (!artist.IsSuccess)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
try
|
||||
{
|
||||
return artist.Data.ArtistFileFolder(destinationRoot);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this._loggingService.Error(ex, ex.Serialize());
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
15
RoadieLibrary/FilePlugins/IFilePlugin.cs
Normal file
|
@ -0,0 +1,15 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Roadie.Library.FilePlugins
|
||||
{
|
||||
public interface IFilePlugin
|
||||
{
|
||||
string[] HandlesTypes { get; }
|
||||
Task<OperationResult<bool>> Process(string destinationRoot, FileInfo file, bool doJustInfo, int? submissionId);
|
||||
}
|
||||
}
|
162
RoadieLibrary/FilePlugins/PluginBase.cs
Normal file
|
@ -0,0 +1,162 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Configuration;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Roadie.Library.Caching;
|
||||
using Roadie.Library.Logging;
|
||||
using Roadie.Library.Utility;
|
||||
using Roadie.Library.Factories;
|
||||
using System.Diagnostics;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Roadie.Library.MetaData.ID3Tags;
|
||||
|
||||
namespace Roadie.Library.FilePlugins
|
||||
{
|
||||
public abstract class PluginBase : IFilePlugin
|
||||
{
|
||||
protected readonly IConfiguration _configuration = null;
|
||||
protected readonly ICacheManager _cacheManager = null;
|
||||
protected readonly ILogger _loggingService = null;
|
||||
protected readonly ArtistFactory _artistFactory = null;
|
||||
protected readonly ReleaseFactory _releaseFactory = null;
|
||||
protected readonly ImageFactory _imageFactory = null;
|
||||
protected ID3TagsHelper _id3TagsHelper = null;
|
||||
protected Audio _audioPlugin = null;
|
||||
|
||||
protected IConfiguration Configuration
|
||||
{
|
||||
get
|
||||
{
|
||||
return this._configuration;
|
||||
}
|
||||
}
|
||||
|
||||
protected ICacheManager CacheManager
|
||||
{
|
||||
get
|
||||
{
|
||||
return this._cacheManager;
|
||||
}
|
||||
}
|
||||
|
||||
protected ILogger Logger
|
||||
{
|
||||
get
|
||||
{
|
||||
return this._loggingService;
|
||||
}
|
||||
}
|
||||
|
||||
protected ArtistFactory ArtistFactory
|
||||
{
|
||||
get
|
||||
{
|
||||
return this._artistFactory;
|
||||
}
|
||||
}
|
||||
|
||||
protected ImageFactory ImageFactory
|
||||
{
|
||||
get
|
||||
{
|
||||
return this._imageFactory;
|
||||
}
|
||||
}
|
||||
|
||||
protected ReleaseFactory ReleaseFactory
|
||||
{
|
||||
get
|
||||
{
|
||||
return this._releaseFactory;
|
||||
}
|
||||
}
|
||||
|
||||
protected ID3TagsHelper ID3TagsHelper
|
||||
{
|
||||
get
|
||||
{
|
||||
return this._id3TagsHelper ?? (this._id3TagsHelper = new ID3TagsHelper(this.Configuration, this.CacheManager, this.Logger));
|
||||
}
|
||||
set
|
||||
{
|
||||
this._id3TagsHelper = value;
|
||||
}
|
||||
}
|
||||
|
||||
protected Audio AudioPlugin
|
||||
{
|
||||
get
|
||||
{
|
||||
return this._audioPlugin ?? (this._audioPlugin = new Audio(this.ArtistFactory, this.ReleaseFactory, this.ImageFactory, this.CacheManager, this.Logger));
|
||||
}
|
||||
set
|
||||
{
|
||||
this._audioPlugin = value;
|
||||
}
|
||||
}
|
||||
|
||||
public PluginBase(IConfiguration configuration, ArtistFactory artistFactory, ReleaseFactory releaseFactory, ImageFactory imageFactory, ICacheManager cacheManager, ILogger logger)
|
||||
{
|
||||
this._configuration = configuration;
|
||||
this._artistFactory = artistFactory;
|
||||
this._releaseFactory = releaseFactory;
|
||||
this._imageFactory = imageFactory;
|
||||
this._cacheManager = cacheManager;
|
||||
this._loggingService = logger;
|
||||
}
|
||||
|
||||
public abstract string[] HandlesTypes { get; }
|
||||
public abstract Task<OperationResult<bool>> Process(string destinationRoot, FileInfo fileInfo, bool doJustInfo, int? submissionId);
|
||||
|
||||
/// <summary>
|
||||
/// Check if exists if not make given folder
|
||||
/// </summary>
|
||||
/// <param name="folder">Folder To Check</param>
|
||||
/// <returns>False if Exists, True if Made</returns>
|
||||
public static bool CheckMakeFolder(string folder)
|
||||
{
|
||||
SimpleContract.Requires<ArgumentException>(!string.IsNullOrEmpty(folder), "Invalid Folder");
|
||||
|
||||
if (!Directory.Exists(folder))
|
||||
{
|
||||
Directory.CreateDirectory(folder);
|
||||
Trace.WriteLine(string.Format("Created Directory [{0}]", folder));
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public int MinWeightToDelete
|
||||
{
|
||||
get
|
||||
{
|
||||
return SafeParser.ToNumber<int>(this.Configuration.GetValue<int>("FilePlugins:MinWeightToDelete", 0));
|
||||
}
|
||||
}
|
||||
|
||||
protected virtual bool IsFileLocked(FileInfo file)
|
||||
{
|
||||
FileStream stream = null;
|
||||
|
||||
try
|
||||
{
|
||||
stream = file.Open(FileMode.Open, FileAccess.ReadWrite, FileShare.None);
|
||||
}
|
||||
catch (IOException)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (stream != null)
|
||||
stream.Close();
|
||||
}
|
||||
|
||||
//file is not locked
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
27
RoadieLibrary/FilePlugins/PluginResultInfo.cs
Normal file
|
@ -0,0 +1,27 @@
|
|||
using Roadie.Library.Enums;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Roadie.Library.FilePlugins
|
||||
{
|
||||
[Serializable]
|
||||
public class PluginResultInfo : OperationResultModel
|
||||
{
|
||||
public const string AdditionalDataKeyPluginResultInfo = "PluginResultInfo";
|
||||
|
||||
public string ArtistFolder { get; set; }
|
||||
public Guid? ArtistId { get; set; }
|
||||
public IEnumerable<string> ArtistNames { get; set; }
|
||||
public string Filename { get; set; }
|
||||
public string ReleaseFolder { get; set; }
|
||||
public Guid? ReleaseId { get; set; }
|
||||
public Statuses Status { get; set; }
|
||||
public short? TrackNumber { get; set; }
|
||||
public string TrackTitle { get; set; }
|
||||
|
||||
public PluginResultInfo()
|
||||
{
|
||||
this.Status = Statuses.Incomplete;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -31,7 +31,7 @@ namespace Roadie.Library.Identity
|
|||
[StringLength(80)]
|
||||
public override string Name { get; set; }
|
||||
|
||||
[Column("roadieId")]
|
||||
[Column("RoadieId")]
|
||||
[StringLength(36)]
|
||||
public string RoadieId { get; set; }
|
||||
|
||||
|
|
|
@ -97,7 +97,7 @@ namespace Roadie.Library.Identity
|
|||
|
||||
public ICollection<Request> Requests { get; set; }
|
||||
|
||||
[Column("roadieId")]
|
||||
[Column("RoadieId")]
|
||||
[StringLength(36)]
|
||||
public string RoadieId { get; set; }
|
||||
|
||||
|
|
165
RoadieLibrary/Imaging/ImageHasher.cs
Normal file
|
@ -0,0 +1,165 @@
|
|||
using SixLabors.ImageSharp;
|
||||
using System;
|
||||
using System.Drawing;
|
||||
using System.Drawing.Drawing2D;
|
||||
using System.Drawing.Imaging;
|
||||
using System.IO;
|
||||
|
||||
|
||||
namespace Roadie.Library.Imaging
|
||||
{
|
||||
/// <summary>
|
||||
/// Contains a variety of methods useful in generating image hashes for image comparison
|
||||
/// and recognition.
|
||||
///
|
||||
/// Credit for the AverageHash implementation to David Oftedal of the University of Oslo.
|
||||
/// </summary>
|
||||
public class ImageHasher
|
||||
{
|
||||
#region Private constants and utility methods
|
||||
|
||||
/// <summary>
|
||||
/// Bitcounts array used for BitCount method (used in Similarity comparisons).
|
||||
/// Don't try to read this or understand it, I certainly don't. Credit goes to
|
||||
/// David Oftedal of the University of Oslo, Norway for this.
|
||||
/// http://folk.uio.no/davidjo/computing.php
|
||||
/// </summary>
|
||||
private static byte[] bitCounts = {
|
||||
0,1,1,2,1,2,2,3,1,2,2,3,2,3,3,4,1,2,2,3,2,3,3,4,2,3,3,4,3,4,4,5,1,2,2,3,2,3,3,4,
|
||||
2,3,3,4,3,4,4,5,2,3,3,4,3,4,4,5,3,4,4,5,4,5,5,6,1,2,2,3,2,3,3,4,2,3,3,4,3,4,4,5,
|
||||
2,3,3,4,3,4,4,5,3,4,4,5,4,5,5,6,2,3,3,4,3,4,4,5,3,4,4,5,4,5,5,6,3,4,4,5,4,5,5,6,
|
||||
4,5,5,6,5,6,6,7,1,2,2,3,2,3,3,4,2,3,3,4,3,4,4,5,2,3,3,4,3,4,4,5,3,4,4,5,4,5,5,6,
|
||||
2,3,3,4,3,4,4,5,3,4,4,5,4,5,5,6,3,4,4,5,4,5,5,6,4,5,5,6,5,6,6,7,2,3,3,4,3,4,4,5,
|
||||
3,4,4,5,4,5,5,6,3,4,4,5,4,5,5,6,4,5,5,6,5,6,6,7,3,4,4,5,4,5,5,6,4,5,5,6,5,6,6,7,
|
||||
4,5,5,6,5,6,6,7,5,6,6,7,6,7,7,8
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Counts bits (duh). Utility function for similarity.
|
||||
/// I wouldn't try to understand this. I just copy-pasta'd it
|
||||
/// from Oftedal's implementation. It works.
|
||||
/// </summary>
|
||||
/// <param name="num">The hash we are counting.</param>
|
||||
/// <returns>The total bit count.</returns>
|
||||
private static uint BitCount(ulong num)
|
||||
{
|
||||
uint count = 0;
|
||||
for (; num > 0; num >>= 8)
|
||||
count += bitCounts[(num & 0xff)];
|
||||
return count;
|
||||
}
|
||||
|
||||
#endregion Private constants and utility methods
|
||||
|
||||
#region Public interface methods
|
||||
|
||||
/// <summary>
|
||||
/// Computes the average hash of an image according to the algorithm given by Dr. Neal Krawetz
|
||||
/// on his blog: http://www.hackerfactor.com/blog/index.php?/archives/432-Looks-Like-It.html.
|
||||
/// </summary>
|
||||
/// <param name="image">The image to hash.</param>
|
||||
/// <returns>The hash of the image.</returns>
|
||||
public static ulong AverageHash(Image image)
|
||||
{
|
||||
// Squeeze the image into an 8x8 canvas
|
||||
Bitmap squeezed = new Bitmap(8, 8, PixelFormat.Format32bppRgb);
|
||||
Graphics canvas = Graphics.FromImage(squeezed);
|
||||
canvas.CompositingQuality = CompositingQuality.HighQuality;
|
||||
canvas.InterpolationMode = InterpolationMode.HighQualityBilinear;
|
||||
canvas.SmoothingMode = SmoothingMode.HighQuality;
|
||||
canvas.DrawImage(image, 0, 0, 8, 8);
|
||||
|
||||
// Reduce colors to 6-bit grayscale and calculate average color value
|
||||
byte[] grayscale = new byte[64];
|
||||
uint averageValue = 0;
|
||||
for (int y = 0; y < 8; y++)
|
||||
for (int x = 0; x < 8; x++)
|
||||
{
|
||||
uint pixel = (uint)squeezed.GetPixel(x, y).ToArgb();
|
||||
uint gray = (pixel & 0x00ff0000) >> 16;
|
||||
gray += (pixel & 0x0000ff00) >> 8;
|
||||
gray += (pixel & 0x000000ff);
|
||||
gray /= 12;
|
||||
|
||||
grayscale[x + (y * 8)] = (byte)gray;
|
||||
averageValue += gray;
|
||||
}
|
||||
averageValue /= 64;
|
||||
|
||||
// Compute the hash: each bit is a pixel
|
||||
// 1 = higher than average, 0 = lower than average
|
||||
ulong hash = 0;
|
||||
for (int i = 0; i < 64; i++)
|
||||
if (grayscale[i] >= averageValue)
|
||||
hash |= (1UL << (63 - i));
|
||||
|
||||
return hash;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Computes the average hash of the image content in the given file.
|
||||
/// </summary>
|
||||
/// <param name="path">Path to the input file.</param>
|
||||
/// <returns>The hash of the input file's image content.</returns>
|
||||
public static ulong AverageHash(String path)
|
||||
{
|
||||
Bitmap bmp = new Bitmap(path);
|
||||
return AverageHash(bmp);
|
||||
}
|
||||
|
||||
public static ulong AverageHash(byte[] imageBytes)
|
||||
{
|
||||
if (imageBytes == null)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
using (var ms = new MemoryStream(imageBytes))
|
||||
{
|
||||
var b = new Bitmap(ms);
|
||||
return AverageHash(b);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a percentage-based similarity value between the two given hashes. The higher
|
||||
/// the percentage, the closer the hashes are to being identical.
|
||||
/// </summary>
|
||||
/// <param name="hash1">The first hash.</param>
|
||||
/// <param name="hash2">The second hash.</param>
|
||||
/// <returns>The similarity percentage.</returns>
|
||||
public static double Similarity(ulong hash1, ulong hash2)
|
||||
{
|
||||
return ((64 - BitCount(hash1 ^ hash2)) * 100) / 64.0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a percentage-based similarity value between the two given images. The higher
|
||||
/// the percentage, the closer the images are to being identical.
|
||||
/// </summary>
|
||||
/// <param name="image1">The first image.</param>
|
||||
/// <param name="image2">The second image.</param>
|
||||
/// <returns>The similarity percentage.</returns>
|
||||
public static double Similarity(Image image1, Image image2)
|
||||
{
|
||||
ulong hash1 = AverageHash(image1);
|
||||
ulong hash2 = AverageHash(image2);
|
||||
return Similarity(hash1, hash2);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a percentage-based similarity value between the image content of the two given
|
||||
/// files. The higher the percentage, the closer the image contents are to being identical.
|
||||
/// </summary>
|
||||
/// <param name="image1">The first image file.</param>
|
||||
/// <param name="image2">The second image file.</param>
|
||||
/// <returns>The similarity percentage.</returns>
|
||||
public static double Similarity(String path1, String path2)
|
||||
{
|
||||
ulong hash1 = AverageHash(path1);
|
||||
ulong hash2 = AverageHash(path2);
|
||||
return Similarity(hash1, hash2);
|
||||
}
|
||||
|
||||
#endregion Public interface methods
|
||||
}
|
||||
}
|
136
RoadieLibrary/Imaging/ImageHelper.cs
Normal file
|
@ -0,0 +1,136 @@
|
|||
using Roadie.Library.Extensions;
|
||||
using Roadie.Library.SearchEngines.Imaging;
|
||||
using Roadie.Library.Utility;
|
||||
using SixLabors.ImageSharp;
|
||||
using SixLabors.ImageSharp.Formats;
|
||||
using SixLabors.ImageSharp.PixelFormats;
|
||||
using SixLabors.ImageSharp.Processing;
|
||||
using SixLabors.Primitives;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace Roadie.Library.Imaging
|
||||
{
|
||||
public static class ImageHelper
|
||||
{
|
||||
public static byte[] ConvertImageToGreyscale(byte[] imageBytes)
|
||||
{
|
||||
using (MemoryStream inStream = new MemoryStream(imageBytes))
|
||||
{
|
||||
using (MemoryStream outStream = new MemoryStream())
|
||||
{
|
||||
byte[] byteArray = new byte[0];
|
||||
using (var image = new Bitmap(inStream))
|
||||
{
|
||||
for (int i = 0; i < image.Width; i++)
|
||||
{
|
||||
for (int j = 0; j < image.Height; j++)
|
||||
{
|
||||
int ser = (image.GetPixel(i, j).R + image.GetPixel(i, j).G + image.GetPixel(i, j).B) / 3;
|
||||
image.SetPixel(i, j, Color.FromArgb(ser, ser, ser));
|
||||
}
|
||||
}
|
||||
using (MemoryStream stream = new MemoryStream())
|
||||
{
|
||||
image.Save(stream, ImageFormat.Jpeg);
|
||||
stream.Close();
|
||||
|
||||
byteArray = stream.ToArray();
|
||||
}
|
||||
}
|
||||
return byteArray;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static byte[] ConvertToJpegFormat(byte[] imageBytes)
|
||||
{
|
||||
if (imageBytes == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
using (MemoryStream outStream = new MemoryStream())
|
||||
{
|
||||
IImageFormat imageFormat = null;
|
||||
using (Image<Rgba32> image = Image.Load(imageBytes, out imageFormat))
|
||||
{
|
||||
image.Save(outStream, ImageFormats.Jpeg);
|
||||
}
|
||||
return outStream.ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
public static string GenerateImageSignature(byte[] imageBytes)
|
||||
{
|
||||
var greyScale = ConvertImageToGreyscale(imageBytes);
|
||||
return ResizeImage(greyScale, 8, 8).ComputeHash().ToString();
|
||||
}
|
||||
|
||||
public static string[] GetFiles(string path, string[] patterns = null, SearchOption options = SearchOption.TopDirectoryOnly)
|
||||
{
|
||||
if (patterns == null || patterns.Length == 0)
|
||||
{
|
||||
return Directory.GetFiles(path, "*", options);
|
||||
}
|
||||
if (patterns.Length == 1)
|
||||
{
|
||||
return Directory.GetFiles(path, patterns[0], options);
|
||||
}
|
||||
return patterns.SelectMany(pattern => Directory.GetFiles(path, pattern, options)).Distinct().ToArray();
|
||||
}
|
||||
|
||||
public static string[] ImageExtensions()
|
||||
{
|
||||
return new string[8] { "*.bmp", "*.jpeg", "*.jpe", "*.jpg", "*.png", "*.gif", "*.tif", "*.tiff" };
|
||||
}
|
||||
|
||||
public static string[] ImageFilesInFolder(string folder)
|
||||
{
|
||||
return ImageHelper.GetFiles(folder, ImageHelper.ImageExtensions());
|
||||
}
|
||||
|
||||
public static string[] ImageMimeTypes()
|
||||
{
|
||||
return new string[5] { "image/bmp", "image/jpeg", "image/png", "image/gif", "image/tiff" };
|
||||
}
|
||||
|
||||
public static ImageSearchResult ImageSearchResultForImageUrl(string imageUrl)
|
||||
{
|
||||
if (!WebHelper.IsStringUrl(imageUrl))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
var result = new ImageSearchResult();
|
||||
var imageBytes = WebHelper.BytesForImageUrl(imageUrl);
|
||||
IImageFormat imageFormat = null;
|
||||
using (Image<Rgba32> image = Image.Load(imageBytes, out imageFormat))
|
||||
{
|
||||
result.Height = image.Height.ToString();
|
||||
result.Width = image.Width.ToString();
|
||||
result.MediaUrl = imageUrl;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resize a given image to given dimensions
|
||||
/// </summary>
|
||||
public static byte[] ResizeImage(byte[] imageBytes, int width, int height)
|
||||
{
|
||||
using (MemoryStream outStream = new MemoryStream())
|
||||
{
|
||||
IImageFormat imageFormat = null;
|
||||
using (Image<Rgba32> image = Image.Load(imageBytes, out imageFormat))
|
||||
{
|
||||
image.Mutate(ctx => ctx.Resize(width, height));
|
||||
image.Save(outStream, imageFormat);
|
||||
}
|
||||
return outStream.ToArray();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
163
RoadieLibrary/Imaging/ImageProcessor.cs
Normal file
|
@ -0,0 +1,163 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Configuration;
|
||||
using System.Drawing;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Simple.ImageResizer;
|
||||
using System.IO;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
|
||||
namespace Roadie.Library.Imaging
|
||||
{
|
||||
/// <summary>
|
||||
/// Processor that takes images and manipulates
|
||||
/// </summary>
|
||||
public sealed class ImageProcessor : IDisposable
|
||||
{
|
||||
private readonly IConfiguration _configuration;
|
||||
private IntPtr nativeResource = Marshal.AllocHGlobal(100);
|
||||
|
||||
private IConfiguration Configuration
|
||||
{
|
||||
get
|
||||
{
|
||||
return this._configuration;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read from Configuration maximum width; if not set uses default (500)
|
||||
/// </summary>
|
||||
public int MaxWidth
|
||||
{
|
||||
get
|
||||
{
|
||||
return this.Configuration.GetValue<int>("ImageProcessor:MaxWidth", 500);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read from Configuration image encoding; if not set uses default (Jpg Quality of 90)
|
||||
/// </summary>
|
||||
public ImageEncoding ImageEncoding
|
||||
{
|
||||
get
|
||||
{
|
||||
var imageEncoding = ConfigurationManager.AppSettings["ImageProcessor:ImageEncoding"];
|
||||
if (!string.IsNullOrEmpty(imageEncoding))
|
||||
{
|
||||
return (ImageEncoding)Enum.Parse(typeof(ImageEncoding), imageEncoding);
|
||||
}
|
||||
return ImageEncoding.Jpg90;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Processor that takes images and performs any manipulations
|
||||
/// </summary>
|
||||
public ImageProcessor(IConfiguration configuration)
|
||||
{
|
||||
this._configuration = configuration;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Perform any necessary adjustments to file
|
||||
/// </summary>
|
||||
/// <param name="file">Filename to modify</param>
|
||||
/// <returns>Success</returns>
|
||||
public bool Process(string file)
|
||||
{
|
||||
File.WriteAllBytes(file, this.Process(File.ReadAllBytes(file)));
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Perform any necessary adjustments to byte array writing modified file to filename
|
||||
/// </summary>
|
||||
/// <param name="filename">Filename to Write Modified Byte Array to</param>
|
||||
/// <param name="imageBytes">Byte Array of Image To Manipulate</param>
|
||||
/// <returns>Success</returns>
|
||||
public bool Process(string filename, byte[] imageBytes)
|
||||
{
|
||||
File.WriteAllBytes(filename, this.Process(imageBytes));
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Perform any necessary adjustments to byte array returning modified array
|
||||
/// </summary>
|
||||
/// <param name="imageBytes">Byte Array of Image To Manipulate</param>
|
||||
/// <returns>Modified Byte Array of Image</returns>
|
||||
public byte[] Process(byte[] imageBytes)
|
||||
{
|
||||
using (var resizer = new ImageResizer(imageBytes))
|
||||
{
|
||||
return resizer.Resize(this.MaxWidth, this.ImageEncoding);
|
||||
}
|
||||
}
|
||||
|
||||
#region IDisposable Implementation
|
||||
|
||||
~ImageProcessor()
|
||||
{
|
||||
Dispose(false);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
public void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
|
||||
}
|
||||
if (nativeResource != IntPtr.Zero)
|
||||
{
|
||||
Marshal.FreeHGlobal(nativeResource);
|
||||
nativeResource = IntPtr.Zero;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
/// Fetch Image from Given Url and Return Image
|
||||
/// </summary>
|
||||
/// <param name="url">FQDN of Url to Image</param>
|
||||
/// <returns>Image</returns>
|
||||
public static Image GetImageFromUrl(string url)
|
||||
{
|
||||
HttpWebRequest httpWebRequest = (HttpWebRequest)HttpWebRequest.Create(url);
|
||||
|
||||
using (HttpWebResponse httpWebReponse = (HttpWebResponse)httpWebRequest.GetResponse())
|
||||
{
|
||||
using (Stream stream = httpWebReponse.GetResponseStream())
|
||||
{
|
||||
return Image.FromStream(stream);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get all Bytes for an Image
|
||||
/// </summary>
|
||||
/// <param name="imageIn">Image to Get Bytes For</param>
|
||||
/// <returns>Byte Array of Image</returns>
|
||||
public static byte[] ImageToByteArray(Image imageIn)
|
||||
{
|
||||
using (var ms = new MemoryStream())
|
||||
{
|
||||
imageIn.Save(ms, System.Drawing.Imaging.ImageFormat.Jpeg);
|
||||
return ms.ToArray();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
26
RoadieLibrary/Logging/ILogger.cs
Normal file
|
@ -0,0 +1,26 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace Roadie.Library.Logging
|
||||
{
|
||||
public interface ILogger
|
||||
{
|
||||
string Name { get; }
|
||||
|
||||
void Debug(string message);
|
||||
void Debug(string message, params object[] args);
|
||||
void Error(string message);
|
||||
void Error(string message, params object[] args);
|
||||
void Error(Exception exception, string message = null, bool isStackTraceIncluded = true);
|
||||
void Fatal(string message);
|
||||
void Fatal(string message, params object[] args);
|
||||
void Fatal(Exception exception, string message = null, bool isStackTraceIncluded = true);
|
||||
void Info(string message);
|
||||
void Info(string message, params object[] args);
|
||||
void Trace(string message);
|
||||
void Trace(string message, params object[] args);
|
||||
void Warning(string message);
|
||||
void Warning(string message, params object[] args);
|
||||
}
|
||||
}
|
102
RoadieLibrary/Logging/SerilogLogger.cs
Normal file
|
@ -0,0 +1,102 @@
|
|||
using Serilog;
|
||||
using Serilog.Sinks.SystemConsole.Themes;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace Roadie.Library.Logging
|
||||
{
|
||||
public sealed class Logger : ILogger
|
||||
{
|
||||
private Serilog.Core.Logger _log = null;
|
||||
|
||||
public string Name
|
||||
{
|
||||
get
|
||||
{
|
||||
return "Roadie Serilog Logger";
|
||||
}
|
||||
}
|
||||
|
||||
public Logger()
|
||||
{
|
||||
this._log = new LoggerConfiguration()
|
||||
.MinimumLevel.Verbose()
|
||||
.WriteTo.Console(theme: AnsiConsoleTheme.Literate)
|
||||
.WriteTo.File("logs//log-.txt", Serilog.Events.LogEventLevel.Debug)
|
||||
.WriteTo.File("logs//errors-.txt", Serilog.Events.LogEventLevel.Error)
|
||||
.CreateLogger();
|
||||
}
|
||||
|
||||
|
||||
public void Debug(string message)
|
||||
{
|
||||
this._log.Debug(message);
|
||||
}
|
||||
|
||||
public void Debug(string message, params object[] args)
|
||||
{
|
||||
this._log.Debug(message, args);
|
||||
}
|
||||
|
||||
public void Error(string message)
|
||||
{
|
||||
this._log.Error(message);
|
||||
}
|
||||
|
||||
public void Error(string message, params object[] args)
|
||||
{
|
||||
this._log.Error(message, args);
|
||||
}
|
||||
|
||||
public void Error(Exception exception, string message = null, bool isStackTraceIncluded = true)
|
||||
{
|
||||
this._log.Error(exception, message);
|
||||
}
|
||||
|
||||
public void Fatal(string message)
|
||||
{
|
||||
this._log.Fatal(message);
|
||||
}
|
||||
|
||||
public void Fatal(string message, params object[] args)
|
||||
{
|
||||
this._log.Fatal(message, args);
|
||||
}
|
||||
|
||||
public void Fatal(Exception exception, string message = null, bool isStackTraceIncluded = true)
|
||||
{
|
||||
this._log.Fatal(exception, message);
|
||||
}
|
||||
|
||||
public void Info(string message)
|
||||
{
|
||||
this._log.Information(message);
|
||||
}
|
||||
|
||||
public void Info(string message, params object[] args)
|
||||
{
|
||||
this._log.Information(message, args);
|
||||
}
|
||||
|
||||
public void Trace(string message)
|
||||
{
|
||||
this._log.Verbose(message);
|
||||
}
|
||||
|
||||
public void Trace(string message, params object[] args)
|
||||
{
|
||||
this._log.Verbose(message, args);
|
||||
}
|
||||
|
||||
public void Warning(string message)
|
||||
{
|
||||
this._log.Warning(message);
|
||||
}
|
||||
|
||||
public void Warning(string message, params object[] args)
|
||||
{
|
||||
this._log.Warning(message, args);
|
||||
}
|
||||
}
|
||||
}
|
20
RoadieLibrary/OperationResult.cs
Normal file
|
@ -0,0 +1,20 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Roadie.Library
|
||||
{
|
||||
public sealed class OperationResult<T>
|
||||
{
|
||||
public Dictionary<string, object> AdditionalData { get; set; }
|
||||
public T Data { get; set; }
|
||||
public IEnumerable<Exception> Errors { get; set; }
|
||||
public bool IsSuccess { get; set; }
|
||||
public IEnumerable<string> Messages { get; set; }
|
||||
public long OperationTime { get; set; }
|
||||
|
||||
public OperationResult()
|
||||
{
|
||||
this.AdditionalData = new Dictionary<string, object>();
|
||||
}
|
||||
}
|
||||
}
|
23
RoadieLibrary/OperationResultModel.cs
Normal file
|
@ -0,0 +1,23 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Roadie.Library
|
||||
{
|
||||
[Serializable]
|
||||
public class OperationResultModel
|
||||
{
|
||||
public const string NoImageDataFound = "NO_IMAGE_DATA_FOUND";
|
||||
public const string NotModified = "NotModified";
|
||||
public const string OkMessage = "OK";
|
||||
public virtual Dictionary<string, string> Data { get; set; }
|
||||
public IEnumerable<string> Errors { get; set; }
|
||||
public bool IsSuccess { get; set; }
|
||||
public long OperationTime { get; set; }
|
||||
public string RoadieId { get; set; }
|
||||
|
||||
public OperationResultModel()
|
||||
{
|
||||
this.Data = new Dictionary<string, string>();
|
||||
}
|
||||
}
|
||||
}
|
167
RoadieLibrary/Processors/FileProcessor.cs
Normal file
|
@ -0,0 +1,167 @@
|
|||
using Roadie.Library.Caching;
|
||||
using Roadie.Library.Extensions;
|
||||
using Roadie.Library.FilePlugins;
|
||||
using Roadie.Library.Utility;
|
||||
using Roadie.Library.Logging;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Data.SqlClient;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Roadie.Library.Data;
|
||||
|
||||
namespace Roadie.Library.Processors
|
||||
{
|
||||
public sealed class FileProcessor : ProcessorBase
|
||||
{
|
||||
|
||||
private IEnumerable<IFilePlugin> _plugins = null;
|
||||
|
||||
public IEnumerable<IFilePlugin> Plugins
|
||||
{
|
||||
get
|
||||
{
|
||||
if(this._plugins == null)
|
||||
{
|
||||
var plugins = new List<IFilePlugin>();
|
||||
try
|
||||
{
|
||||
var type = typeof(IFilePlugin);
|
||||
var types = AppDomain.CurrentDomain.GetAssemblies()
|
||||
.SelectMany(s => s.GetTypes())
|
||||
.Where(p => type.IsAssignableFrom(p));
|
||||
foreach (Type t in types)
|
||||
{
|
||||
if (t.GetInterface("IFilePlugin") != null && !t.IsAbstract && !t.IsInterface)
|
||||
{
|
||||
IFilePlugin plugin = Activator.CreateInstance(t, new object[] { this.ArtistFactory, this.ReleaseFactory, this.ImageFactory, this.CacheManager, this.LoggingService }) as IFilePlugin;
|
||||
plugins.Add(plugin);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
this._plugins = plugins.ToArray();
|
||||
}
|
||||
return this._plugins;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
public FileProcessor(IConfiguration configuration, string destinationRoot, IRoadieDbContext context, ICacheManager cacheManager, ILogger logger) : base(configuration, destinationRoot, context, cacheManager, logger)
|
||||
{
|
||||
}
|
||||
|
||||
public async Task<OperationResult<bool>> Process(string filename, bool doJustInfo = false)
|
||||
{
|
||||
return await this.Process(new FileInfo(filename), doJustInfo);
|
||||
}
|
||||
|
||||
public async Task<OperationResult<bool>> Process(FileInfo fileInfo, bool doJustInfo = false)
|
||||
{
|
||||
var result = new OperationResult<bool>();
|
||||
|
||||
try
|
||||
{
|
||||
// Determine what type of file this is
|
||||
var fileType = FileProcessor.DetermineFileType(fileInfo);
|
||||
|
||||
OperationResult<bool> pluginResult = null;
|
||||
foreach (var p in this.Plugins)
|
||||
{
|
||||
// See if there is a plugin
|
||||
if (p.HandlesTypes.Contains(fileType))
|
||||
{
|
||||
pluginResult = await p.Process(this.DestinationRoot, fileInfo, doJustInfo, this.SubmissionId);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!doJustInfo)
|
||||
{
|
||||
// If no plugin, or if plugin not successfull and toggle then move unknown file
|
||||
if ((pluginResult == null || !pluginResult.IsSuccess) && this.DoMoveUnknowns)
|
||||
{
|
||||
var uf = this.UnknownFolder;
|
||||
if (!string.IsNullOrEmpty(uf))
|
||||
{
|
||||
if (!Directory.Exists(uf))
|
||||
{
|
||||
Directory.CreateDirectory(uf);
|
||||
}
|
||||
if (!fileInfo.DirectoryName.Equals(this.UnknownFolder))
|
||||
{
|
||||
if (File.Exists(fileInfo.FullName))
|
||||
{
|
||||
var df = Path.Combine(this.UnknownFolder, string.Format("{0}~{1}~{2}", Guid.NewGuid(), fileInfo.Directory.Name, fileInfo.Name));
|
||||
this.LoggingService.Debug("Moving Unknown/Invalid File [{0}] -> [{1}] to UnknownFolder", fileInfo.FullName, df);
|
||||
fileInfo.MoveTo(df);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
result = pluginResult;
|
||||
}
|
||||
catch (System.IO.PathTooLongException ex)
|
||||
{
|
||||
this.LoggingService.Error(ex, string.Format("Error Processing File. File Name Too Long. Deleting."));
|
||||
if (!doJustInfo)
|
||||
{
|
||||
fileInfo.Delete();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
var willMove = !fileInfo.DirectoryName.Equals(this.UnknownFolder);
|
||||
this.LoggingService.Error(ex, string.Format("Error Processing File [{0}], WillMove [{1}]\n{2}", fileInfo.FullName, willMove, ex.Serialize()));
|
||||
string newPath = null;
|
||||
try
|
||||
{
|
||||
newPath = Path.Combine(this.UnknownFolder, fileInfo.Directory.Parent.Name, fileInfo.Directory.Name, fileInfo.Name);
|
||||
if (willMove && !doJustInfo)
|
||||
{
|
||||
var directoryPath = Path.GetDirectoryName(newPath);
|
||||
if(!Directory.Exists(directoryPath))
|
||||
{
|
||||
Directory.CreateDirectory(directoryPath);
|
||||
}
|
||||
fileInfo.MoveTo(newPath);
|
||||
}
|
||||
}
|
||||
catch (Exception ex1)
|
||||
{
|
||||
this.LoggingService.Error(ex1, string.Format("Unable to move file [{0}] to [{1}]", fileInfo.FullName, newPath));
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public static string DetermineFileType(System.IO.FileInfo fileinfo)
|
||||
{
|
||||
string r = MimeMapping.MimeUtility.GetMimeMapping(fileinfo.FullName);
|
||||
if (r.Equals("application/octet-stream"))
|
||||
{
|
||||
if (fileinfo.Extension.Equals(".cue"))
|
||||
{
|
||||
r = "audio/r-cue";
|
||||
}
|
||||
if (fileinfo.Extension.Equals(".mp4") || fileinfo.Extension.Equals(".m4a"))
|
||||
{
|
||||
r = "audio/mp4";
|
||||
}
|
||||
}
|
||||
Trace.WriteLine(string.Format("FileType [{0}] For File [{1}]", r, fileinfo.FullName));
|
||||
return r;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
159
RoadieLibrary/Processors/FolderProcessor.cs
Normal file
|
@ -0,0 +1,159 @@
|
|||
using Roadie.Library.Caching;
|
||||
using Roadie.Library.Extensions;
|
||||
using Roadie.Library.FilePlugins;
|
||||
using Roadie.Library.Utility;
|
||||
using Roadie.Library.Logging;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Roadie.Library.Data;
|
||||
|
||||
namespace Roadie.Library.Processors
|
||||
{
|
||||
public sealed class FolderProcessor : ProcessorBase
|
||||
{
|
||||
private readonly FileProcessor _fileProcessor;
|
||||
private FileProcessor FileProcessor
|
||||
{
|
||||
get
|
||||
{
|
||||
return this._fileProcessor;
|
||||
}
|
||||
}
|
||||
public int? ProcessLimit { get; set; }
|
||||
|
||||
public FolderProcessor(IConfiguration configuration,string destinationRoot, IRoadieDbContext context, ICacheManager cacheManager, ILogger loggingService)
|
||||
: base(configuration, destinationRoot, context, cacheManager, loggingService)
|
||||
{
|
||||
SimpleContract.Requires<ArgumentNullException>(!string.IsNullOrEmpty(destinationRoot), "Invalid Destination Folder");
|
||||
this._fileProcessor = new FileProcessor(configuration, destinationRoot, context, cacheManager, loggingService);
|
||||
}
|
||||
|
||||
public async Task<OperationResult<bool>> Process(DirectoryInfo inboundFolder, bool doJustInfo, int? submissionId = null)
|
||||
{
|
||||
var sw = new Stopwatch();
|
||||
sw.Start();
|
||||
this.PrePrecessFolder(inboundFolder, doJustInfo);
|
||||
int processedFiles = 0;
|
||||
var pluginResultInfos = new List<PluginResultInfo>();
|
||||
var errors = new List<string>();
|
||||
this.FileProcessor.SubmissionId = submissionId;
|
||||
foreach (var file in Directory.EnumerateFiles(inboundFolder.FullName, "*.*", SearchOption.AllDirectories).ToArray())
|
||||
{
|
||||
var operation = await this.FileProcessor.Process(file, doJustInfo);
|
||||
if(operation != null && operation.AdditionalData != null && operation.AdditionalData.ContainsKey(PluginResultInfo.AdditionalDataKeyPluginResultInfo))
|
||||
{
|
||||
pluginResultInfos.Add(operation.AdditionalData[PluginResultInfo.AdditionalDataKeyPluginResultInfo] as PluginResultInfo);
|
||||
}
|
||||
if(operation == null)
|
||||
{
|
||||
var fileExtensionsToDelete = this.Configuration.GetValue<string[]>("FileExtensionsToDelete", new string[0]);
|
||||
if (fileExtensionsToDelete.Any(x => x.Equals(Path.GetExtension(file), StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
if (!doJustInfo)
|
||||
{
|
||||
if (!Path.GetFileNameWithoutExtension(file).ToLower().Equals("cover"))
|
||||
{
|
||||
File.Delete(file);
|
||||
this.LoggingService.Info("x Deleted File [{0}], Was foud in in FileExtensionsToDelete", file);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
processedFiles++;
|
||||
if (this.ProcessLimit.HasValue && processedFiles > this.ProcessLimit.Value)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
await this.PostProcessFolder(inboundFolder, pluginResultInfos, doJustInfo);
|
||||
sw.Stop();
|
||||
this.LoggingService.Info("** Completed! Processed Folder [{0}]: Processed Files [{1}] : Elapsed Time [{2}]", inboundFolder.FullName.ToString(), processedFiles, sw.Elapsed);
|
||||
return new OperationResult<bool>
|
||||
{
|
||||
IsSuccess = !errors.Any(),
|
||||
AdditionalData = new Dictionary<string, object> {
|
||||
{ "processedFiles", processedFiles },
|
||||
{ "newArtists", this.ArtistFactory.AddedArtistIds.Count() },
|
||||
{ "newReleases", this.ReleaseFactory.AddedReleaseIds.Count() },
|
||||
{ "newTracks", this.ReleaseFactory.AddedTrackIds.Count() }
|
||||
},
|
||||
|
||||
OperationTime = sw.ElapsedMilliseconds
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Perform any operations to the given folder before processing
|
||||
/// </summary>
|
||||
private bool PrePrecessFolder(DirectoryInfo inboundFolder, bool doJustInfo = false)
|
||||
{
|
||||
// If Folder name starts with "~" then remove the tilde and set all files in the folder artist to the folder name
|
||||
if (this.Configuration.GetValue<bool>("Processing:DoFolderArtistNameSet", true) && inboundFolder.Name.StartsWith("~"))
|
||||
{
|
||||
var artist = inboundFolder.Name.Replace("~", "");
|
||||
this.LoggingService.Info("Setting Folder File Tags To [{0}]", artist);
|
||||
if (!doJustInfo)
|
||||
{
|
||||
foreach (var file in inboundFolder.GetFiles("*.*", SearchOption.AllDirectories))
|
||||
{
|
||||
var extension = file.Extension.ToLower();
|
||||
if (extension.Equals(".mp3") || extension.Equals(".flac"))
|
||||
{
|
||||
var tagFile = TagLib.File.Create(file.FullName);
|
||||
tagFile.Tag.Performers = null;
|
||||
tagFile.Tag.Performers = new[] { artist };
|
||||
tagFile.Save();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Perform any operations to the given folder and the plugin results after processing
|
||||
/// </summary>
|
||||
private async Task<bool> PostProcessFolder(DirectoryInfo inboundFolder, IEnumerable<PluginResultInfo> pluginResults, bool doJustInfo)
|
||||
{
|
||||
SimpleContract.Requires<ArgumentNullException>(inboundFolder != null, "Invalid InboundFolder");
|
||||
if (!doJustInfo)
|
||||
{
|
||||
this.DeleteEmptyFolders(inboundFolder);
|
||||
}
|
||||
if (pluginResults != null)
|
||||
{
|
||||
//await Task.Run(() => Parallel.ForEach(pluginResults.GroupBy(x => x.ReleaseId).Select(x => x.First()), async releasesInfo =>
|
||||
//{
|
||||
// await this.ReleaseFactory.ScanReleaseFolder(releasesInfo.ReleaseId, this.DestinationRoot, doJustInfo);
|
||||
//}));
|
||||
|
||||
foreach (var releasesInfo in pluginResults.GroupBy(x => x.ReleaseId).Select(x => x.First()))
|
||||
{
|
||||
await this.ReleaseFactory.ScanReleaseFolder(releasesInfo.ReleaseId, this.DestinationRoot, doJustInfo);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public OperationResult<bool> DeleteEmptyFolders(DirectoryInfo processingFolder)
|
||||
{
|
||||
var result = new OperationResult<bool>();
|
||||
try
|
||||
{
|
||||
result.IsSuccess = FolderPathHelper.DeleteEmptyFolders(processingFolder);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this.LoggingService.Error(ex, string.Format("Error Deleting Empty Folder [{0}] Error [{1}]", processingFolder.FullName, ex.Serialize()));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
134
RoadieLibrary/Processors/ProcessorBase.cs
Normal file
|
@ -0,0 +1,134 @@
|
|||
using Roadie.Library.Caching;
|
||||
using Roadie.Library.Factories;
|
||||
using Roadie.Library.Utility;
|
||||
using Roadie.Library.Logging;
|
||||
using System.Linq;
|
||||
using Roadie.Library.Data;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
|
||||
namespace Roadie.Library.Processors
|
||||
{
|
||||
public abstract class ProcessorBase
|
||||
{
|
||||
protected readonly string _destinationRoot = null;
|
||||
protected readonly ICacheManager _cacheManager = null;
|
||||
protected readonly ILogger _logger = null;
|
||||
protected readonly IRoadieDbContext _dbContext = null;
|
||||
protected readonly IConfiguration _configuration = null;
|
||||
|
||||
protected ArtistFactory _artistFactory = null;
|
||||
protected ReleaseFactory _releaseFactory = null;
|
||||
protected ImageFactory _imageFactory = null;
|
||||
|
||||
public int? SubmissionId { get; set; }
|
||||
|
||||
protected string DestinationRoot
|
||||
{
|
||||
get
|
||||
{
|
||||
return this._destinationRoot;
|
||||
}
|
||||
}
|
||||
|
||||
protected ArtistFactory ArtistFactory
|
||||
{
|
||||
get
|
||||
{
|
||||
return this._artistFactory ?? (this._artistFactory = new ArtistFactory(this.Configuration, this.DbContext, this.CacheManager, this.LoggingService));
|
||||
}
|
||||
set
|
||||
{
|
||||
this._artistFactory = value;
|
||||
}
|
||||
}
|
||||
|
||||
protected ReleaseFactory ReleaseFactory
|
||||
{
|
||||
get
|
||||
{
|
||||
return this._releaseFactory ?? (this._releaseFactory = new ReleaseFactory(this.Configuration, this.DbContext, this.CacheManager, this.LoggingService));
|
||||
}
|
||||
set
|
||||
{
|
||||
this._releaseFactory = value;
|
||||
}
|
||||
}
|
||||
|
||||
protected ImageFactory ImageFactory
|
||||
{
|
||||
get
|
||||
{
|
||||
return this._imageFactory ?? (this._imageFactory = new ImageFactory(this.Configuration, this.DbContext, this.CacheManager, this.LoggingService));
|
||||
}
|
||||
set
|
||||
{
|
||||
this._imageFactory = value;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
protected bool DoMoveUnknowns
|
||||
{
|
||||
get
|
||||
{
|
||||
return SettingsHelper.Instance.Processing.DoMoveUnknowns;
|
||||
}
|
||||
}
|
||||
|
||||
protected bool DoDeleteUnknowns
|
||||
{
|
||||
get
|
||||
{
|
||||
return SettingsHelper.Instance.Processing.DoDeleteUnknowns;
|
||||
}
|
||||
}
|
||||
|
||||
protected string UnknownFolder
|
||||
{
|
||||
get
|
||||
{
|
||||
return SettingsHelper.Instance.Processing.UnknownFolder;
|
||||
}
|
||||
}
|
||||
protected ICacheManager CacheManager
|
||||
{
|
||||
get
|
||||
{
|
||||
return this._cacheManager;
|
||||
}
|
||||
}
|
||||
|
||||
protected ILogger LoggingService
|
||||
{
|
||||
get
|
||||
{
|
||||
return this._logger;
|
||||
}
|
||||
}
|
||||
|
||||
protected IRoadieDbContext DbContext
|
||||
{
|
||||
get
|
||||
{
|
||||
return this._dbContext;
|
||||
}
|
||||
}
|
||||
|
||||
protected IConfiguration Configuration
|
||||
{
|
||||
get
|
||||
{
|
||||
return this._configuration;
|
||||
}
|
||||
}
|
||||
|
||||
public ProcessorBase(IConfiguration configuration, string destinationRoot, IRoadieDbContext context, ICacheManager cacheManager, ILogger logger)
|
||||
{
|
||||
this._configuration = configuration;
|
||||
this._dbContext = context;
|
||||
this._destinationRoot = destinationRoot;
|
||||
this._cacheManager = cacheManager;
|
||||
this._logger = logger;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -6,11 +6,29 @@
|
|||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="HtmlAgilityPack" Version="1.8.9" />
|
||||
<PackageReference Include="Inflatable.Lastfm" Version="1.1.0.339" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="2.1.3" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="2.1.4" />
|
||||
<PackageReference Include="Microsoft.Extensions.Caching.Redis" Version="2.1.2" />
|
||||
<PackageReference Include="MimeMapping" Version="1.0.1.12" />
|
||||
<PackageReference Include="Orthogonal.NTagLite" Version="2.0.9" />
|
||||
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="2.1.2" />
|
||||
<PackageReference Include="RestSharp" Version="106.5.4" />
|
||||
<PackageReference Include="Serilog" Version="2.7.1" />
|
||||
<PackageReference Include="Serilog.Sinks.Console" Version="3.1.1" />
|
||||
<PackageReference Include="Serilog.Sinks.RollingFile" Version="3.3.0" />
|
||||
<PackageReference Include="SixLabors.ImageSharp" Version="1.0.0-beta0005" />
|
||||
<PackageReference Include="SixLabors.ImageSharp.Drawing" Version="1.0.0-beta0005" />
|
||||
<PackageReference Include="System.Runtime.Caching" Version="4.5.0" />
|
||||
<PackageReference Include="taglib" Version="2.1.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="Factories\" />
|
||||
<Folder Include="FilePlugins\" />
|
||||
<Folder Include="Processors\" />
|
||||
<Folder Include="SearchEngines\MetaData\Audio\" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
96
RoadieLibrary/SearchEngines/Imaging/BingImageResultModels.cs
Normal file
|
@ -0,0 +1,96 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Roadie.Library.SearchEngines.Imaging.BingModels
|
||||
{
|
||||
[Serializable]
|
||||
public class BingImageResult
|
||||
{
|
||||
public string _type { get; set; }
|
||||
public Instrumentation instrumentation { get; set; }
|
||||
public string webSearchUrl { get; set; }
|
||||
public int? totalEstimatedMatches { get; set; }
|
||||
public List<Value> value { get; set; }
|
||||
public List<Queryexpansion> queryExpansions { get; set; }
|
||||
public int nextOffsetAddCount { get; set; }
|
||||
public List<Pivotsuggestion> pivotSuggestions { get; set; }
|
||||
public bool? displayShoppingSourcesBadges { get; set; }
|
||||
public bool? displayRecipeSourcesBadges { get; set; }
|
||||
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public class Instrumentation
|
||||
{
|
||||
public string pageLoadPingUrl { get; set; }
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public class Value
|
||||
{
|
||||
public string name { get; set; }
|
||||
public string webSearchUrl { get; set; }
|
||||
public string thumbnailUrl { get; set; }
|
||||
public DateTime? datePublished { get; set; }
|
||||
public string contentUrl { get; set; }
|
||||
public string hostPageUrl { get; set; }
|
||||
public string contentSize { get; set; }
|
||||
public string encodingFormat { get; set; }
|
||||
public string hostPageDisplayUrl { get; set; }
|
||||
public int? width { get; set; }
|
||||
public int? height { get; set; }
|
||||
public Thumbnail thumbnail { get; set; }
|
||||
public string imageInsightsToken { get; set; }
|
||||
public Insightssourcessummary insightsSourcesSummary { get; set; }
|
||||
public string imageId { get; set; }
|
||||
public string accentColor { get; set; }
|
||||
}
|
||||
|
||||
public class Thumbnail
|
||||
{
|
||||
public int? width { get; set; }
|
||||
public int? height { get; set; }
|
||||
}
|
||||
|
||||
public class Insightssourcessummary
|
||||
{
|
||||
public int? shoppingSourcesCount { get; set; }
|
||||
public int? recipeSourcesCount { get; set; }
|
||||
}
|
||||
|
||||
public class Queryexpansion
|
||||
{
|
||||
public string text { get; set; }
|
||||
public string displayText { get; set; }
|
||||
public string webSearchUrl { get; set; }
|
||||
public string searchLink { get; set; }
|
||||
public Thumbnail1 thumbnail { get; set; }
|
||||
}
|
||||
|
||||
public class Thumbnail1
|
||||
{
|
||||
public string thumbnailUrl { get; set; }
|
||||
}
|
||||
|
||||
public class Pivotsuggestion
|
||||
{
|
||||
public string pivot { get; set; }
|
||||
public List<Suggestion> suggestions { get; set; }
|
||||
}
|
||||
|
||||
public class Suggestion
|
||||
{
|
||||
public string text { get; set; }
|
||||
public string displayText { get; set; }
|
||||
public string webSearchUrl { get; set; }
|
||||
public string searchLink { get; set; }
|
||||
public Thumbnail2 thumbnail { get; set; }
|
||||
}
|
||||
|
||||
public class Thumbnail2
|
||||
{
|
||||
public string thumbnailUrl { get; set; }
|
||||
}
|
||||
|
||||
|
||||
}
|
98
RoadieLibrary/SearchEngines/Imaging/BingImageSearchEngine.cs
Normal file
|
@ -0,0 +1,98 @@
|
|||
using RestSharp;
|
||||
using Roadie.Library.SearchEngines.Imaging.BingModels;
|
||||
using Roadie.Library.Utility;
|
||||
using Roadie.Library.Logging;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Security.Authentication;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Roadie.Library.Setttings;
|
||||
|
||||
namespace Roadie.Library.SearchEngines.Imaging
|
||||
{
|
||||
/// <summary>
|
||||
/// https://msdn.microsoft.com/en-us/library/dn760791(v=bsynd.50).aspx
|
||||
/// </summary>
|
||||
public class BingImageSearchEngine : ImageSearchEngineBase
|
||||
{
|
||||
|
||||
public BingImageSearchEngine(IConfiguration configuration, ILogger loggingService, string requestIp = null, string referrer = null)
|
||||
: base(configuration, loggingService, "https://api.cognitive.microsoft.com", requestIp, referrer)
|
||||
{
|
||||
this._apiKey = configuration.GetValue<List<ApiKey>>("ApiKeys", new List<ApiKey>()).FirstOrDefault(x => x.ApiName == "BingImageSearch") ?? new ApiKey();
|
||||
}
|
||||
|
||||
public override async Task<IEnumerable<ImageSearchResult>> PerformImageSearch(string query, int resultsCount)
|
||||
{
|
||||
var request = this.BuildRequest(query, resultsCount);
|
||||
|
||||
var response = await _client.ExecuteTaskAsync<BingImageResult>(request);
|
||||
|
||||
if (response.ResponseStatus == ResponseStatus.Error)
|
||||
{
|
||||
if (response.StatusCode == HttpStatusCode.Unauthorized)
|
||||
{
|
||||
throw new AuthenticationException("Api Key is not correct");
|
||||
}
|
||||
throw new Exception(string.Format("Request Error Message: {0}. Content: {1}.", response.ErrorMessage, response.Content));
|
||||
}
|
||||
if (response.Data == null || response.Data.value == null)
|
||||
{
|
||||
this.LoggingService.Warning("Response Is Null on PerformImageSearch [" + Newtonsoft.Json.JsonConvert.SerializeObject(response) + "]");
|
||||
return null;
|
||||
}
|
||||
return response.Data.value.Select(x => new ImageSearchResult
|
||||
{
|
||||
Width = (x.width ?? 0).ToString(),
|
||||
Height = (x.height ?? 0).ToString(),
|
||||
MediaUrl = x.contentUrl,
|
||||
Title = x.name
|
||||
}).ToArray();
|
||||
}
|
||||
|
||||
public override RestRequest BuildRequest(string query, int resultsCount)
|
||||
{
|
||||
var request = new RestRequest
|
||||
{
|
||||
Resource = "/bing/v5.0/images/search",
|
||||
Method = Method.GET,
|
||||
RequestFormat = DataFormat.Json
|
||||
};
|
||||
|
||||
request.AddHeader("Ocp-Apim-Subscription-Key", this.ApiKey.Key);
|
||||
|
||||
request.AddParameter(new Parameter
|
||||
{
|
||||
Name = "count",
|
||||
Value = resultsCount > 0 ? resultsCount : 10,
|
||||
Type = ParameterType.GetOrPost
|
||||
});
|
||||
|
||||
request.AddParameter(new Parameter
|
||||
{
|
||||
Name = "safeSearch",
|
||||
Value = "Off",
|
||||
Type = ParameterType.GetOrPost
|
||||
});
|
||||
|
||||
request.AddParameter(new Parameter
|
||||
{
|
||||
Name = "aspect",
|
||||
Value = "Square",
|
||||
Type = ParameterType.GetOrPost
|
||||
});
|
||||
|
||||
request.AddParameter(new Parameter
|
||||
{
|
||||
Name = "q",
|
||||
Value = string.Format("'{0}'", query.Trim()),
|
||||
Type = ParameterType.GetOrPost
|
||||
});
|
||||
|
||||
return request;
|
||||
}
|
||||
}
|
||||
}
|
13
RoadieLibrary/SearchEngines/Imaging/IImageSearchEngine.cs
Normal file
|
@ -0,0 +1,13 @@
|
|||
using RestSharp;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Roadie.Library.SearchEngines.Imaging
|
||||
{
|
||||
public interface IImageSearchEngine
|
||||
{
|
||||
Task<IEnumerable<ImageSearchResult>> PerformImageSearch(string query, int resultsCount);
|
||||
|
||||
RestRequest BuildRequest(string query, int resultsCount);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Roadie.Library.SearchEngines.Imaging
|
||||
{
|
||||
public class ITunesSearchResult
|
||||
{
|
||||
public int resultCount { get; set; }
|
||||
public List<Result> results { get; set; }
|
||||
}
|
||||
|
||||
public class Result
|
||||
{
|
||||
public string wrapperType { get; set; }
|
||||
public string collectionType { get; set; }
|
||||
public int artistId { get; set; }
|
||||
public int collectionId { get; set; }
|
||||
public int amgArtistId { get; set; }
|
||||
public string artistName { get; set; }
|
||||
public string artistType { get; set; }
|
||||
public string collectionName { get; set; }
|
||||
public string collectionCensoredName { get; set; }
|
||||
public string artistViewUrl { get; set; }
|
||||
public string artistLinkUrl { get; set; }
|
||||
public string collectionViewUrl { get; set; }
|
||||
public string artworkUrl60 { get; set; }
|
||||
public string artworkUrl100 { get; set; }
|
||||
public float collectionPrice { get; set; }
|
||||
public string collectionExplicitness { get; set; }
|
||||
public int trackCount { get; set; }
|
||||
public string copyright { get; set; }
|
||||
public string country { get; set; }
|
||||
public string currency { get; set; }
|
||||
public DateTime releaseDate { get; set; }
|
||||
public string primaryGenreName { get; set; }
|
||||
}
|
||||
}
|
237
RoadieLibrary/SearchEngines/Imaging/ITunesSearchEngine.cs
Normal file
|
@ -0,0 +1,237 @@
|
|||
using Roadie.Library.Caching;
|
||||
using RestSharp;
|
||||
using Roadie.Library.Extensions;
|
||||
using Roadie.Library.SearchEngines.MetaData;
|
||||
using Roadie.Library.Logging;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Security.Authentication;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
|
||||
namespace Roadie.Library.SearchEngines.Imaging
|
||||
{
|
||||
public class ITunesSearchEngine : ImageSearchEngineBase, IArtistSearchEngine, IReleaseSearchEngine
|
||||
{
|
||||
private readonly ICacheManager _cacheManager = null;
|
||||
|
||||
private ICacheManager CacheManager
|
||||
{
|
||||
get
|
||||
{
|
||||
return this._cacheManager;
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsEnabled
|
||||
{
|
||||
get
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public ITunesSearchEngine(IConfiguration configuration, ICacheManager cacheManager, ILogger logger, string requestIp = null, string referrer = null)
|
||||
: base(configuration, logger, "http://itunes.apple.com", requestIp, referrer)
|
||||
{
|
||||
this._cacheManager = cacheManager;
|
||||
}
|
||||
|
||||
|
||||
public override RestRequest BuildRequest(string query, int resultsCount)
|
||||
{
|
||||
return this.BuildRequest(query, resultsCount, "Release");
|
||||
}
|
||||
|
||||
private RestRequest BuildRequest(string query, int resultsCount, string entityType)
|
||||
{
|
||||
var request = new RestRequest
|
||||
{
|
||||
Resource = "search",
|
||||
Method = Method.GET,
|
||||
RequestFormat = DataFormat.Json
|
||||
};
|
||||
|
||||
if (resultsCount > 0)
|
||||
{
|
||||
request.AddParameter(new Parameter
|
||||
{
|
||||
Name = "limit",
|
||||
Value = resultsCount,
|
||||
Type = ParameterType.GetOrPost
|
||||
});
|
||||
}
|
||||
|
||||
request.AddParameter(new Parameter
|
||||
{
|
||||
Name = "entity",
|
||||
Value = entityType,
|
||||
Type = ParameterType.GetOrPost
|
||||
});
|
||||
|
||||
request.AddParameter(new Parameter
|
||||
{
|
||||
Name = "country",
|
||||
Value = "us",
|
||||
Type = ParameterType.GetOrPost
|
||||
});
|
||||
|
||||
request.AddParameter(new Parameter
|
||||
{
|
||||
Name = "term",
|
||||
Value = string.Format("'{0}'", query.Trim()),
|
||||
Type = ParameterType.GetOrPost
|
||||
});
|
||||
|
||||
return request;
|
||||
}
|
||||
|
||||
public override async Task<IEnumerable<ImageSearchResult>> PerformImageSearch(string query, int resultsCount)
|
||||
{
|
||||
var request = this.BuildRequest(query, resultsCount);
|
||||
ImageSearchResult[] result = null;
|
||||
try
|
||||
{
|
||||
var response = _client.Execute<ITunesSearchResult>(request);
|
||||
if (response.ResponseStatus == ResponseStatus.Error)
|
||||
{
|
||||
if (response.StatusCode == HttpStatusCode.Unauthorized)
|
||||
{
|
||||
throw new AuthenticationException("Unauthorized");
|
||||
}
|
||||
throw new Exception(string.Format("Request Error Message: {0}. Content: {1}.", response.ErrorMessage, response.Content));
|
||||
}
|
||||
if (response.Data.results == null)
|
||||
{
|
||||
return new ImageSearchResult[0];
|
||||
}
|
||||
result = response.Data.results.Select(x => new ImageSearchResult
|
||||
{
|
||||
ArtistId = x.artistId.ToString(),
|
||||
ArtistName = x.artistName,
|
||||
MediaUrl = x.artworkUrl100,
|
||||
Height = "100",
|
||||
Width = "100",
|
||||
Title = x.collectionName
|
||||
}).ToArray();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this.LoggingService.Error(ex.Serialize());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public async Task<OperationResult<IEnumerable<ArtistSearchResult>>> PerformArtistSearch(string query, int resultsCount)
|
||||
{
|
||||
ArtistSearchResult data = null;
|
||||
|
||||
try
|
||||
{
|
||||
var request = this.BuildRequest(query, 1, "musicArtist");
|
||||
var response = await _client.ExecuteTaskAsync<ITunesSearchResult>(request);
|
||||
if (response.ResponseStatus == ResponseStatus.Error)
|
||||
{
|
||||
if (response.StatusCode == HttpStatusCode.Unauthorized)
|
||||
{
|
||||
throw new AuthenticationException("Unauthorized");
|
||||
}
|
||||
throw new Exception(string.Format("Request Error Message: {0}. Content: {1}.", response.ErrorMessage, response.Content));
|
||||
}
|
||||
Result responseData = response.Data.resultCount > 0 && response.Data.results != null ? response.Data.results.First() : null;
|
||||
if (responseData != null)
|
||||
{
|
||||
var urls = new List<string>();
|
||||
if (!string.IsNullOrEmpty(responseData.artistLinkUrl))
|
||||
{
|
||||
urls.Add(responseData.artistLinkUrl);
|
||||
}
|
||||
if (!string.IsNullOrEmpty(responseData.artistViewUrl))
|
||||
{
|
||||
urls.Add(responseData.artistViewUrl);
|
||||
}
|
||||
if (!string.IsNullOrEmpty(responseData.collectionViewUrl))
|
||||
{
|
||||
urls.Add(responseData.collectionViewUrl);
|
||||
}
|
||||
data = new ArtistSearchResult
|
||||
{
|
||||
ArtistName = responseData.artistName,
|
||||
iTunesId = responseData.artistId.ToString(),
|
||||
AmgId = responseData.amgArtistId.ToString(),
|
||||
ArtistType = responseData.artistType,
|
||||
ArtistThumbnailUrl = responseData.artworkUrl100,
|
||||
ArtistGenres = new string[] { responseData.primaryGenreName },
|
||||
Urls = urls
|
||||
};
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this.LoggingService.Error(ex);
|
||||
}
|
||||
return new OperationResult<IEnumerable<ArtistSearchResult>>
|
||||
{
|
||||
IsSuccess = data != null,
|
||||
Data = new ArtistSearchResult[] { data }
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<OperationResult<IEnumerable<ReleaseSearchResult>>> PerformReleaseSearch(string artistName, string query, int resultsCount)
|
||||
{
|
||||
var request = this.BuildRequest(query, 1, "album");
|
||||
var response = await _client.ExecuteTaskAsync<ITunesSearchResult>(request);
|
||||
if (response.ResponseStatus == ResponseStatus.Error)
|
||||
{
|
||||
if (response.StatusCode == HttpStatusCode.Unauthorized)
|
||||
{
|
||||
throw new AuthenticationException("Unauthorized");
|
||||
}
|
||||
throw new Exception(string.Format("Request Error Message: {0}. Content: {1}.", response.ErrorMessage, response.Content));
|
||||
}
|
||||
ReleaseSearchResult data = null;
|
||||
try
|
||||
{
|
||||
Result responseData = response.Data.results != null && response.Data.results.Any() ? response.Data.results.First() : null;
|
||||
if (responseData != null)
|
||||
{
|
||||
var urls = new List<string>();
|
||||
if (!string.IsNullOrEmpty(responseData.artistLinkUrl))
|
||||
{
|
||||
urls.Add(responseData.artistLinkUrl);
|
||||
}
|
||||
if (!string.IsNullOrEmpty(responseData.artistViewUrl))
|
||||
{
|
||||
urls.Add(responseData.artistViewUrl);
|
||||
}
|
||||
if (!string.IsNullOrEmpty(responseData.collectionViewUrl))
|
||||
{
|
||||
urls.Add(responseData.collectionViewUrl);
|
||||
}
|
||||
data = new ReleaseSearchResult
|
||||
{
|
||||
ReleaseTitle = responseData.collectionName,
|
||||
iTunesId = responseData.artistId.ToString(),
|
||||
AmgId = responseData.amgArtistId.ToString(),
|
||||
ReleaseType = responseData.collectionType,
|
||||
ReleaseThumbnailUrl = responseData.artworkUrl100,
|
||||
ReleaseGenres = new string[] { responseData.primaryGenreName },
|
||||
Urls = urls
|
||||
};
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this.LoggingService.Error(ex);
|
||||
}
|
||||
return new OperationResult<IEnumerable<ReleaseSearchResult>>
|
||||
{
|
||||
IsSuccess = data != null,
|
||||
Data = new ReleaseSearchResult[] { data }
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
83
RoadieLibrary/SearchEngines/Imaging/ImageSearchEngineBase.cs
Normal file
|
@ -0,0 +1,83 @@
|
|||
using RestSharp;
|
||||
using Roadie.Library.Utility;
|
||||
using Roadie.Library.Logging;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Net;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Roadie.Library.Setttings;
|
||||
|
||||
namespace Roadie.Library.SearchEngines.Imaging
|
||||
{
|
||||
public abstract class ImageSearchEngineBase : IImageSearchEngine
|
||||
{
|
||||
protected readonly string _requestIp = null;
|
||||
protected readonly string _referrer = null;
|
||||
protected readonly RestClient _client = null;
|
||||
|
||||
protected ILogger _loggingService = null;
|
||||
protected ILogger LoggingService
|
||||
{
|
||||
get
|
||||
{
|
||||
return this._loggingService;
|
||||
}
|
||||
}
|
||||
|
||||
protected readonly IConfiguration _configuratio = null;
|
||||
protected IConfiguration Configuration
|
||||
{
|
||||
get
|
||||
{
|
||||
return this._configuratio;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
protected ApiKey _apiKey = null;
|
||||
protected ApiKey ApiKey
|
||||
{
|
||||
get
|
||||
{
|
||||
return this._apiKey;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public ImageSearchEngineBase(IConfiguration configuration, ILogger loggingService, string baseUrl, string requestIp = null, string referrer = null)
|
||||
{
|
||||
this._configuratio = configuration;
|
||||
if (string.IsNullOrEmpty(referrer) || referrer.StartsWith("http://localhost"))
|
||||
{
|
||||
referrer = "http://github.com/sphildreth/Roadie";
|
||||
}
|
||||
this._referrer = referrer;
|
||||
if (string.IsNullOrEmpty(requestIp) || requestIp == "::1")
|
||||
{
|
||||
requestIp = "192.30.252.128";
|
||||
}
|
||||
this._requestIp = requestIp;
|
||||
this._loggingService = loggingService;
|
||||
|
||||
this._client = new RestClient(baseUrl);
|
||||
this._client.UserAgent = WebHelper.UserAgent;
|
||||
|
||||
System.Net.ServicePointManager.ServerCertificateValidationCallback += delegate (object sender, System.Security.Cryptography.X509Certificates.X509Certificate certificate,
|
||||
System.Security.Cryptography.X509Certificates.X509Chain chain,
|
||||
System.Net.Security.SslPolicyErrors sslPolicyErrors)
|
||||
{
|
||||
return true; // **** Always accept
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
public abstract RestRequest BuildRequest(string query, int resultsCount);
|
||||
|
||||
|
||||
public virtual async Task<IEnumerable<ImageSearchResult>> PerformImageSearch(string query, int resultsCount)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
}
|
55
RoadieLibrary/SearchEngines/Imaging/ImageSearchManager.cs
Normal file
|
@ -0,0 +1,55 @@
|
|||
using Roadie.Library.Caching;
|
||||
using Roadie.Library.Utility;
|
||||
using Roadie.Library.Logging;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Roadie.Library.Imaging;
|
||||
|
||||
namespace Roadie.Library.SearchEngines.Imaging
|
||||
{
|
||||
public class ImageSearchManager
|
||||
{
|
||||
private readonly IImageSearchEngine _bingSearchEngine = null;
|
||||
private readonly IImageSearchEngine _itunesSearchEngine = null;
|
||||
|
||||
private int DefaultResultsCount
|
||||
{
|
||||
get
|
||||
{
|
||||
return 10;
|
||||
}
|
||||
}
|
||||
|
||||
public ImageSearchManager(ICacheManager cacheManager, ILogger loggingService, string requestIp = null, string referrer = null)
|
||||
{
|
||||
this._bingSearchEngine = new BingImageSearchEngine(loggingService, requestIp, referrer);
|
||||
this._itunesSearchEngine = new ITunesSearchEngine(cacheManager, loggingService, requestIp, referrer);
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<ImageSearchResult>> ImageSearch(string query, int? resultsCount = null)
|
||||
{
|
||||
var count = resultsCount ?? this.DefaultResultsCount;
|
||||
var result = new List<ImageSearchResult>();
|
||||
|
||||
if (WebHelper.IsStringUrl(query))
|
||||
{
|
||||
var s = ImageHelper.ImageSearchResultForImageUrl(query);
|
||||
if (s != null)
|
||||
{
|
||||
result.Add(s);
|
||||
}
|
||||
}
|
||||
var bingResults = await this._bingSearchEngine.PerformImageSearch(query, count);
|
||||
if (bingResults != null)
|
||||
{
|
||||
result.AddRange(bingResults);
|
||||
}
|
||||
var iTunesResults = await this._itunesSearchEngine.PerformImageSearch(query, count);
|
||||
if (iTunesResults != null)
|
||||
{
|
||||
result.AddRange(iTunesResults);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
40
RoadieLibrary/SearchEngines/Imaging/ImageSearchResult.cs
Normal file
|
@ -0,0 +1,40 @@
|
|||
using System;
|
||||
|
||||
namespace Roadie.Library.SearchEngines.Imaging
|
||||
{
|
||||
[Serializable]
|
||||
public class ImageSearchResult
|
||||
{
|
||||
public __Metadata __metadata { get; set; }
|
||||
public string ID { get; set; }
|
||||
public string ArtistId { get; set; }
|
||||
public string ArtistName { get; set; }
|
||||
public string Title { get; set; }
|
||||
public string MediaUrl { get; set; }
|
||||
public string SourceUrl { get; set; }
|
||||
public string DisplayUrl { get; set; }
|
||||
public string Width { get; set; }
|
||||
public string Height { get; set; }
|
||||
public string FileSize { get; set; }
|
||||
public string ContentType { get; set; }
|
||||
public Thumbnail Thumbnail { get; set; }
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public class __Metadata
|
||||
{
|
||||
public string uri { get; set; }
|
||||
public string type { get; set; }
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public class Thumbnail
|
||||
{
|
||||
public __Metadata __metadata { get; set; }
|
||||
public string MediaUrl { get; set; }
|
||||
public string ContentType { get; set; }
|
||||
public string Width { get; set; }
|
||||
public string Height { get; set; }
|
||||
public string FileSize { get; set; }
|
||||
}
|
||||
}
|
20
RoadieLibrary/SearchEngines/MetaData/ArtistSearchResult.cs
Normal file
|
@ -0,0 +1,20 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Roadie.Library.SearchEngines.MetaData
|
||||
{
|
||||
[Serializable]
|
||||
public class ArtistSearchResult : SearchResultBase
|
||||
{
|
||||
public DateTime? BirthDate { get; set; }
|
||||
public DateTime? EndDate { get; set; }
|
||||
public DateTime? BeginDate { get; set; }
|
||||
public ICollection<string> ArtistGenres { get; set; }
|
||||
public string ArtistName { get; set; }
|
||||
public string ArtistRealName { get; set; }
|
||||
public string ArtistSortName { get; set; }
|
||||
public string ArtistThumbnailUrl { get; set; }
|
||||
public string ArtistType { get; set; }
|
||||
public ICollection<ReleaseSearchResult> Releases { get; set; }
|
||||
}
|
||||
}
|
388
RoadieLibrary/SearchEngines/MetaData/Audio/AudioMetaData.cs
Normal file
|
@ -0,0 +1,388 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using Roadie.Library.Extensions;
|
||||
using System.IO;
|
||||
|
||||
namespace Roadie.Library.MetaData.Audio
|
||||
{
|
||||
[Serializable]
|
||||
[DebuggerDisplay("Artist: {Artist}, TrackArtist: {TrackArtist}, Release: {Release}, TrackNumber: {TrackNumber}, Title: {Title}, Year: {Year}")]
|
||||
public sealed class AudioMetaData
|
||||
{
|
||||
public const char ArtistSplitCharacter = '/';
|
||||
|
||||
private bool _doModifyArtistNameOnGet = true;
|
||||
|
||||
private string _trackArtist = null;
|
||||
private string _artist = null;
|
||||
|
||||
public const string SoundTrackArtist = "Sound Tracks";
|
||||
public const int MinimumYearValue = 1900;
|
||||
|
||||
/// <summary>
|
||||
/// Full filename to the file used to get this AudioMetaData
|
||||
/// </summary>
|
||||
public string Filename { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Directory holding file used to get this AudioMetaData
|
||||
/// </summary>
|
||||
public string Directory
|
||||
{
|
||||
get
|
||||
{
|
||||
if(string.IsNullOrEmpty(this.Filename))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
return Path.GetDirectoryName(this.Filename);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// TPE1 First Lead Artist
|
||||
/// </summary>
|
||||
public string Artist
|
||||
{
|
||||
get
|
||||
{
|
||||
if (this._doModifyArtistNameOnGet)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(this._artist) && this._artist.Contains(AudioMetaData.ArtistSplitCharacter.ToString()))
|
||||
{
|
||||
return this._artist.Split(AudioMetaData.ArtistSplitCharacter).First();
|
||||
}
|
||||
}
|
||||
return this._artist;
|
||||
}
|
||||
set
|
||||
{
|
||||
this._artist = value;
|
||||
if (!string.IsNullOrEmpty(this._artist))
|
||||
{
|
||||
this._artist = this._artist.Replace(';', AudioMetaData.ArtistSplitCharacter);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public ICollection<string> Genres { get; set; }
|
||||
|
||||
public string ArtistRaw { get; set; }
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// TPE1 All Lead Artists
|
||||
/// <seealso cref="http://id3.org/id3v2.3.0"/>
|
||||
/// </summary>
|
||||
/// <remarks>Per ID3.Org Spec: The 'Lead artist(s)/Lead performer(s)/Soloist(s)/Performing group' is used for the main artist(s). They are seperated with the "/" character.</remarks>
|
||||
public IEnumerable<string> Artists
|
||||
{
|
||||
get
|
||||
{
|
||||
if(string.IsNullOrEmpty(this._artist))
|
||||
{
|
||||
return new string[0];
|
||||
}
|
||||
if(!this._artist.Contains(AudioMetaData.ArtistSplitCharacter.ToString()))
|
||||
{
|
||||
return new string[0];
|
||||
}
|
||||
return this._artist.Split(AudioMetaData.ArtistSplitCharacter).Select(x => x.ToTitleCase()).ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// TOPE First Contributing Artist
|
||||
/// </summary>
|
||||
public string TrackArtist
|
||||
{
|
||||
get
|
||||
{
|
||||
if (!string.IsNullOrEmpty(this._trackArtist) && this._trackArtist.Contains(AudioMetaData.ArtistSplitCharacter.ToString()))
|
||||
{
|
||||
return this._trackArtist.Split(AudioMetaData.ArtistSplitCharacter).First().ToTitleCase();
|
||||
}
|
||||
if(!string.IsNullOrEmpty(this._artist) || !string.IsNullOrEmpty(this._trackArtist))
|
||||
{
|
||||
return !this._artist.Equals(this._trackArtist, StringComparison.OrdinalIgnoreCase) ? this._trackArtist : null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
set
|
||||
{
|
||||
this._trackArtist = value;
|
||||
if (!string.IsNullOrEmpty(this._trackArtist))
|
||||
{
|
||||
this._trackArtist = this._trackArtist.Replace(';', AudioMetaData.ArtistSplitCharacter).ToTitleCase();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public string TrackArtistRaw { get; set; }
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// TOPE All Contributing Artists
|
||||
/// </summary>
|
||||
/// <remarks>Per ID3.Org Spec: They are seperated with the "/" character.</remarks>
|
||||
public IEnumerable<string> TrackArtists
|
||||
{
|
||||
get
|
||||
{
|
||||
if (string.IsNullOrEmpty(this._trackArtist))
|
||||
{
|
||||
return new string[0];
|
||||
}
|
||||
if (!this._trackArtist.Contains(AudioMetaData.ArtistSplitCharacter.ToString()))
|
||||
{
|
||||
return new string[1] { this.TrackArtist };
|
||||
}
|
||||
if (!string.IsNullOrEmpty(this._artist) || !string.IsNullOrEmpty(this._trackArtist))
|
||||
{
|
||||
if(!this._artist.Equals(this._trackArtist, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return this._trackArtist.Split(AudioMetaData.ArtistSplitCharacter).Select(x => x.ToTitleCase()).ToArray();
|
||||
}
|
||||
}
|
||||
return new string[0];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// TALB
|
||||
/// </summary>
|
||||
public string Release { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// TIT2
|
||||
/// </summary>
|
||||
private string _title = null;
|
||||
|
||||
public string Title
|
||||
{
|
||||
get
|
||||
{
|
||||
return this.SpecialTitle ?? this._title;
|
||||
}
|
||||
set
|
||||
{
|
||||
this._title = value;
|
||||
if (IsSoundTrack)
|
||||
{
|
||||
this.Artist = AudioMetaData.SoundTrackArtist;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public string SpecialTitle { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// TYER
|
||||
/// </summary>
|
||||
private int? _year = null;
|
||||
|
||||
public int? Year
|
||||
{
|
||||
get
|
||||
{
|
||||
return this._year;
|
||||
}
|
||||
set
|
||||
{
|
||||
this._year = value < AudioMetaData.MinimumYearValue ? null : value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// TPOS
|
||||
/// </summary>
|
||||
public int? Disk { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// TRCK
|
||||
/// </summary>
|
||||
public short? TrackNumber { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// TRCK 0[/OptionalElements]
|
||||
/// </summary>
|
||||
public int? TotalTrackNumbers { get; set; }
|
||||
|
||||
public int? AudioBitrate { get; set; }
|
||||
|
||||
public int? AudioChannels { get; set; }
|
||||
|
||||
public int? AudioSampleRate { get; set; }
|
||||
|
||||
public string ReleaseMusicBrainzId { get; set; }
|
||||
|
||||
public string ReleaseLastFmId { get; set; }
|
||||
|
||||
public string LastFmId { get; set; }
|
||||
|
||||
public string MusicBrainzId { get; set; }
|
||||
public string AmgId { get; set; }
|
||||
|
||||
public string SpotifyId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// TIME
|
||||
/// </summary>
|
||||
public TimeSpan? Time { get; set; }
|
||||
|
||||
public ulong? SampleLength { get; set; }
|
||||
|
||||
public IEnumerable<AudioMetaDataImage> Images { get; set; }
|
||||
|
||||
public double TotalSeconds
|
||||
{
|
||||
get
|
||||
{
|
||||
if (this.Time == null)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
return this.Time.Value.TotalSeconds;
|
||||
}
|
||||
}
|
||||
|
||||
public AudioMetaDataWeights AudioMetaDataWeights
|
||||
{
|
||||
get
|
||||
{
|
||||
var result = AudioMetaDataWeights.None;
|
||||
if (!string.IsNullOrEmpty(this.Artist))
|
||||
{
|
||||
result |= AudioMetaDataWeights.Artist;
|
||||
}
|
||||
if (!string.IsNullOrEmpty(this.Title))
|
||||
{
|
||||
result |= AudioMetaDataWeights.Time;
|
||||
}
|
||||
if ((this.Year ?? 0) > 1)
|
||||
{
|
||||
result |= AudioMetaDataWeights.Year;
|
||||
}
|
||||
if ((this.TrackNumber ?? 0) > 1)
|
||||
{
|
||||
result |= AudioMetaDataWeights.TrackNumber;
|
||||
}
|
||||
if (this.TotalSeconds > 1)
|
||||
{
|
||||
result |= AudioMetaDataWeights.Time;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
public int ValidWeight
|
||||
{
|
||||
get
|
||||
{
|
||||
return (int)this.AudioMetaDataWeights;
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsValid
|
||||
{
|
||||
get
|
||||
{
|
||||
try
|
||||
{
|
||||
this.Artist = this.Artist == null ? null : this.Artist.Equals("Unknown Artist") ? null : this.Artist;
|
||||
this.Release = this.Release == null ? null : this.Release.Equals("Unknown Release") ? null : this.Release;
|
||||
if (!string.IsNullOrEmpty(this.Title))
|
||||
{
|
||||
var trackNumberTitle = string.Format("Track {0}", this.TrackNumber);
|
||||
this.Title = this.Title == trackNumberTitle ? null : this.Title;
|
||||
}
|
||||
return !string.IsNullOrEmpty(this.Artist)
|
||||
&& !string.IsNullOrEmpty(this.Release)
|
||||
&& !string.IsNullOrEmpty(this.Title)
|
||||
&& (this.Year ?? 0) > 0
|
||||
&& (this.TrackNumber ?? 0) > 0;
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public AudioMetaData()
|
||||
{
|
||||
this.Images = new AudioMetaDataImage[0];
|
||||
}
|
||||
|
||||
public override bool Equals(object obj)
|
||||
{
|
||||
var item = obj as AudioMetaData;
|
||||
if (item == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return item.GetHashCode() == this.GetHashCode();
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
unchecked
|
||||
{
|
||||
int hash = 17;
|
||||
hash = hash * 23 + this.Artist.GetHashCode();
|
||||
hash = hash * 23 + this.Release.GetHashCode();
|
||||
hash = hash * 23 + this.Title.GetHashCode();
|
||||
hash = hash * 23 + this.TrackNumber.GetHashCode();
|
||||
hash = hash * 23 + this.AudioBitrate.GetHashCode();
|
||||
hash = hash * 23 + this.AudioSampleRate.GetHashCode();
|
||||
return hash;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Use this value for the artist name, dont process in any way
|
||||
/// </summary>
|
||||
/// <param name="name"></param>
|
||||
public void SetArtistName(string name)
|
||||
{
|
||||
this._artist = name;
|
||||
this._doModifyArtistNameOnGet = false;
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return string.Format("IsValid: {0}{7}, ValidWeight {1}, Artist: {2}, Release: {3}, TrackNumber: {4}, Title: {5}, Year: {6}, Duration: {8}",
|
||||
this.IsValid,
|
||||
this.ValidWeight,
|
||||
this.Artist,
|
||||
this.Release,
|
||||
this.TrackNumber,
|
||||
this.Title,
|
||||
this.Year,
|
||||
this.IsSoundTrack ? " [SoundTrack ]" : string.Empty,
|
||||
this.Time == null ? "-" : this.Time.Value.ToString());
|
||||
}
|
||||
|
||||
public bool IsSoundTrack
|
||||
{
|
||||
get
|
||||
{
|
||||
if (this.Genres != null && this.Genres.Any())
|
||||
{
|
||||
var soundtrackGenres = new List<string> { "24", "soundtrack" };
|
||||
if (this.Genres.Intersect(soundtrackGenres, StringComparer.OrdinalIgnoreCase).Any())
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public string ISRC { get; internal set; }
|
||||
}
|
||||
}
|
|
@ -0,0 +1,377 @@
|
|||
using Roadie.Library.Caching;
|
||||
using Roadie.Library.Extensions;
|
||||
using Roadie.Library.Factories;
|
||||
using Roadie.Library.MetaData.FileName;
|
||||
using Roadie.Library.MetaData.ID3Tags;
|
||||
using Roadie.Library.MetaData.LastFm;
|
||||
using Roadie.Library.MetaData.MusicBrainz;
|
||||
using Roadie.Library.Utility;
|
||||
using Roadie.Library.Logging;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Roadie.Library.Data;
|
||||
|
||||
namespace Roadie.Library.MetaData.Audio
|
||||
{
|
||||
public sealed class AudioMetaDataHelper : IDisposable
|
||||
{
|
||||
private readonly IConfiguration _configuration = null;
|
||||
private readonly IRoadieDbContext _dbContext = null;
|
||||
private readonly ICacheManager _cacheManager = null;
|
||||
private readonly ILogger _logger = null;
|
||||
private readonly MusicBrainzProvider _musicBrainzProvider = null;
|
||||
private readonly LastFmHelper _lastFmHelper = null;
|
||||
private readonly FileNameHelper _fileNameHelper = null;
|
||||
private ID3TagsHelper _id3TagsHelper = null;
|
||||
private ImageFactory _imageFactory = null;
|
||||
|
||||
private IConfiguration Configuration
|
||||
{
|
||||
get
|
||||
{
|
||||
return this._configuration;
|
||||
}
|
||||
}
|
||||
|
||||
private ICacheManager CacheManager
|
||||
{
|
||||
get
|
||||
{
|
||||
return this._cacheManager;
|
||||
}
|
||||
}
|
||||
|
||||
private ILogger Logger
|
||||
{
|
||||
get
|
||||
{
|
||||
return this._logger;
|
||||
}
|
||||
}
|
||||
|
||||
private MusicBrainzProvider MusicBrainzProvider
|
||||
{
|
||||
get
|
||||
{
|
||||
return this._musicBrainzProvider;
|
||||
}
|
||||
}
|
||||
|
||||
private LastFmHelper LastFmHelper
|
||||
{
|
||||
get
|
||||
{
|
||||
return this._lastFmHelper;
|
||||
}
|
||||
}
|
||||
|
||||
private FileNameHelper FileNameHelper
|
||||
{
|
||||
get
|
||||
{
|
||||
return this._fileNameHelper;
|
||||
}
|
||||
}
|
||||
|
||||
private ID3TagsHelper ID3TagsHelper
|
||||
{
|
||||
get
|
||||
{
|
||||
return this._id3TagsHelper ?? (this._id3TagsHelper = new ID3TagsHelper(this.Configuration, this.CacheManager, this.Logger));
|
||||
}
|
||||
set
|
||||
{
|
||||
this._id3TagsHelper = value;
|
||||
}
|
||||
}
|
||||
|
||||
private IRoadieDbContext DBContext
|
||||
{
|
||||
get
|
||||
{
|
||||
return this._dbContext;
|
||||
}
|
||||
}
|
||||
|
||||
private ImageFactory ImageFactory
|
||||
{
|
||||
get
|
||||
{
|
||||
return this._imageFactory ?? (this._imageFactory = new ImageFactory(this.Configuration, this.DBContext, this.CacheManager, this.Logger));
|
||||
}
|
||||
set
|
||||
{
|
||||
this._imageFactory = value;
|
||||
}
|
||||
}
|
||||
|
||||
private IntPtr nativeResource = Marshal.AllocHGlobal(100);
|
||||
|
||||
public bool DoParseFromFileName { get; set; }
|
||||
public bool DoParseFromDiscogsDBFindingTrackForArtist { get; set; }
|
||||
public bool DoParseFromDiscogsDB { get; set; }
|
||||
public bool DoParseFromMusicBrainz { get; set; }
|
||||
public bool DoParseFromLastFM { get; set; }
|
||||
|
||||
public AudioMetaDataHelper(IConfiguration configuration, IRoadieDbContext context, MusicBrainzProvider musicBrainzHelper, LastFmHelper lastFmHelper, ICacheManager cacheManager, ILogger logger, ImageFactory imageFactory = null)
|
||||
{
|
||||
this._configuration = configuration;
|
||||
this._dbContext = context;
|
||||
this._cacheManager = cacheManager;
|
||||
this._logger = logger;
|
||||
this._imageFactory = imageFactory;
|
||||
this._fileNameHelper = new FileNameHelper(cacheManager, logger);
|
||||
|
||||
this._musicBrainzProvider = musicBrainzHelper;
|
||||
this._lastFmHelper = lastFmHelper;
|
||||
|
||||
this.DoParseFromFileName = configuration.GetValue<bool>("Processing:DoParseFromFileName", true);
|
||||
this.DoParseFromDiscogsDBFindingTrackForArtist = configuration.GetValue<bool>("Processing:DoParseFromDiscogsDBFindingTrackForArtist", true);
|
||||
this.DoParseFromDiscogsDB = configuration.GetValue<bool>("Processing:DoParseFromDiscogsDB", true);
|
||||
this.DoParseFromMusicBrainz = configuration.GetValue<bool>("Processing:DoParseFromMusicBrainz", true);
|
||||
this.DoParseFromLastFM = configuration.GetValue<bool>("Processing:DoParseFromLastFM", true);
|
||||
}
|
||||
|
||||
#region IDisposable Implementation
|
||||
|
||||
~AudioMetaDataHelper()
|
||||
{
|
||||
Dispose(false);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
public void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
//if(this._discogsDB != null)
|
||||
//{
|
||||
// try
|
||||
// {
|
||||
// this._discogsDB.Dispose();
|
||||
// }
|
||||
// finally
|
||||
// {
|
||||
// this._discogsDB = null;
|
||||
// }
|
||||
//}
|
||||
}
|
||||
if (nativeResource != IntPtr.Zero)
|
||||
{
|
||||
Marshal.FreeHGlobal(nativeResource);
|
||||
nativeResource = IntPtr.Zero;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion IDisposable Implementation
|
||||
|
||||
/// <summary>
|
||||
/// For the given File extract out all the information if successfully pulled out then return true
|
||||
/// </summary>
|
||||
/// <param name="fileInfo">FileInfo to Process</param>
|
||||
/// <param name="doJustInfo">Toggle To Only Print Info Not Modify Files</param>
|
||||
/// <returns>If parsing information for File was successful</returns>
|
||||
public async Task<AudioMetaData> GetInfo(FileInfo fileInfo, bool doJustInfo = false)
|
||||
{
|
||||
var tagSources = new List<string> { "Tags" };
|
||||
var result = this.ParseFromTags(fileInfo);
|
||||
result.Filename = fileInfo.FullName;
|
||||
if (!result.IsValid)
|
||||
{
|
||||
tagSources.Add("Filename");
|
||||
result = this.ParseFromFilename(result, fileInfo);
|
||||
if (string.IsNullOrEmpty(result.Artist) || string.IsNullOrEmpty(result.Release))
|
||||
{
|
||||
if (string.IsNullOrEmpty(result.Artist) || string.IsNullOrEmpty(result.Release))
|
||||
{
|
||||
this.Logger.Warning("File [{0}] MetaData [{1}]: Unable to Determine Artist and Release; aborting getting info.", fileInfo.FullName, result.ToString());
|
||||
return result;
|
||||
}
|
||||
}
|
||||
if (!result.IsValid)
|
||||
{
|
||||
if (!result.IsValid)
|
||||
{
|
||||
tagSources.Add("MusicBrainz");
|
||||
result = await this.ParseFromMusicBrainz(result);
|
||||
if (!result.IsValid)
|
||||
{
|
||||
tagSources.Add("LastFm");
|
||||
result = await this.GetFromLastFmIntegration(result);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!result.IsValid)
|
||||
{
|
||||
this.Logger.Warning("File [{0}] MetaData Invalid, TagSources [{1}] MetaData [{2}]", fileInfo.FullName, string.Join(",", tagSources), result.ToString());
|
||||
}
|
||||
else
|
||||
{
|
||||
if (result.IsValid && !doJustInfo)
|
||||
{
|
||||
if (result.Images == null || !result.Images.Any())
|
||||
{
|
||||
var imageMetaData = this.ImageFactory.GetPictureForMetaData(fileInfo.FullName, result);
|
||||
var tagImages = imageMetaData == null ? null : new List<AudioMetaDataImage> { imageMetaData };
|
||||
result.Images = tagImages != null && tagImages.Any() ? tagImages : null;
|
||||
if (result.Images == null || !result.Images.Any())
|
||||
{
|
||||
this.Logger.Trace("File [{0} No Images Set and Unable to Find Images", fileInfo.FullName);
|
||||
}
|
||||
}
|
||||
this.WriteTags(result, fileInfo);
|
||||
}
|
||||
}
|
||||
}
|
||||
var artistNameReplacements = this.Configuration.GetValue<Dictionary<string, List<string>>>("ArtistNameReplace");
|
||||
if (artistNameReplacements != null)
|
||||
{
|
||||
var artistNameReplaceKp = artistNameReplacements.FirstOrDefault(x => x.Value.Any(v => v.Equals(result.ArtistRaw, StringComparison.OrdinalIgnoreCase)));
|
||||
if (artistNameReplaceKp.Key != null && artistNameReplaceKp.Key != result.Artist)
|
||||
{
|
||||
result.SetArtistName(artistNameReplaceKp.Key);
|
||||
}
|
||||
}
|
||||
this.Logger.Info("File [{0}], TagSources [{1}] MetaData [{2}]", fileInfo.Name, string.Join(",", tagSources), result.ToString());
|
||||
return result;
|
||||
}
|
||||
|
||||
public bool WriteTags(AudioMetaData metaData, FileInfo fileInfo)
|
||||
{
|
||||
return this.ID3TagsHelper.WriteTags(metaData, fileInfo.FullName);
|
||||
}
|
||||
|
||||
private AudioMetaData ParseFromFilename(AudioMetaData metaData, FileInfo fileInfo)
|
||||
{
|
||||
if (this.DoParseFromFileName)
|
||||
{
|
||||
var filename = fileInfo.Name.Replace(fileInfo.Extension, "");
|
||||
var mdFromFilename = this.FileNameHelper.MetaDataFromFilename(filename);
|
||||
if (mdFromFilename.ValidWeight < 32)
|
||||
{
|
||||
var mdFromFileInfo = FileNameHelper.MetaDataFromFileInfo(fileInfo);
|
||||
if (mdFromFileInfo.ValidWeight > mdFromFilename.ValidWeight)
|
||||
{
|
||||
mdFromFilename = mdFromFileInfo;
|
||||
}
|
||||
}
|
||||
if ((mdFromFilename.Year ?? 0) < 1)
|
||||
{
|
||||
mdFromFilename.Year = SafeParser.ToYear(fileInfo.Directory.Name.Substring(0, 4));
|
||||
}
|
||||
return MergeAudioData(this.Configuration, metaData, mdFromFilename);
|
||||
}
|
||||
return metaData;
|
||||
}
|
||||
|
||||
private AudioMetaData ParseFromTags(FileInfo fileInfo)
|
||||
{
|
||||
try
|
||||
{
|
||||
var metaDataFromFile = this.ID3TagsHelper.MetaDataForFile(fileInfo.FullName);
|
||||
if (metaDataFromFile.IsSuccess)
|
||||
{
|
||||
return metaDataFromFile.Data;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this.Logger.Error(ex, string.Format("Error With ID3TagsHelper.MetaDataForFile From File [{0}]", fileInfo.FullName));
|
||||
}
|
||||
return new AudioMetaData
|
||||
{
|
||||
Filename = fileInfo.FullName
|
||||
};
|
||||
}
|
||||
|
||||
private async Task<AudioMetaData> ParseFromMusicBrainz(AudioMetaData metaData)
|
||||
{
|
||||
if (this.DoParseFromMusicBrainz)
|
||||
{
|
||||
var musicBrainzReleaseTracks = await this.MusicBrainzProvider.MusicBrainzReleaseTracks(metaData.Artist, metaData.Release);
|
||||
if (musicBrainzReleaseTracks != null)
|
||||
{
|
||||
var musicBrainzReleaseTrack = musicBrainzReleaseTracks.FirstOrDefault(x => x.TrackNumber == metaData.TrackNumber || x.Title.Equals(metaData.Title, StringComparison.InvariantCultureIgnoreCase));
|
||||
if (musicBrainzReleaseTrack != null)
|
||||
{
|
||||
return MergeAudioData(this.Configuration, metaData, musicBrainzReleaseTrack);
|
||||
}
|
||||
}
|
||||
}
|
||||
return metaData;
|
||||
}
|
||||
|
||||
private async Task<AudioMetaData> GetFromLastFmIntegration(AudioMetaData metaData)
|
||||
{
|
||||
var artistName = metaData.Artist;
|
||||
var ReleaseName = metaData.Release;
|
||||
|
||||
if (this.DoParseFromLastFM)
|
||||
{
|
||||
if (string.IsNullOrEmpty(artistName) && string.IsNullOrEmpty(ReleaseName))
|
||||
{
|
||||
return metaData;
|
||||
}
|
||||
var lastFmReleaseTracks = await this.LastFmHelper.TracksForRelease(artistName, ReleaseName);
|
||||
if (lastFmReleaseTracks != null)
|
||||
{
|
||||
var lastFmReleaseTrack = lastFmReleaseTracks.FirstOrDefault(x => x.TrackNumber == metaData.TrackNumber || x.Title.Equals(metaData.Title, StringComparison.InvariantCultureIgnoreCase));
|
||||
if (lastFmReleaseTrack != null)
|
||||
{
|
||||
return MergeAudioData(this.Configuration, metaData, lastFmReleaseTrack);
|
||||
}
|
||||
}
|
||||
}
|
||||
return metaData;
|
||||
}
|
||||
|
||||
private static AudioMetaData MergeAudioData(IConfiguration configuration, AudioMetaData left, AudioMetaData right)
|
||||
{
|
||||
var result = new AudioMetaData();
|
||||
if (left == null)
|
||||
{
|
||||
return right;
|
||||
}
|
||||
if (right == null)
|
||||
{
|
||||
return left;
|
||||
}
|
||||
result.Release = left.Release.Or(right.Release).SafeReplace("_").SafeReplace("~", ",").CleanString(configuration);
|
||||
result.ArtistRaw = left.ArtistRaw.Or(right.ArtistRaw);
|
||||
result.TrackArtistRaw = left.TrackArtistRaw.Or(right.TrackArtistRaw);
|
||||
result.Artist = left.Artist.Or(right.Artist).SafeReplace("_").SafeReplace("~", ",").CleanString(configuration);
|
||||
result.Title = left.Title.Or(right.Title).SafeReplace("_").SafeReplace("~", ",").CleanString(configuration);
|
||||
result.Year = left.Year.Or(right.Year);
|
||||
result.TrackNumber = left.TrackNumber.Or(right.TrackNumber);
|
||||
result.TotalTrackNumbers = left.TotalTrackNumbers.Or(right.TotalTrackNumbers);
|
||||
result.Disk = left.Disk.Or(right.Disk);
|
||||
result.Time = left.Time ?? right.Time;
|
||||
result.AudioBitrate = left.AudioBitrate.Or(right.AudioBitrate);
|
||||
result.AudioChannels = left.AudioChannels.Or(right.AudioChannels);
|
||||
result.AudioSampleRate = left.AudioSampleRate.Or(right.AudioSampleRate);
|
||||
if (left.Images != null && right.Images == null)
|
||||
{
|
||||
result.Images = left.Images;
|
||||
}
|
||||
else if (left.Images == null && right.Images != null)
|
||||
{
|
||||
result.Images = right.Images;
|
||||
}
|
||||
else if (left.Images != null && right.Images != null)
|
||||
{
|
||||
result.Images = left.Images.Union(right.Images);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Roadie.Library.MetaData.Audio
|
||||
{
|
||||
public sealed class AudioMetaDataImage
|
||||
{
|
||||
public string Url { get; set; }
|
||||
public byte[] Data { get; set; }
|
||||
public string Description { get; set; }
|
||||
public string MimeType { get; set; }
|
||||
public AudioMetaDataImageType Type { get; set; }
|
||||
}
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Roadie.Library.MetaData.Audio
|
||||
{
|
||||
public enum AudioMetaDataImageType
|
||||
{
|
||||
Other = 0,
|
||||
FileIcon = 1,
|
||||
OtherFileIcon = 2,
|
||||
FrontCover = 3,
|
||||
BackCover = 4,
|
||||
LeafletPage = 5,
|
||||
Media = 6,
|
||||
LeadArtist = 7,
|
||||
Artist = 8,
|
||||
Conductor = 9,
|
||||
Band = 10,
|
||||
Composer = 11,
|
||||
Lyricist = 12,
|
||||
RecordingLocation = 13,
|
||||
DuringRecording = 14,
|
||||
DuringPerformance = 15,
|
||||
MovieScreenCapture = 16,
|
||||
ColoredFish = 17,
|
||||
Illustration = 18,
|
||||
BandLogo = 19,
|
||||
PublisherLogo = 20,
|
||||
}
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Roadie.Library.MetaData.Audio
|
||||
{
|
||||
[Flags]
|
||||
public enum AudioMetaDataWeights
|
||||
{
|
||||
None = 0,
|
||||
Year = 1,
|
||||
Time = 2,
|
||||
TrackNumber = 4,
|
||||
Release = 8,
|
||||
Title = 16,
|
||||
Artist = 32
|
||||
}
|
||||
|
||||
//Artist + Release + TrackTitle 56
|
||||
//Artist + Release + TrackNumber 44
|
||||
//Artist + TrackNumber + Title 38
|
||||
//Artist + Release + TrackNumber + TrackTitle = 60
|
||||
|
||||
}
|
452
RoadieLibrary/SearchEngines/MetaData/Discogs/DiscogsHelper.cs
Normal file
|
@ -0,0 +1,452 @@
|
|||
using Roadie.Library.Caching;
|
||||
using RestSharp;
|
||||
using Roadie.Library.Extensions;
|
||||
using Roadie.Library.MetaData;
|
||||
using Roadie.Library.Utility;
|
||||
using Roadie.Library.Logging;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Security.Authentication;
|
||||
using System.Threading.Tasks;
|
||||
using Roadie.Library.Setttings;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
|
||||
namespace Roadie.Library.SearchEngines.MetaData.Discogs
|
||||
{
|
||||
public class DiscogsHelper : MetaDataProviderBase, IArtistSearchEngine, IReleaseSearchEngine, ILabelSearchEngine
|
||||
{
|
||||
public override bool IsEnabled
|
||||
{
|
||||
get
|
||||
{
|
||||
return this.Configuration.GetValue<bool>("Integrations:DiscogsProviderEnabled", true) &&
|
||||
!string.IsNullOrEmpty(this.ApiKey.Key);
|
||||
}
|
||||
}
|
||||
|
||||
public DiscogsHelper(IConfiguration configuration, ICacheManager cacheManager, ILogger loggingService) : base(configuration, cacheManager, loggingService)
|
||||
{
|
||||
this._apiKey = configuration.GetValue<List<ApiKey>>("ApiKeys", new List<ApiKey>()).FirstOrDefault(x => x.ApiName == "DiscogsConsumerKey") ?? new ApiKey();
|
||||
}
|
||||
|
||||
private RestRequest BuildSearchRequest(string query, int resultsCount, string entityType, string artist = null)
|
||||
{
|
||||
var request = new RestRequest
|
||||
{
|
||||
Resource = "search",
|
||||
Method = Method.GET,
|
||||
RequestFormat = DataFormat.Json
|
||||
};
|
||||
if (resultsCount > 0)
|
||||
{
|
||||
request.AddParameter(new Parameter
|
||||
{
|
||||
Name = "page",
|
||||
Value = 1,
|
||||
Type = ParameterType.GetOrPost
|
||||
});
|
||||
request.AddParameter(new Parameter
|
||||
{
|
||||
Name = "per_page",
|
||||
Value = resultsCount,
|
||||
Type = ParameterType.GetOrPost
|
||||
});
|
||||
}
|
||||
request.AddParameter(new Parameter
|
||||
{
|
||||
Name = "type",
|
||||
Value = entityType,
|
||||
Type = ParameterType.GetOrPost
|
||||
});
|
||||
request.AddParameter(new Parameter
|
||||
{
|
||||
Name = "q",
|
||||
Value = string.Format("'{0}'", query.Trim()),
|
||||
Type = ParameterType.GetOrPost
|
||||
});
|
||||
if (!string.IsNullOrEmpty(artist))
|
||||
{
|
||||
request.AddParameter(new Parameter
|
||||
{
|
||||
Name = "artist",
|
||||
Value = string.Format("'{0}'", artist.Trim()),
|
||||
Type = ParameterType.GetOrPost
|
||||
});
|
||||
}
|
||||
request.AddParameter(new Parameter
|
||||
{
|
||||
Name = "key",
|
||||
Value = this.ApiKey.Key,
|
||||
Type = ParameterType.GetOrPost
|
||||
});
|
||||
request.AddParameter(new Parameter
|
||||
{
|
||||
Name = "secret",
|
||||
Value = this.ApiKey.Secret,
|
||||
Type = ParameterType.GetOrPost
|
||||
});
|
||||
|
||||
return request;
|
||||
}
|
||||
|
||||
private RestRequest BuildArtistRequest(int? artistId)
|
||||
{
|
||||
var request = new RestRequest
|
||||
{
|
||||
Resource = "artists/{id}",
|
||||
Method = Method.GET,
|
||||
RequestFormat = DataFormat.Json
|
||||
};
|
||||
request.AddUrlSegment("id", artistId.ToString());
|
||||
request.AddParameter(new Parameter
|
||||
{
|
||||
Name = "key",
|
||||
Value = this.ApiKey.Key,
|
||||
Type = ParameterType.GetOrPost
|
||||
});
|
||||
request.AddParameter(new Parameter
|
||||
{
|
||||
Name = "secret",
|
||||
Value = this.ApiKey.Secret,
|
||||
Type = ParameterType.GetOrPost
|
||||
});
|
||||
|
||||
return request;
|
||||
}
|
||||
|
||||
private RestRequest BuildLabelRequest(int? artistId)
|
||||
{
|
||||
var request = new RestRequest
|
||||
{
|
||||
Resource = "labels/{id}",
|
||||
Method = Method.GET,
|
||||
RequestFormat = DataFormat.Json
|
||||
};
|
||||
request.AddUrlSegment("id", artistId.ToString());
|
||||
request.AddParameter(new Parameter
|
||||
{
|
||||
Name = "key",
|
||||
Value = this.ApiKey.Key,
|
||||
Type = ParameterType.GetOrPost
|
||||
});
|
||||
request.AddParameter(new Parameter
|
||||
{
|
||||
Name = "secret",
|
||||
Value = this.ApiKey.Secret,
|
||||
Type = ParameterType.GetOrPost
|
||||
});
|
||||
|
||||
return request;
|
||||
}
|
||||
|
||||
private RestRequest BuildReleaseRequest(int? releaseId)
|
||||
{
|
||||
var request = new RestRequest
|
||||
{
|
||||
Resource = "releases/{id}",
|
||||
Method = Method.GET,
|
||||
RequestFormat = DataFormat.Json
|
||||
};
|
||||
request.AddUrlSegment("id", releaseId.ToString());
|
||||
request.AddParameter(new Parameter
|
||||
{
|
||||
Name = "key",
|
||||
Value = this.ApiKey.Key,
|
||||
Type = ParameterType.GetOrPost
|
||||
});
|
||||
request.AddParameter(new Parameter
|
||||
{
|
||||
Name = "secret",
|
||||
Value = this.ApiKey.Secret,
|
||||
Type = ParameterType.GetOrPost
|
||||
});
|
||||
|
||||
return request;
|
||||
}
|
||||
|
||||
public async Task<OperationResult<IEnumerable<ArtistSearchResult>>> PerformArtistSearch(string query, int resultsCount)
|
||||
{
|
||||
ArtistSearchResult data = null;
|
||||
try
|
||||
{
|
||||
this.Logger.Trace("DiscogsHelper:PerformArtistSearch:{0}", query);
|
||||
var request = this.BuildSearchRequest(query, 1, "artist");
|
||||
|
||||
var client = new RestClient("https://api.discogs.com/database");
|
||||
client.UserAgent = WebHelper.UserAgent;
|
||||
|
||||
var response = await client.ExecuteTaskAsync<DiscogsResult>(request);
|
||||
|
||||
if (response.ResponseStatus == ResponseStatus.Error)
|
||||
{
|
||||
if (response.StatusCode == HttpStatusCode.Unauthorized)
|
||||
{
|
||||
throw new AuthenticationException("Unauthorized");
|
||||
}
|
||||
throw new Exception(string.Format("Request Error Message: {0}. Content: {1}.", response.ErrorMessage, response.Content));
|
||||
}
|
||||
Result responseData = response.Data.results != null && response.Data.results.Any() ? response.Data.results.First() : null;
|
||||
if (responseData != null)
|
||||
{
|
||||
request = this.BuildArtistRequest(responseData.id);
|
||||
var c2 = new RestClient("https://api.discogs.com/");
|
||||
c2.UserAgent = WebHelper.UserAgent;
|
||||
var artistResponse = await c2.ExecuteTaskAsync<DiscogArtistResponse>(request);
|
||||
DiscogArtistResponse artist = artistResponse.Data;
|
||||
if (artist != null)
|
||||
{
|
||||
var urls = new List<string>();
|
||||
var images = new List<string>();
|
||||
var alternateNames = new List<string>();
|
||||
string artistThumbnailUrl = null;
|
||||
urls.Add(artist.uri);
|
||||
if (artist.urls != null)
|
||||
{
|
||||
urls.AddRange(artist.urls);
|
||||
}
|
||||
if (artist.images != null)
|
||||
{
|
||||
images.AddRange(artist.images.Where(x => x.type != "primary").Select(x => x.uri));
|
||||
var primaryImage = artist.images.FirstOrDefault(x => x.type == "primary");
|
||||
if (primaryImage != null)
|
||||
{
|
||||
artistThumbnailUrl = primaryImage.uri;
|
||||
}
|
||||
if (string.IsNullOrEmpty(artistThumbnailUrl))
|
||||
{
|
||||
artistThumbnailUrl = artist.images.First(x => !string.IsNullOrEmpty(x.uri)).uri;
|
||||
}
|
||||
}
|
||||
if (artist.namevariations != null)
|
||||
{
|
||||
alternateNames.AddRange(artist.namevariations.Distinct());
|
||||
}
|
||||
data = new ArtistSearchResult
|
||||
{
|
||||
ArtistName = artist.name,
|
||||
DiscogsId = artist.id.ToString(),
|
||||
ArtistType = responseData.type,
|
||||
Profile = artist.profile,
|
||||
AlternateNames = alternateNames,
|
||||
ArtistThumbnailUrl = artistThumbnailUrl,
|
||||
Urls = urls,
|
||||
ImageUrls = images
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this.Logger.Error(ex);
|
||||
}
|
||||
return new OperationResult<IEnumerable<ArtistSearchResult>>
|
||||
{
|
||||
IsSuccess = data != null,
|
||||
Data = new ArtistSearchResult[] { data }
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<OperationResult<IEnumerable<ReleaseSearchResult>>> PerformReleaseSearch(string artistName, string query, int resultsCount)
|
||||
{
|
||||
ReleaseSearchResult data = null;
|
||||
try
|
||||
{
|
||||
var request = this.BuildSearchRequest(query, 10, "release", artistName);
|
||||
|
||||
var client = new RestClient("https://api.discogs.com/database");
|
||||
client.UserAgent = WebHelper.UserAgent;
|
||||
client.ReadWriteTimeout = this.Configuration.GetValue<int>("Integrations:DiscogsReadWriteTimeout", 45);
|
||||
client.Timeout = this.Configuration.GetValue<int>("Integrations:DiscogsTimeout", 60);
|
||||
|
||||
var response = await client.ExecuteTaskAsync<DiscogsReleaseSearchResult>(request);
|
||||
|
||||
if (response.ResponseStatus == ResponseStatus.Error)
|
||||
{
|
||||
if (response.StatusCode == HttpStatusCode.Unauthorized)
|
||||
{
|
||||
throw new AuthenticationException("Unauthorized");
|
||||
}
|
||||
throw new Exception(string.Format("Request Error Message: {0}. Content: {1}.", response.ErrorMessage, response.Content));
|
||||
}
|
||||
var responseData = response.Data != null && response.Data.results.Any() ? response.Data.results.OrderBy(x => x.year).First() : null;
|
||||
if (responseData != null)
|
||||
{
|
||||
request = this.BuildReleaseRequest(responseData.id);
|
||||
var c2 = new RestClient("https://api.discogs.com/");
|
||||
c2.UserAgent = WebHelper.UserAgent;
|
||||
var releaseResult = await c2.ExecuteTaskAsync<DiscogReleaseDetail>(request);
|
||||
var release = releaseResult != null && releaseResult.Data != null ? releaseResult.Data : null;
|
||||
if (release != null)
|
||||
{
|
||||
var urls = new List<string>();
|
||||
var images = new List<string>();
|
||||
string releaseThumbnailUrl = null;
|
||||
urls.Add(release.uri);
|
||||
if (release.images != null)
|
||||
{
|
||||
images.AddRange(release.images.Where(x => x.type != "primary").Select(x => x.uri));
|
||||
var primaryImage = release.images.FirstOrDefault(x => x.type == "primary");
|
||||
if (primaryImage != null)
|
||||
{
|
||||
releaseThumbnailUrl = primaryImage.uri;
|
||||
}
|
||||
if (string.IsNullOrEmpty(releaseThumbnailUrl))
|
||||
{
|
||||
releaseThumbnailUrl = release.images.First(x => !string.IsNullOrEmpty(x.uri)).uri;
|
||||
}
|
||||
}
|
||||
data = new ReleaseSearchResult
|
||||
{
|
||||
DiscogsId = release.id.ToString(),
|
||||
ReleaseType = responseData.type,
|
||||
ReleaseDate = SafeParser.ToDateTime(release.released),
|
||||
Profile = release.notes,
|
||||
ReleaseThumbnailUrl = releaseThumbnailUrl,
|
||||
Urls = urls,
|
||||
ImageUrls = images
|
||||
};
|
||||
if (release.genres != null)
|
||||
{
|
||||
data.ReleaseGenres = release.genres.ToList();
|
||||
}
|
||||
if (release.labels != null)
|
||||
{
|
||||
data.ReleaseLabel = release.labels.Select(x => new ReleaseLabelSearchResult
|
||||
{
|
||||
CatalogNumber = x.catno,
|
||||
Label = new LabelSearchResult
|
||||
{
|
||||
LabelName = x.name,
|
||||
DiscogsId = x.id.ToString()
|
||||
}
|
||||
}).ToList();
|
||||
}
|
||||
if (release.tracklist != null)
|
||||
{
|
||||
var releaseMediaCount = 1;
|
||||
var releaseMedias = new List<ReleaseMediaSearchResult>();
|
||||
for (short? i = 1; i <= releaseMediaCount; i++)
|
||||
{
|
||||
var releaseTracks = new List<TrackSearchResult>();
|
||||
short? looper = 0;
|
||||
foreach (var dTrack in release.tracklist.OrderBy(x => x.position))
|
||||
{
|
||||
looper++;
|
||||
releaseTracks.Add(new TrackSearchResult
|
||||
{
|
||||
TrackNumber = looper,
|
||||
Title = dTrack.title,
|
||||
Duration = dTrack.duration.ToTrackDuration(),
|
||||
TrackType = dTrack.type_
|
||||
});
|
||||
}
|
||||
releaseMedias.Add(new ReleaseMediaSearchResult
|
||||
{
|
||||
ReleaseMediaNumber = i,
|
||||
TrackCount = (short)releaseTracks.Count(),
|
||||
Tracks = releaseTracks
|
||||
});
|
||||
}
|
||||
data.ReleaseMedia = releaseMedias;
|
||||
}
|
||||
if (release.identifiers != null)
|
||||
{
|
||||
var barcode = release.identifiers.FirstOrDefault(x => x.type == "Barcode");
|
||||
if (barcode != null && !string.IsNullOrEmpty(barcode.value))
|
||||
{
|
||||
data.Tags = new string[] { "barcode:" + barcode.value };
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this.Logger.Error(ex);
|
||||
}
|
||||
return new OperationResult<IEnumerable<ReleaseSearchResult>>
|
||||
{
|
||||
IsSuccess = data != null,
|
||||
Data = new ReleaseSearchResult[] { data }
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<OperationResult<IEnumerable<LabelSearchResult>>> PerformLabelSearch(string labelName, int resultsCount)
|
||||
{
|
||||
LabelSearchResult data = null;
|
||||
try
|
||||
{
|
||||
var request = this.BuildSearchRequest(labelName, 1, "label");
|
||||
|
||||
var client = new RestClient("https://api.discogs.com/database");
|
||||
client.UserAgent = WebHelper.UserAgent;
|
||||
|
||||
var response = await client.ExecuteTaskAsync<DiscogsResult>(request);
|
||||
|
||||
if (response.ResponseStatus == ResponseStatus.Error)
|
||||
{
|
||||
if (response.StatusCode == HttpStatusCode.Unauthorized)
|
||||
{
|
||||
throw new AuthenticationException("Unauthorized");
|
||||
}
|
||||
throw new Exception(string.Format("Request Error Message: {0}. Content: {1}.", response.ErrorMessage, response.Content));
|
||||
}
|
||||
Result responseData = response.Data.results != null && response.Data.results.Any() ? response.Data.results.First() : null;
|
||||
if (responseData != null)
|
||||
{
|
||||
request = this.BuildLabelRequest(responseData.id);
|
||||
var c2 = new RestClient("https://api.discogs.com/");
|
||||
c2.UserAgent = WebHelper.UserAgent;
|
||||
var labelResponse = await c2.ExecuteTaskAsync<DiscogsLabelResult>(request);
|
||||
DiscogsLabelResult label = labelResponse.Data;
|
||||
if (label != null)
|
||||
{
|
||||
var urls = new List<string>();
|
||||
var images = new List<string>();
|
||||
var alternateNames = new List<string>();
|
||||
string labelThumbnailUrl = null;
|
||||
urls.Add(label.uri);
|
||||
if (label.urls != null)
|
||||
{
|
||||
urls.AddRange(label.urls);
|
||||
}
|
||||
if (label.images != null)
|
||||
{
|
||||
images.AddRange(label.images.Where(x => x.type != "primary").Select(x => x.uri));
|
||||
var primaryImage = label.images.FirstOrDefault(x => x.type == "primary");
|
||||
if (primaryImage != null)
|
||||
{
|
||||
labelThumbnailUrl = primaryImage.uri;
|
||||
}
|
||||
if (string.IsNullOrEmpty(labelThumbnailUrl))
|
||||
{
|
||||
labelThumbnailUrl = label.images.First(x => !string.IsNullOrEmpty(x.uri)).uri;
|
||||
}
|
||||
}
|
||||
data = new LabelSearchResult
|
||||
{
|
||||
LabelName = label.name,
|
||||
DiscogsId = label.id.ToString(),
|
||||
Profile = label.profile,
|
||||
AlternateNames = alternateNames,
|
||||
LabelImageUrl = labelThumbnailUrl,
|
||||
Urls = urls,
|
||||
ImageUrls = images
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this.Logger.Error(ex);
|
||||
}
|
||||
return new OperationResult<IEnumerable<LabelSearchResult>>
|
||||
{
|
||||
IsSuccess = data != null,
|
||||
Data = new LabelSearchResult[] { data }
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
254
RoadieLibrary/SearchEngines/MetaData/Discogs/Entities.cs
Normal file
|
@ -0,0 +1,254 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Roadie.Library.SearchEngines.MetaData.Discogs
|
||||
{
|
||||
public class DiscogsResult
|
||||
{
|
||||
public Pagination pagination { get; set; }
|
||||
public List<Result> results { get; set; }
|
||||
}
|
||||
|
||||
public class Pagination
|
||||
{
|
||||
public int? per_page { get; set; }
|
||||
public int? items { get; set; }
|
||||
public int? page { get; set; }
|
||||
public Urls urls { get; set; }
|
||||
public int? pages { get; set; }
|
||||
}
|
||||
|
||||
public class Urls
|
||||
{
|
||||
public string last { get; set; }
|
||||
public string next { get; set; }
|
||||
}
|
||||
|
||||
public class Result
|
||||
{
|
||||
public string thumb { get; set; }
|
||||
public string title { get; set; }
|
||||
public string uri { get; set; }
|
||||
public string resource_url { get; set; }
|
||||
public string type { get; set; }
|
||||
public int? id { get; set; }
|
||||
}
|
||||
|
||||
public class DiscogArtistResponse
|
||||
{
|
||||
public string profile { get; set; }
|
||||
public string releases_url { get; set; }
|
||||
public string name { get; set; }
|
||||
public List<string> namevariations { get; set; }
|
||||
public string uri { get; set; }
|
||||
public List<string> urls { get; set; }
|
||||
public List<Image> images { get; set; }
|
||||
public string resource_url { get; set; }
|
||||
public List<Group> groups { get; set; }
|
||||
public int? id { get; set; }
|
||||
public string data_quality { get; set; }
|
||||
public string realname { get; set; }
|
||||
}
|
||||
|
||||
public class Image
|
||||
{
|
||||
public string uri { get; set; }
|
||||
public int? height { get; set; }
|
||||
public int? width { get; set; }
|
||||
public string resource_url { get; set; }
|
||||
public string type { get; set; }
|
||||
public string uri150 { get; set; }
|
||||
}
|
||||
|
||||
public class Group
|
||||
{
|
||||
public bool active { get; set; }
|
||||
public string resource_url { get; set; }
|
||||
public int? id { get; set; }
|
||||
public string name { get; set; }
|
||||
}
|
||||
|
||||
public class DiscogsReleaseSearchResult
|
||||
{
|
||||
public Pagination pagination { get; set; }
|
||||
public List<ReleaseSearchRelease> results { get; set; }
|
||||
}
|
||||
|
||||
public class ReleaseSearchRelease
|
||||
{
|
||||
public List<string> style { get; set; }
|
||||
public string thumb { get; set; }
|
||||
public List<string> format { get; set; }
|
||||
public string country { get; set; }
|
||||
public List<string> barcode { get; set; }
|
||||
public string uri { get; set; }
|
||||
public Community community { get; set; }
|
||||
public List<string> label { get; set; }
|
||||
public string catno { get; set; }
|
||||
public string year { get; set; }
|
||||
public List<string> genre { get; set; }
|
||||
public string title { get; set; }
|
||||
public string resource_url { get; set; }
|
||||
public string type { get; set; }
|
||||
public int? id { get; set; }
|
||||
}
|
||||
|
||||
public class Community
|
||||
{
|
||||
public string status { get; set; }
|
||||
public Rating rating { get; set; }
|
||||
public int? want { get; set; }
|
||||
public List<Contributor> contributors { get; set; }
|
||||
public int? have { get; set; }
|
||||
public Submitter submitter { get; set; }
|
||||
public string data_quality { get; set; }
|
||||
}
|
||||
|
||||
public class DiscogReleaseDetail
|
||||
{
|
||||
public List<string> styles { get; set; }
|
||||
public List<Video> videos { get; set; }
|
||||
public List<string> series { get; set; }
|
||||
public List<Label> labels { get; set; }
|
||||
public Community community { get; set; }
|
||||
public int? year { get; set; }
|
||||
public List<Image> images { get; set; }
|
||||
public int? format_quantity { get; set; }
|
||||
public int? id { get; set; }
|
||||
public List<string> genres { get; set; }
|
||||
public string thumb { get; set; }
|
||||
public List<Extraartist> extraartists { get; set; }
|
||||
public string title { get; set; }
|
||||
public List<Artist> artists { get; set; }
|
||||
public DateTime date_changed { get; set; }
|
||||
public int? master_id { get; set; }
|
||||
public List<Tracklist> tracklist { get; set; }
|
||||
public string status { get; set; }
|
||||
public string released_formatted { get; set; }
|
||||
public int? estimated_weight { get; set; }
|
||||
public string master_url { get; set; }
|
||||
public string released { get; set; }
|
||||
public DateTime date_added { get; set; }
|
||||
public string country { get; set; }
|
||||
public string notes { get; set; }
|
||||
public List<Identifier> identifiers { get; set; }
|
||||
public List<Company> companies { get; set; }
|
||||
public string uri { get; set; }
|
||||
public List<Format> formats { get; set; }
|
||||
public string resource_url { get; set; }
|
||||
public string data_quality { get; set; }
|
||||
}
|
||||
|
||||
public class Rating
|
||||
{
|
||||
public int? count { get; set; }
|
||||
public float average { get; set; }
|
||||
}
|
||||
|
||||
public class Submitter
|
||||
{
|
||||
public string username { get; set; }
|
||||
public string resource_url { get; set; }
|
||||
}
|
||||
|
||||
public class Contributor
|
||||
{
|
||||
public string username { get; set; }
|
||||
public string resource_url { get; set; }
|
||||
}
|
||||
|
||||
public class Video
|
||||
{
|
||||
public int? duration { get; set; }
|
||||
public bool embed { get; set; }
|
||||
public string title { get; set; }
|
||||
public string description { get; set; }
|
||||
public string uri { get; set; }
|
||||
}
|
||||
|
||||
public class Label
|
||||
{
|
||||
public string name { get; set; }
|
||||
public string entity_type { get; set; }
|
||||
public string catno { get; set; }
|
||||
public string resource_url { get; set; }
|
||||
public int? id { get; set; }
|
||||
public string entity_type_name { get; set; }
|
||||
}
|
||||
|
||||
public class Extraartist
|
||||
{
|
||||
public string join { get; set; }
|
||||
public string name { get; set; }
|
||||
public string anv { get; set; }
|
||||
public string tracks { get; set; }
|
||||
public string role { get; set; }
|
||||
public string resource_url { get; set; }
|
||||
public int? id { get; set; }
|
||||
}
|
||||
|
||||
public class Artist
|
||||
{
|
||||
public string join { get; set; }
|
||||
public string name { get; set; }
|
||||
public string anv { get; set; }
|
||||
public string tracks { get; set; }
|
||||
public string role { get; set; }
|
||||
public string resource_url { get; set; }
|
||||
public int? id { get; set; }
|
||||
}
|
||||
|
||||
public class Tracklist
|
||||
{
|
||||
public string duration { get; set; }
|
||||
public string position { get; set; }
|
||||
public string type_ { get; set; }
|
||||
public string title { get; set; }
|
||||
}
|
||||
|
||||
public class Identifier
|
||||
{
|
||||
public string type { get; set; }
|
||||
public string value { get; set; }
|
||||
public string description { get; set; }
|
||||
}
|
||||
|
||||
public class Company
|
||||
{
|
||||
public string name { get; set; }
|
||||
public string entity_type { get; set; }
|
||||
public string catno { get; set; }
|
||||
public string resource_url { get; set; }
|
||||
public int? id { get; set; }
|
||||
public string entity_type_name { get; set; }
|
||||
}
|
||||
|
||||
public class Format
|
||||
{
|
||||
public string qty { get; set; }
|
||||
public List<string> descriptions { get; set; }
|
||||
public string name { get; set; }
|
||||
}
|
||||
|
||||
public class DiscogsLabelResult
|
||||
{
|
||||
public string profile { get; set; }
|
||||
public string releases_url { get; set; }
|
||||
public string name { get; set; }
|
||||
public string contact_info { get; set; }
|
||||
public string uri { get; set; }
|
||||
public List<Sublabel> sublabels { get; set; }
|
||||
public List<string> urls { get; set; }
|
||||
public List<Image> images { get; set; }
|
||||
public string resource_url { get; set; }
|
||||
public int? id { get; set; }
|
||||
public string data_quality { get; set; }
|
||||
}
|
||||
|
||||
public class Sublabel
|
||||
{
|
||||
public string resource_url { get; set; }
|
||||
public int? id { get; set; }
|
||||
public string name { get; set; }
|
||||
}
|
||||
}
|
323
RoadieLibrary/SearchEngines/MetaData/FileName/FileNameHelper.cs
Normal file
|
@ -0,0 +1,323 @@
|
|||
using Roadie.Library.Caching;
|
||||
using Roadie.Library.Extensions;
|
||||
using Roadie.Library.Utility;
|
||||
using Roadie.Library.Logging;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using Roadie.Library.MetaData.Audio;
|
||||
|
||||
namespace Roadie.Library.MetaData.FileName
|
||||
{
|
||||
public class FileNameHelper : MetaDataProviderBase
|
||||
{
|
||||
public FileNameHelper(ICacheManager cacheManager, ILogger loggingService) : base(cacheManager, loggingService)
|
||||
{ }
|
||||
|
||||
public static string CleanString(string input)
|
||||
{
|
||||
if (string.IsNullOrEmpty(input))
|
||||
{
|
||||
return input;
|
||||
}
|
||||
return input.CleanString();
|
||||
}
|
||||
|
||||
public AudioMetaData MetaDataFromFilename(string rawFilename)
|
||||
{
|
||||
var filename = CleanString(rawFilename);
|
||||
if (IsTalbTyerTalbTrckTit2(filename))
|
||||
{
|
||||
// GUID~TPE1 - [TYER] - TALB~TRCK. TIT2
|
||||
var parts = filename.Split('~');
|
||||
|
||||
var firstParts = parts[1].Split('-');
|
||||
var artist = firstParts[0];
|
||||
var year = firstParts[1].Replace("[", "").Replace("]", "");
|
||||
var Release = firstParts[2];
|
||||
|
||||
var trck = parts[2].Substring(0, 2);
|
||||
var title = parts[2].Substring(3, parts[2].Length - 3);
|
||||
|
||||
return new AudioMetaData
|
||||
{
|
||||
Artist = CleanString(artist),
|
||||
Year = SafeParser.ToNumber<int?>(CleanString(year)),
|
||||
Release = CleanString(Release),
|
||||
TrackNumber = SafeParser.ToNumber<short?>(CleanString(trck)),
|
||||
Title = CleanString(title)
|
||||
};
|
||||
}
|
||||
else if (IsTpe1TalbTyerTrckTpe1Tit2(filename))
|
||||
{
|
||||
// GUID~TPE1-TALB-TYER~TRCK-TPE1-TIT2
|
||||
var parts = filename.Split('~');
|
||||
|
||||
var firstParts = parts[1].Split('-');
|
||||
var secondParts = parts[2].Split('-');
|
||||
|
||||
var artist = firstParts[0];
|
||||
var Release = firstParts[1];
|
||||
var year = firstParts[2];
|
||||
var trck = secondParts[0].Substring(0, 2);
|
||||
var title = secondParts[1];
|
||||
|
||||
return new AudioMetaData
|
||||
{
|
||||
Artist = CleanString(artist),
|
||||
Year = SafeParser.ToNumber<int?>(CleanString(year)),
|
||||
Release = CleanString(Release),
|
||||
TrackNumber = SafeParser.ToNumber<short?>(CleanString(trck)),
|
||||
Title = CleanString(title)
|
||||
};
|
||||
}
|
||||
else if (IsTpe1TalbTyerTrckTit2(filename))
|
||||
{
|
||||
// GUID~TPE1-TALB (TYER)~TRCK-TIT2
|
||||
var parts = filename.Split('~');
|
||||
|
||||
var firstParts = parts[1].Split('-');
|
||||
var secondParts = parts[2].Split('-');
|
||||
|
||||
var artist = firstParts[0];
|
||||
var year = firstParts[1].Substring(firstParts[1].Length - 6, 6).Replace("(", "").Replace(")", "");
|
||||
var Release = firstParts[1].Substring(0, firstParts[1].Length - 6);
|
||||
var trck = secondParts[1];
|
||||
var title = secondParts[2];
|
||||
|
||||
return new AudioMetaData
|
||||
{
|
||||
Artist = CleanString(artist),
|
||||
Year = SafeParser.ToNumber<int?>(CleanString(year)),
|
||||
Release = CleanString(Release),
|
||||
TrackNumber = SafeParser.ToNumber<short?>(CleanString(trck)),
|
||||
Title = CleanString(title)
|
||||
};
|
||||
}
|
||||
else if (IsTalbTposTpe1TrckTit2(filename))
|
||||
{
|
||||
// GUID~TALB~TPOS TPE1 - TRCK - TIT2
|
||||
var parts = filename.Split('~');
|
||||
var Release = parts[1];
|
||||
|
||||
var secondParts = parts[2].Split('-');
|
||||
|
||||
var tpos = secondParts[0].Substring(0, 2);
|
||||
var artist = secondParts[0].Substring(2, secondParts[0].Length - 2);
|
||||
var trck = secondParts[1];
|
||||
var title = secondParts[2];
|
||||
|
||||
return new AudioMetaData
|
||||
{
|
||||
Artist = CleanString(artist),
|
||||
Disk = SafeParser.ToNumber<int?>(CleanString(tpos)),
|
||||
Release = CleanString(Release),
|
||||
TrackNumber = SafeParser.ToNumber<short?>(CleanString(trck)),
|
||||
Title = CleanString(title)
|
||||
};
|
||||
}
|
||||
else if (IsTyerTalbTrckTit2(filename))
|
||||
{
|
||||
// GUID~[TYER] TALB~TRCK - TIT2
|
||||
var parts = filename.Split('~');
|
||||
var year = parts[1].Split(' ').First().Replace("[", "").Replace("]", "").Replace("(", "").Replace(")", "");
|
||||
var Release = string.Join(" ", parts[1].Split(' ').Skip(1));
|
||||
|
||||
var secondParts = parts[2];
|
||||
if (secondParts.StartsWith("-"))
|
||||
{
|
||||
secondParts = secondParts.Substring(1, secondParts.Length - 1);
|
||||
}
|
||||
var track = secondParts.Split('-').First();
|
||||
var title = string.Join(" ", secondParts.Split('-').Skip(1));
|
||||
return new AudioMetaData
|
||||
{
|
||||
Year = SafeParser.ToNumber<int?>(CleanString(year)),
|
||||
Release = CleanString(Release),
|
||||
TrackNumber = SafeParser.ToNumber<short?>(CleanString(track)),
|
||||
Title = CleanString(title)
|
||||
};
|
||||
}
|
||||
else if (IsTyerTalbTpe1Tit2(filename))
|
||||
{
|
||||
// GUID~TYER - TALB~TPE1 - TIT2
|
||||
var parts = filename.Split('~');
|
||||
var secondParts = parts[1].Split('-');
|
||||
|
||||
var year = secondParts[0];
|
||||
var Release = secondParts[1];
|
||||
|
||||
var thirdParts = parts[2].Split('-');
|
||||
var artist = thirdParts[0];
|
||||
var title = thirdParts[1];
|
||||
|
||||
return new AudioMetaData
|
||||
{
|
||||
Year = SafeParser.ToNumber<int?>(CleanString(year)),
|
||||
Artist = CleanString(artist),
|
||||
Release = CleanString(Release),
|
||||
Title = CleanString(title)
|
||||
};
|
||||
}
|
||||
else if (IsTpe1TrckTit2(filename))
|
||||
{
|
||||
// GUID~TPE1~TRCK TIT2
|
||||
var parts = filename.Split('~');
|
||||
var track = parts[2].Split(' ').First();
|
||||
var title = string.Join(" ", parts[2].Split(' ').Skip(1));
|
||||
return new AudioMetaData
|
||||
{
|
||||
Artist = CleanString(parts[1]),
|
||||
TrackNumber = SafeParser.ToNumber<short?>(CleanString(track)),
|
||||
Title = CleanString(title)
|
||||
};
|
||||
}
|
||||
else if (IsTpe1Tit2(filename))
|
||||
{
|
||||
var parts = filename.Split('~');
|
||||
return new AudioMetaData
|
||||
{
|
||||
Artist = CleanString(parts[1]),
|
||||
Title = CleanString(parts[2])
|
||||
};
|
||||
}
|
||||
|
||||
return new AudioMetaData();
|
||||
}
|
||||
|
||||
public static AudioMetaData MetaDataFromFileInfo(FileInfo fileInfo)
|
||||
{
|
||||
var justFilename = CleanString(fileInfo.Name.Replace(fileInfo.Extension, ""));
|
||||
if (IsTrckTit2(justFilename))
|
||||
{
|
||||
var Release = fileInfo.Directory.Name;
|
||||
var ReleaseYear = SafeParser.ToYear(Release.Substring(0, 4));
|
||||
if (ReleaseYear.HasValue)
|
||||
{
|
||||
Release = Release.Substring(5, Release.Length - 5);
|
||||
}
|
||||
var artist = fileInfo.Directory.Parent.Name;
|
||||
|
||||
var title = justFilename.Substring(2, justFilename.Length - 2);
|
||||
var artistYearRelease = CleanString(string.Format("{0} {1} {2}", artist, ReleaseYear, Release));
|
||||
if (justFilename.StartsWith(artistYearRelease) || CleanString(justFilename.Replace("The ", "")).StartsWith(CleanString(artistYearRelease.Replace("The ", ""))))
|
||||
{
|
||||
title = CleanString(CleanString(justFilename.Replace("The ", "")).Replace(CleanString(artistYearRelease.Replace("The ", "")), ""));
|
||||
}
|
||||
else
|
||||
{
|
||||
var regex = new Regex(@"[0-9]{2}-[\w\s,$/.'-`#&()!]+");
|
||||
if (regex.IsMatch(title))
|
||||
{
|
||||
title = fileInfo.Name.Replace(fileInfo.Extension, "");
|
||||
title = title.Substring(6, title.Length - 6);
|
||||
title = Regex.Replace(title, @"(\B[A-Z]+?(?=[A-Z][^A-Z])|\B[A-Z]+?(?=[^A-Z]))", " $1");
|
||||
}
|
||||
}
|
||||
var trackNumber = SafeParser.ToNumber<short>(title.Substring(0, 2));
|
||||
return new AudioMetaData
|
||||
{
|
||||
Artist = artist,
|
||||
Release = Release,
|
||||
Year = ReleaseYear,
|
||||
TrackNumber = trackNumber,
|
||||
Title = CleanString(title.Replace(trackNumber.ToString("D2") + " ", ""))
|
||||
};
|
||||
}
|
||||
return new AudioMetaData();
|
||||
}
|
||||
|
||||
public static bool IsValidAudioFileName(string filename)
|
||||
{
|
||||
var regex = new Regex(@"[a-zA-Z]:\\[\\\w\s,\$\/\.'`#&()!\‐\-]+\[[0-9]{4}\]\s[\[\]\w\s,\$\/\.'`#&()!\‐\-]+[\\CD0-9]*\\[0-9]{2,}\s[\[\]\w\s,\$\/\.'`#&()!\‐\-]+\.(mp3|flac)");
|
||||
return regex.IsMatch(filename);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// TRCK TIT2
|
||||
/// </summary>
|
||||
public static bool IsTrckTit2(string filename)
|
||||
{
|
||||
var regex = new Regex(@"[0-9]{2}\s[\w\s,$/.'-`#&()!]+");
|
||||
return regex.IsMatch(filename);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// GUID~TALB~TPOS TPE1 - TRCK - TIT2
|
||||
/// </summary>
|
||||
public static bool IsTalbTposTpe1TrckTit2(string filename)
|
||||
{
|
||||
var regex = new Regex(@"[-a-zA-Z0-9]{36}~[\w\s,$/.'`#&()!-]+[\s\w()'&]~[0-9]{2}[-\w\s,$/.'`#&()!]+[-\s~]\s[0-9]{1,2}[-\s~]+[\w\s,$/.'-`#&()!]+");
|
||||
return regex.IsMatch(filename);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// GUID~TPE1 - [TYER] - TALB~TRCK. TIT2
|
||||
/// </summary>
|
||||
public static bool IsTalbTyerTalbTrckTit2(string filename)
|
||||
{
|
||||
var regex = new Regex(@"[-a-zA-Z0-9]{36}~[\w\s,$/.'`#&()!]+-\s\[[0-9]{4}]\s-\s[\w\s,$/.'`#&()!]+~[0-9]{2}.[\w\s,$/.'`#&()!]+");
|
||||
return regex.IsMatch(filename);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// GUID~TPE1~TIT2
|
||||
/// </summary>
|
||||
public static bool IsTpe1Tit2(string filename)
|
||||
{
|
||||
var regex = new Regex(@"[-a-zA-Z0-9]{36}~[\w\s,$/.'-`#&()!]+~[\w\s,$/.'-`#&()!]+");
|
||||
return regex.IsMatch(filename);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// GUID~TYER - TALB~TPE1 - TIT2
|
||||
/// </summary>
|
||||
public static bool IsTyerTalbTpe1Tit2(string filename)
|
||||
{
|
||||
var regex = new Regex(@"[-a-zA-Z0-9]{36}~[0-9]{4}[\w\s,$/.'-`#&()!]+~[\w\s,$/.'-`#&()!]+");
|
||||
return regex.IsMatch(filename);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// GUID~[TYER] TALB~TRCK - TIT2
|
||||
/// </summary>
|
||||
public static bool IsTyerTalbTrckTit2(string filename)
|
||||
{
|
||||
var regex = new Regex(@"[-a-zA-Z0-9]{36}~[\[\(]*[0-9]{4}[\]\)]*[\w\s,$/.'-`#&()!]+[~-]*((\\)*(/)*([0-9]{2})*)[\s~-]*[\w\s,$/.'-`#&()!]+");
|
||||
return regex.IsMatch(filename);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// GUID~TPE1-TALB-TYER~TRCK-TPE1-TIT2
|
||||
/// </summary>
|
||||
/// <param name="filename"></param>
|
||||
/// <returns></returns>
|
||||
public static bool IsTpe1TalbTyerTrckTpe1Tit2(string filename)
|
||||
{
|
||||
var regex = new Regex(@"[-a-zA-Z0-9]{36}~[\w\s,$/.'`#&()!]+-[\w\s,$/.'`#&()!]+-[0-9]{4}[\]\)]*~[\w\s,$/.'`#&()!]+-[\w\s,$/.'`#&()!]+");
|
||||
return regex.IsMatch(filename);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// GUID~TPE1-TALB (TYER)~TRCK-TIT2
|
||||
/// </summary>
|
||||
/// <param name="filename"></param>
|
||||
/// <returns></returns>
|
||||
public static bool IsTpe1TalbTyerTrckTit2(string filename)
|
||||
{
|
||||
var regex = new Regex(@"[-a-zA-Z0-9]{36}~[\w\s,$/.'`#&()!]+-[\w\s,$/.'`#&()!]+~[\w\s,$/.'`#&()!]+-\s[0-9]{2}\s-[\w\s,$/.'`#&()!]+");
|
||||
return regex.IsMatch(filename);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// GUID~TPE1~TRCK TIT2
|
||||
/// </summary>
|
||||
/// <param name="filename"></param>
|
||||
/// <returns></returns>
|
||||
public static bool IsTpe1TrckTit2(string filename)
|
||||
{
|
||||
var regex = new Regex(@"[-a-zA-Z0-9]{36}~[-\w\s,$/.'`#&()!]+[ -~][0-9]{2}[\w\s,$/.'-`#&()!]+");
|
||||
return regex.IsMatch(filename);
|
||||
}
|
||||
}
|
||||
}
|
12
RoadieLibrary/SearchEngines/MetaData/IArtistSearchEngine.cs
Normal file
|
@ -0,0 +1,12 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Roadie.Library.SearchEngines.MetaData
|
||||
{
|
||||
public interface IArtistSearchEngine
|
||||
{
|
||||
bool IsEnabled { get; }
|
||||
|
||||
Task<OperationResult<IEnumerable<ArtistSearchResult>>> PerformArtistSearch(string query, int resultsCount);
|
||||
}
|
||||
}
|
197
RoadieLibrary/SearchEngines/MetaData/ID3Tags/ID3TagsHelper.cs
Normal file
|
@ -0,0 +1,197 @@
|
|||
using Roadie.Library.Caching;
|
||||
using Orthogonal.NTagLite;
|
||||
using Roadie.Library.Utility;
|
||||
using Roadie.Library.Extensions;
|
||||
using Roadie.Library.Logging;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Roadie.Library.MetaData.Audio;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
|
||||
namespace Roadie.Library.MetaData.ID3Tags
|
||||
{
|
||||
public class ID3TagsHelper : MetaDataProviderBase
|
||||
{
|
||||
public ID3TagsHelper(IConfiguration configuration, ICacheManager cacheManager, ILogger loggingService) : base(configuration, cacheManager, loggingService)
|
||||
{
|
||||
}
|
||||
|
||||
public bool WriteTags(AudioMetaData metaData, string filename, bool force = false)
|
||||
{
|
||||
try
|
||||
{
|
||||
var tagFile = TagLib.File.Create(filename);
|
||||
tagFile.Tag.AlbumArtists = null;
|
||||
tagFile.Tag.AlbumArtists = new[] { metaData.Artist };
|
||||
tagFile.Tag.Performers = null;
|
||||
if (metaData.TrackArtists.Any())
|
||||
{
|
||||
tagFile.Tag.Performers = metaData.TrackArtists.ToArray();
|
||||
}
|
||||
tagFile.Tag.Album = metaData.Release;
|
||||
tagFile.Tag.Title = metaData.Title;
|
||||
tagFile.Tag.Year = force ? (uint)(metaData.Year ?? 0) : tagFile.Tag.Year > 0 ? tagFile.Tag.Year : (uint)(metaData.Year ?? 0);
|
||||
tagFile.Tag.Track = force ? (uint)(metaData.TrackNumber ?? 0) : tagFile.Tag.Track > 0 ? tagFile.Tag.Track : (uint)(metaData.TrackNumber ?? 0);
|
||||
tagFile.Tag.TrackCount = force ? (uint)(metaData.TotalTrackNumbers ?? 0) : tagFile.Tag.TrackCount > 0 ? tagFile.Tag.TrackCount : (uint)(metaData.TotalTrackNumbers ?? 0);
|
||||
tagFile.Tag.Disc = force ? (uint)(metaData.Disk ?? 0) : tagFile.Tag.Disc > 0 ? tagFile.Tag.Disc : (uint)(metaData.Disk ?? 0);
|
||||
tagFile.Tag.Pictures = metaData.Images == null ? null : metaData.Images.Select(x => new TagLib.Picture
|
||||
{
|
||||
Data = new TagLib.ByteVector(x.Data),
|
||||
Description = x.Description,
|
||||
MimeType = x.MimeType,
|
||||
Type = (TagLib.PictureType)x.Type
|
||||
}).ToArray();
|
||||
tagFile.Save();
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this.Logger.Error(ex, string.Format("MetaData [{0}], Filename [{1}]", metaData.ToString(), filename));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public OperationResult<AudioMetaData> MetaDataForFile(string fileName)
|
||||
{
|
||||
var result = this.MetaDataForFileFromTagLib(fileName);
|
||||
if (result.IsSuccess)
|
||||
{
|
||||
return result;
|
||||
}
|
||||
result = this.MetaDataForFileFromNTagLite(fileName);
|
||||
if (result.IsSuccess)
|
||||
{
|
||||
return result;
|
||||
}
|
||||
return new OperationResult<AudioMetaData>();
|
||||
}
|
||||
|
||||
public OperationResult<IEnumerable<AudioMetaData>> MetaDataForFolder(string folderName)
|
||||
{
|
||||
return this.MetaDataForFiles(Directory.EnumerateFiles(folderName, "*.mp3", SearchOption.AllDirectories).ToArray());
|
||||
}
|
||||
|
||||
public OperationResult<IEnumerable<AudioMetaData>> MetaDataForFiles(IEnumerable<string> fileNames)
|
||||
{
|
||||
var result = new List<AudioMetaData>();
|
||||
foreach (var fileName in fileNames)
|
||||
{
|
||||
var r = this.MetaDataForFileFromTagLib(fileName);
|
||||
if (r.IsSuccess)
|
||||
{
|
||||
result.Add(r.Data);
|
||||
}
|
||||
else
|
||||
{
|
||||
r = this.MetaDataForFileFromNTagLite(fileName);
|
||||
if (r.IsSuccess)
|
||||
{
|
||||
result.Add(r.Data);
|
||||
}
|
||||
}
|
||||
}
|
||||
return new OperationResult<IEnumerable<AudioMetaData>>
|
||||
{
|
||||
IsSuccess = result.Any(),
|
||||
Data = result
|
||||
};
|
||||
}
|
||||
|
||||
private OperationResult<AudioMetaData> MetaDataForFileFromTagLib(string fileName)
|
||||
{
|
||||
var sw = new Stopwatch();
|
||||
sw.Start();
|
||||
AudioMetaData result = new AudioMetaData();
|
||||
var isSuccess = false;
|
||||
try
|
||||
{
|
||||
var tagFile = TagLib.File.Create(fileName);
|
||||
result.Release = tagFile.Tag.Album;
|
||||
result.Artist = !string.IsNullOrEmpty(tagFile.Tag.JoinedAlbumArtists) ? tagFile.Tag.JoinedAlbumArtists : tagFile.Tag.JoinedPerformers;
|
||||
result.ArtistRaw = !string.IsNullOrEmpty(tagFile.Tag.JoinedAlbumArtists) ? tagFile.Tag.JoinedAlbumArtists : tagFile.Tag.JoinedPerformers;
|
||||
result.Genres = tagFile.Tag.Genres != null ? tagFile.Tag.Genres : new string[0];
|
||||
result.TrackArtist = tagFile.Tag.JoinedPerformers;
|
||||
result.TrackArtistRaw = tagFile.Tag.JoinedPerformers;
|
||||
result.AudioBitrate = (tagFile.Properties.AudioBitrate > 0 ? (int?)tagFile.Properties.AudioBitrate : null);
|
||||
result.AudioChannels = (tagFile.Properties.AudioChannels > 0 ? (int?)tagFile.Properties.AudioChannels : null);
|
||||
result.AudioSampleRate = (tagFile.Properties.AudioSampleRate > 0 ? (int?)tagFile.Properties.AudioSampleRate : null);
|
||||
result.Disk = (tagFile.Tag.Disc > 0 ? (int?)tagFile.Tag.Disc : null);
|
||||
result.Images = (tagFile.Tag.Pictures != null ? tagFile.Tag.Pictures.Select(x => new AudioMetaDataImage
|
||||
{
|
||||
Data = x.Data.Data,
|
||||
Description = x.Description,
|
||||
MimeType = x.MimeType,
|
||||
Type = (AudioMetaDataImageType)x.Type
|
||||
}).ToArray() : null);
|
||||
result.Time = (tagFile.Properties.Duration.TotalMinutes > 0 ? (TimeSpan?)tagFile.Properties.Duration : null);
|
||||
result.Title = tagFile.Tag.Title.ToTitleCase(false);
|
||||
result.TotalTrackNumbers = (tagFile.Tag.TrackCount > 0 ? (int?)tagFile.Tag.TrackCount : null);
|
||||
result.TrackNumber = (tagFile.Tag.Track > 0 ? (short?)tagFile.Tag.Track : null);
|
||||
result.Year = (tagFile.Tag.Year > 0 ? (int?)tagFile.Tag.Year : null);
|
||||
isSuccess = true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this.Logger.Error(ex, "MetaDataForFileFromTagLib Filename [" + fileName + "] Error [" + ex.Serialize() + "]");
|
||||
}
|
||||
sw.Stop();
|
||||
return new OperationResult<AudioMetaData>
|
||||
{
|
||||
IsSuccess = isSuccess,
|
||||
OperationTime = sw.ElapsedMilliseconds,
|
||||
Data = result
|
||||
};
|
||||
}
|
||||
|
||||
private OperationResult<AudioMetaData> MetaDataForFileFromNTagLite(string fileName)
|
||||
{
|
||||
var sw = new Stopwatch();
|
||||
sw.Start();
|
||||
AudioMetaData result = new AudioMetaData();
|
||||
var isSuccess = false;
|
||||
try
|
||||
{
|
||||
var file = LiteFile.LoadFromFile(fileName);
|
||||
var tpos = file.Tag.FindFirstFrameById(FrameId.TPOS);
|
||||
Picture[] pics = file.Tag.FindFramesById(FrameId.APIC).Select(f => f.GetPicture()).ToArray();
|
||||
result.Release = file.Tag.Album;
|
||||
result.Artist = file.Tag.Artist;
|
||||
result.ArtistRaw = file.Tag.Artist;
|
||||
result.Genres = (file.Tag.Genre ?? string.Empty).Split(';');
|
||||
result.TrackArtist = file.Tag.OriginalArtist;
|
||||
result.TrackArtistRaw = file.Tag.OriginalArtist;
|
||||
result.AudioBitrate = file.Bitrate;
|
||||
result.AudioChannels = file.AudioMode.HasValue ? (int?)file.AudioMode.Value : null;
|
||||
result.AudioSampleRate = file.Frequency;
|
||||
result.Disk = tpos != null ? SafeParser.ToNumber<int?>(tpos.Text) : null;
|
||||
result.Images = pics.Select(x => new AudioMetaDataImage
|
||||
{
|
||||
Data = x.Data,
|
||||
Description = x.Description,
|
||||
MimeType = x.MimeType,
|
||||
Type = (AudioMetaDataImageType)x.PictureType
|
||||
}).ToArray();
|
||||
result.Time = file.Duration;
|
||||
result.Title = file.Tag.Title.ToTitleCase(false);
|
||||
result.TotalTrackNumbers = file.Tag.TrackCount;
|
||||
result.TrackNumber = file.Tag.TrackNumber;
|
||||
result.Year = file.Tag.Year;
|
||||
isSuccess = true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this.Logger.Error(ex, "MetaDataForFileFromTagLib Filename [" + fileName + "] Error [" + ex.Serialize() + "]");
|
||||
}
|
||||
sw.Stop();
|
||||
return new OperationResult<AudioMetaData>
|
||||
{
|
||||
IsSuccess = isSuccess,
|
||||
OperationTime = sw.ElapsedMilliseconds,
|
||||
Data = result
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
12
RoadieLibrary/SearchEngines/MetaData/ILabelSearchEngine.cs
Normal file
|
@ -0,0 +1,12 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Roadie.Library.SearchEngines.MetaData
|
||||
{
|
||||
public interface ILabelSearchEngine
|
||||
{
|
||||
bool IsEnabled { get; }
|
||||
|
||||
Task<OperationResult<IEnumerable<LabelSearchResult>>> PerformLabelSearch(string labelName, int resultsCount);
|
||||
}
|
||||
}
|
12
RoadieLibrary/SearchEngines/MetaData/IReleaseSearchEngine.cs
Normal file
|
@ -0,0 +1,12 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Roadie.Library.SearchEngines.MetaData
|
||||
{
|
||||
public interface IReleaseSearchEngine
|
||||
{
|
||||
bool IsEnabled { get; }
|
||||
|
||||
Task<OperationResult<IEnumerable<ReleaseSearchResult>>> PerformReleaseSearch(string artistName, string query, int resultsCount);
|
||||
}
|
||||
}
|
14
RoadieLibrary/SearchEngines/MetaData/LabelSearchResult.cs
Normal file
|
@ -0,0 +1,14 @@
|
|||
using System;
|
||||
|
||||
namespace Roadie.Library.SearchEngines.MetaData
|
||||
{
|
||||
[Serializable]
|
||||
public class LabelSearchResult : SearchResultBase
|
||||
{
|
||||
public string LabelName { get; set; }
|
||||
public string LabelSortName { get; set; }
|
||||
public DateTime? EndDate { get; set; }
|
||||
public DateTime? StartDate { get; set; }
|
||||
public string LabelImageUrl { get; set; }
|
||||
}
|
||||
}
|
464
RoadieLibrary/SearchEngines/MetaData/LastFm/Entities.cs
Normal file
|
@ -0,0 +1,464 @@
|
|||
using System.Collections.Generic;
|
||||
|
||||
namespace Roadie.Library.SearchEngines.MetaData.LastFm
|
||||
{
|
||||
/// <remarks/>
|
||||
[System.Xml.Serialization.XmlTypeAttribute(AnonymousType = true)]
|
||||
[System.Xml.Serialization.XmlRootAttribute(Namespace = "", IsNullable = false)]
|
||||
public partial class lfm
|
||||
{
|
||||
private lfmAlbum albumField;
|
||||
|
||||
private string statusField;
|
||||
|
||||
/// <remarks/>
|
||||
public lfmAlbum album
|
||||
{
|
||||
get
|
||||
{
|
||||
return this.albumField;
|
||||
}
|
||||
set
|
||||
{
|
||||
this.albumField = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <remarks/>
|
||||
[System.Xml.Serialization.XmlAttributeAttribute()]
|
||||
public string status
|
||||
{
|
||||
get
|
||||
{
|
||||
return this.statusField;
|
||||
}
|
||||
set
|
||||
{
|
||||
this.statusField = value;
|
||||
}
|
||||
}
|
||||
|
||||
public lfm()
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
/// <remarks/>
|
||||
[System.Xml.Serialization.XmlTypeAttribute(AnonymousType = true)]
|
||||
public partial class lfmAlbum
|
||||
{
|
||||
private string nameField;
|
||||
|
||||
private string artistField;
|
||||
|
||||
private string mbidField;
|
||||
|
||||
private string urlField;
|
||||
|
||||
private List<lfmAlbumImage> imageField;
|
||||
|
||||
private ushort listenersField;
|
||||
|
||||
private uint playcountField;
|
||||
|
||||
private List<lfmAlbumTrack> tracksField;
|
||||
|
||||
private List<lfmAlbumTag> tagsField;
|
||||
|
||||
/// <remarks/>
|
||||
public string name
|
||||
{
|
||||
get
|
||||
{
|
||||
return this.nameField;
|
||||
}
|
||||
set
|
||||
{
|
||||
this.nameField = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <remarks/>
|
||||
public string artist
|
||||
{
|
||||
get
|
||||
{
|
||||
return this.artistField;
|
||||
}
|
||||
set
|
||||
{
|
||||
this.artistField = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <remarks/>
|
||||
public string mbid
|
||||
{
|
||||
get
|
||||
{
|
||||
return this.mbidField;
|
||||
}
|
||||
set
|
||||
{
|
||||
this.mbidField = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <remarks/>
|
||||
public string url
|
||||
{
|
||||
get
|
||||
{
|
||||
return this.urlField;
|
||||
}
|
||||
set
|
||||
{
|
||||
this.urlField = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <remarks/>
|
||||
[System.Xml.Serialization.XmlElementAttribute("image")]
|
||||
public List<lfmAlbumImage> image
|
||||
{
|
||||
get
|
||||
{
|
||||
return this.imageField;
|
||||
}
|
||||
set
|
||||
{
|
||||
this.imageField = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <remarks/>
|
||||
public ushort listeners
|
||||
{
|
||||
get
|
||||
{
|
||||
return this.listenersField;
|
||||
}
|
||||
set
|
||||
{
|
||||
this.listenersField = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <remarks/>
|
||||
public uint playcount
|
||||
{
|
||||
get
|
||||
{
|
||||
return this.playcountField;
|
||||
}
|
||||
set
|
||||
{
|
||||
this.playcountField = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <remarks/>
|
||||
[System.Xml.Serialization.XmlArrayItemAttribute("track", IsNullable = false)]
|
||||
public List<lfmAlbumTrack> tracks
|
||||
{
|
||||
get
|
||||
{
|
||||
return this.tracksField;
|
||||
}
|
||||
set
|
||||
{
|
||||
this.tracksField = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <remarks/>
|
||||
[System.Xml.Serialization.XmlArrayItemAttribute("tag", IsNullable = false)]
|
||||
public List<lfmAlbumTag> tags
|
||||
{
|
||||
get
|
||||
{
|
||||
return this.tagsField;
|
||||
}
|
||||
set
|
||||
{
|
||||
this.tagsField = value;
|
||||
}
|
||||
}
|
||||
|
||||
public lfmAlbum()
|
||||
{ }
|
||||
}
|
||||
|
||||
/// <remarks/>
|
||||
[System.Xml.Serialization.XmlTypeAttribute(AnonymousType = true)]
|
||||
public partial class lfmAlbumImage
|
||||
{
|
||||
private string sizeField;
|
||||
|
||||
private string valueField;
|
||||
|
||||
/// <remarks/>
|
||||
[System.Xml.Serialization.XmlAttributeAttribute()]
|
||||
public string size
|
||||
{
|
||||
get
|
||||
{
|
||||
return this.sizeField;
|
||||
}
|
||||
set
|
||||
{
|
||||
this.sizeField = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <remarks/>
|
||||
[System.Xml.Serialization.XmlTextAttribute()]
|
||||
public string Value
|
||||
{
|
||||
get
|
||||
{
|
||||
return this.valueField;
|
||||
}
|
||||
set
|
||||
{
|
||||
this.valueField = value;
|
||||
}
|
||||
}
|
||||
|
||||
public lfmAlbumImage()
|
||||
{ }
|
||||
}
|
||||
|
||||
/// <remarks/>
|
||||
[System.Xml.Serialization.XmlTypeAttribute(AnonymousType = true)]
|
||||
public partial class lfmAlbumTrack
|
||||
{
|
||||
private string nameField;
|
||||
|
||||
private string urlField;
|
||||
|
||||
private ushort durationField;
|
||||
|
||||
private lfmAlbumTrackStreamable streamableField;
|
||||
|
||||
private lfmAlbumTrackArtist artistField;
|
||||
|
||||
private byte rankField;
|
||||
|
||||
/// <remarks/>
|
||||
public string name
|
||||
{
|
||||
get
|
||||
{
|
||||
return this.nameField;
|
||||
}
|
||||
set
|
||||
{
|
||||
this.nameField = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <remarks/>
|
||||
public string url
|
||||
{
|
||||
get
|
||||
{
|
||||
return this.urlField;
|
||||
}
|
||||
set
|
||||
{
|
||||
this.urlField = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <remarks/>
|
||||
public ushort duration
|
||||
{
|
||||
get
|
||||
{
|
||||
return this.durationField;
|
||||
}
|
||||
set
|
||||
{
|
||||
this.durationField = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <remarks/>
|
||||
public lfmAlbumTrackStreamable streamable
|
||||
{
|
||||
get
|
||||
{
|
||||
return this.streamableField;
|
||||
}
|
||||
set
|
||||
{
|
||||
this.streamableField = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <remarks/>
|
||||
public lfmAlbumTrackArtist artist
|
||||
{
|
||||
get
|
||||
{
|
||||
return this.artistField;
|
||||
}
|
||||
set
|
||||
{
|
||||
this.artistField = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <remarks/>
|
||||
[System.Xml.Serialization.XmlAttributeAttribute()]
|
||||
public byte rank
|
||||
{
|
||||
get
|
||||
{
|
||||
return this.rankField;
|
||||
}
|
||||
set
|
||||
{
|
||||
this.rankField = value;
|
||||
}
|
||||
}
|
||||
|
||||
public lfmAlbumTrack()
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
/// <remarks/>
|
||||
[System.Xml.Serialization.XmlTypeAttribute(AnonymousType = true)]
|
||||
public partial class lfmAlbumTrackStreamable
|
||||
{
|
||||
private byte fulltrackField;
|
||||
|
||||
private byte valueField;
|
||||
|
||||
/// <remarks/>
|
||||
[System.Xml.Serialization.XmlAttributeAttribute()]
|
||||
public byte fulltrack
|
||||
{
|
||||
get
|
||||
{
|
||||
return this.fulltrackField;
|
||||
}
|
||||
set
|
||||
{
|
||||
this.fulltrackField = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <remarks/>
|
||||
[System.Xml.Serialization.XmlTextAttribute()]
|
||||
public byte Value
|
||||
{
|
||||
get
|
||||
{
|
||||
return this.valueField;
|
||||
}
|
||||
set
|
||||
{
|
||||
this.valueField = value;
|
||||
}
|
||||
}
|
||||
|
||||
public lfmAlbumTrackStreamable()
|
||||
{ }
|
||||
}
|
||||
|
||||
/// <remarks/>
|
||||
[System.Xml.Serialization.XmlTypeAttribute(AnonymousType = true)]
|
||||
public partial class lfmAlbumTrackArtist
|
||||
{
|
||||
private string nameField;
|
||||
|
||||
private string mbidField;
|
||||
|
||||
private string urlField;
|
||||
|
||||
/// <remarks/>
|
||||
public string name
|
||||
{
|
||||
get
|
||||
{
|
||||
return this.nameField;
|
||||
}
|
||||
set
|
||||
{
|
||||
this.nameField = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <remarks/>
|
||||
public string mbid
|
||||
{
|
||||
get
|
||||
{
|
||||
return this.mbidField;
|
||||
}
|
||||
set
|
||||
{
|
||||
this.mbidField = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <remarks/>
|
||||
public string url
|
||||
{
|
||||
get
|
||||
{
|
||||
return this.urlField;
|
||||
}
|
||||
set
|
||||
{
|
||||
this.urlField = value;
|
||||
}
|
||||
}
|
||||
|
||||
public lfmAlbumTrackArtist()
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
/// <remarks/>
|
||||
[System.Xml.Serialization.XmlTypeAttribute(AnonymousType = true)]
|
||||
public partial class lfmAlbumTag
|
||||
{
|
||||
private string nameField;
|
||||
|
||||
private string urlField;
|
||||
|
||||
/// <remarks/>
|
||||
public string name
|
||||
{
|
||||
get
|
||||
{
|
||||
return this.nameField;
|
||||
}
|
||||
set
|
||||
{
|
||||
this.nameField = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <remarks/>
|
||||
public string url
|
||||
{
|
||||
get
|
||||
{
|
||||
return this.urlField;
|
||||
}
|
||||
set
|
||||
{
|
||||
this.urlField = value;
|
||||
}
|
||||
}
|
||||
|
||||
public lfmAlbumTag()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
206
RoadieLibrary/SearchEngines/MetaData/LastFm/LastFmHelper.cs
Normal file
|
@ -0,0 +1,206 @@
|
|||
using Roadie.Library.Caching;
|
||||
using IF.Lastfm.Core.Api;
|
||||
using IF.Lastfm.Core.Objects;
|
||||
using RestSharp;
|
||||
using Roadie.Library.Extensions;
|
||||
using Roadie.Library.SearchEngines.MetaData;
|
||||
using Roadie.Library.SearchEngines.MetaData.LastFm;
|
||||
using Roadie.Library.Utility;
|
||||
using Roadie.Library.Logging;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Roadie.Library.MetaData.Audio;
|
||||
using Roadie.Library.Setttings;
|
||||
|
||||
namespace Roadie.Library.MetaData.LastFm
|
||||
{
|
||||
public class LastFmHelper : MetaDataProviderBase, IArtistSearchEngine, IReleaseSearchEngine
|
||||
{
|
||||
public override bool IsEnabled
|
||||
{
|
||||
get
|
||||
{
|
||||
return this.Configuration.GetValue<bool>("Integrations:LastFmProviderEnabled", true) &&
|
||||
!string.IsNullOrEmpty(this.ApiKey.Key);
|
||||
}
|
||||
}
|
||||
|
||||
public LastFmHelper(IConfiguration configuration, ICacheManager cacheManager, ILogger loggingService) : base(configuration, cacheManager, loggingService)
|
||||
{
|
||||
this._apiKey = configuration.GetValue<List<ApiKey>>("ApiKeys", new List<ApiKey>()).FirstOrDefault(x => x.ApiName == "LastFMApiKey") ?? new ApiKey();
|
||||
}
|
||||
|
||||
public async Task<OperationResult<IEnumerable<ArtistSearchResult>>> PerformArtistSearch(string query, int resultsCount)
|
||||
{
|
||||
try
|
||||
{
|
||||
this.Logger.Trace("LastFmHelper:PerformArtistSearch:{0}", query);
|
||||
var auth = new LastAuth(this.ApiKey.Key, this.ApiKey.Secret);
|
||||
var albumApi = new ArtistApi(auth);
|
||||
var response = await albumApi.GetInfoAsync(query);
|
||||
if (!response.Success)
|
||||
{
|
||||
return new OperationResult<IEnumerable<ArtistSearchResult>>();
|
||||
}
|
||||
var lastFmArtist = response.Content;
|
||||
var result = new ArtistSearchResult
|
||||
{
|
||||
ArtistName = lastFmArtist.Name,
|
||||
LastFMId = lastFmArtist.Id,
|
||||
MusicBrainzId = lastFmArtist.Mbid,
|
||||
Bio = lastFmArtist.Bio != null ? lastFmArtist.Bio.Content : null
|
||||
};
|
||||
if (lastFmArtist.Tags != null)
|
||||
{
|
||||
result.Tags = lastFmArtist.Tags.Select(x => x.Name).ToList();
|
||||
}
|
||||
if (lastFmArtist.MainImage != null && (lastFmArtist.MainImage.ExtraLarge != null || lastFmArtist.MainImage.Large != null ))
|
||||
{
|
||||
result.ArtistThumbnailUrl = (lastFmArtist.MainImage.ExtraLarge ?? lastFmArtist.MainImage.Large).ToString();
|
||||
}
|
||||
if (lastFmArtist.Url != null)
|
||||
{
|
||||
result.Urls = new string[] { lastFmArtist.Url.ToString() };
|
||||
}
|
||||
return new OperationResult<IEnumerable<ArtistSearchResult>>
|
||||
{
|
||||
IsSuccess = response.Success,
|
||||
Data = new List<ArtistSearchResult> { result }
|
||||
};
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this.Logger.Error(ex, ex.Serialize());
|
||||
}
|
||||
return new OperationResult<IEnumerable<ArtistSearchResult>>();
|
||||
}
|
||||
|
||||
public async Task<OperationResult<IEnumerable<ReleaseSearchResult>>> PerformReleaseSearch(string artistName, string query, int resultsCount)
|
||||
{
|
||||
var request = new RestRequest(Method.GET);
|
||||
var client = new RestClient(string.Format("http://ws.audioscrobbler.com/2.0/?method=album.getinfo&api_key={0}&artist={1}&album={2}&format=xml", this.ApiKey.Key, artistName, query));
|
||||
var responseData = await client.ExecuteTaskAsync<lfm>(request);
|
||||
|
||||
ReleaseSearchResult result = null;
|
||||
|
||||
var response = responseData != null && responseData.Data != null ? responseData.Data : null;
|
||||
if (response != null && response.album != null)
|
||||
{
|
||||
var lastFmAlbum = response.album;
|
||||
result = new ReleaseSearchResult
|
||||
{
|
||||
ReleaseTitle = lastFmAlbum.name,
|
||||
MusicBrainzId = lastFmAlbum.mbid
|
||||
};
|
||||
|
||||
if (lastFmAlbum.image != null)
|
||||
{
|
||||
result.ImageUrls = lastFmAlbum.image.Where(x => x.size == "extralarge").Select(x => x.Value).ToList();
|
||||
}
|
||||
if (lastFmAlbum.tags != null)
|
||||
{
|
||||
result.Tags = lastFmAlbum.tags.Select(x => x.name).ToList();
|
||||
}
|
||||
if (lastFmAlbum.tracks != null)
|
||||
{
|
||||
var tracks = new List<TrackSearchResult>();
|
||||
foreach (var lastFmTrack in lastFmAlbum.tracks)
|
||||
{
|
||||
tracks.Add(new TrackSearchResult
|
||||
{
|
||||
TrackNumber = SafeParser.ToNumber<short?>(lastFmTrack.rank),
|
||||
Title = lastFmTrack.name,
|
||||
Duration = SafeParser.ToNumber<int?>(lastFmTrack.duration),
|
||||
Urls = string.IsNullOrEmpty(lastFmTrack.url) ? new string[] { lastFmTrack.url } : null,
|
||||
});
|
||||
}
|
||||
result.ReleaseMedia = new List<ReleaseMediaSearchResult>
|
||||
{
|
||||
new ReleaseMediaSearchResult
|
||||
{
|
||||
ReleaseMediaNumber = 1,
|
||||
Tracks = tracks
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
return new OperationResult<IEnumerable<ReleaseSearchResult>>
|
||||
{
|
||||
IsSuccess = result != null,
|
||||
Data = new List<ReleaseSearchResult> { result }
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<AudioMetaData>> TracksForRelease(string artist, string Release)
|
||||
{
|
||||
if (string.IsNullOrEmpty(artist) || string.IsNullOrEmpty(Release))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
var result = new List<AudioMetaData>();
|
||||
|
||||
try
|
||||
{
|
||||
var responseCacheKey = string.Format("uri:lastFm:artistAndRelease:{0}:{1}", artist, Release);
|
||||
LastAlbum releaseInfo = this.CacheManager.Get<LastAlbum>(responseCacheKey);
|
||||
if (releaseInfo == null)
|
||||
{
|
||||
try
|
||||
{
|
||||
var auth = new LastAuth(this.ApiKey.Key, this.ApiKey.Secret);
|
||||
var albumApi = new AlbumApi(auth); // this is an unauthenticated call to the API
|
||||
var response = await albumApi.GetInfoAsync(artist, Release);
|
||||
releaseInfo = response.Content;
|
||||
if (releaseInfo != null)
|
||||
{
|
||||
this.CacheManager.Add(responseCacheKey, releaseInfo);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
this.Logger.Warning("LastFmAPI: Error Getting Tracks For Artist [{0}], Release [{1}]", artist, Release);
|
||||
}
|
||||
}
|
||||
|
||||
if (releaseInfo != null && releaseInfo.Tracks != null && releaseInfo.Tracks.Any())
|
||||
{
|
||||
var tracktotal = releaseInfo.Tracks.Where(x => x.Rank.HasValue).Max(x => x.Rank);
|
||||
List<AudioMetaDataImage> images = null;
|
||||
if (releaseInfo.Images != null)
|
||||
{
|
||||
images = releaseInfo.Images.Select(x => new AudioMetaDataImage
|
||||
{
|
||||
Url = x.AbsoluteUri
|
||||
}).ToList();
|
||||
}
|
||||
foreach (var track in releaseInfo.Tracks)
|
||||
{
|
||||
result.Add(new AudioMetaData
|
||||
{
|
||||
Artist = track.ArtistName,
|
||||
Release = track.AlbumName,
|
||||
Title = track.Name,
|
||||
Year = releaseInfo.ReleaseDateUtc != null ? (int?)releaseInfo.ReleaseDateUtc.Value.Year : null,
|
||||
TrackNumber = (short?)track.Rank,
|
||||
TotalTrackNumbers = tracktotal,
|
||||
Time = track.Duration,
|
||||
LastFmId = track.Id,
|
||||
ReleaseLastFmId = releaseInfo.Id,
|
||||
ReleaseMusicBrainzId = releaseInfo.Mbid,
|
||||
MusicBrainzId = track.Mbid,
|
||||
Images = images
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (System.Exception ex)
|
||||
{
|
||||
this.Logger.Error(ex, string.Format("LastFm: Error Getting Tracks For Artist [{0}], Release [{1}]", artist, Release));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
71
RoadieLibrary/SearchEngines/MetaData/MetaDataProviderBase.cs
Normal file
|
@ -0,0 +1,71 @@
|
|||
using Microsoft.Extensions.Configuration;
|
||||
using Roadie.Library.Caching;
|
||||
using Roadie.Library.Logging;
|
||||
using Roadie.Library.Setttings;
|
||||
using System.Net;
|
||||
|
||||
namespace Roadie.Library.MetaData
|
||||
{
|
||||
public abstract class MetaDataProviderBase
|
||||
{
|
||||
protected readonly IConfiguration _configuration = null;
|
||||
protected readonly ICacheManager _cacheManager = null;
|
||||
protected readonly ILogger _loggingService = null;
|
||||
|
||||
protected ICacheManager CacheManager
|
||||
{
|
||||
get
|
||||
{
|
||||
return this._cacheManager;
|
||||
}
|
||||
}
|
||||
|
||||
protected ILogger Logger
|
||||
{
|
||||
get
|
||||
{
|
||||
return this._loggingService;
|
||||
}
|
||||
}
|
||||
|
||||
protected IConfiguration Configuration
|
||||
{
|
||||
get
|
||||
{
|
||||
return this._configuration;
|
||||
}
|
||||
}
|
||||
|
||||
protected ApiKey _apiKey = null;
|
||||
protected ApiKey ApiKey
|
||||
{
|
||||
get
|
||||
{
|
||||
return this._apiKey;
|
||||
}
|
||||
}
|
||||
|
||||
public MetaDataProviderBase(IConfiguration configuration, ICacheManager cacheManager, ILogger loggingService)
|
||||
{
|
||||
this._configuration = configuration;
|
||||
this._cacheManager = cacheManager;
|
||||
this._loggingService = loggingService;
|
||||
|
||||
System.Net.ServicePointManager.ServerCertificateValidationCallback += delegate (object sender, System.Security.Cryptography.X509Certificates.X509Certificate certificate,
|
||||
System.Security.Cryptography.X509Certificates.X509Chain chain,
|
||||
System.Net.Security.SslPolicyErrors sslPolicyErrors)
|
||||
{
|
||||
return true; // **** Always accept
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
public virtual bool IsEnabled
|
||||
{
|
||||
get
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
namespace Roadie.Library.MetaData.MusicBrainz
|
||||
{
|
||||
public class CoverArtArchivesResult
|
||||
{
|
||||
public Image[] images { get; set; }
|
||||
public string release { get; set; }
|
||||
}
|
||||
|
||||
public class Image
|
||||
{
|
||||
public string[] types { get; set; }
|
||||
public bool front { get; set; }
|
||||
public bool back { get; set; }
|
||||
public int edit { get; set; }
|
||||
public string image { get; set; }
|
||||
public string comment { get; set; }
|
||||
public bool approved { get; set; }
|
||||
public string id { get; set; }
|
||||
public Thumbnails thumbnails { get; set; }
|
||||
}
|
||||
|
||||
public class Thumbnails
|
||||
{
|
||||
public string large { get; set; }
|
||||
public string small { get; set; }
|
||||
}
|
||||
}
|
307
RoadieLibrary/SearchEngines/MetaData/MusicBrainz/Entities.cs
Normal file
|
@ -0,0 +1,307 @@
|
|||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace Roadie.Library.MetaData.MusicBrainz
|
||||
{
|
||||
public class ArtistResult
|
||||
{
|
||||
public DateTime? created { get; set; }
|
||||
|
||||
public int? count { get; set; }
|
||||
|
||||
public int? offset { get; set; }
|
||||
|
||||
public List<Artist> artists { get; set; }
|
||||
}
|
||||
|
||||
[DebuggerDisplay("name: {name}")]
|
||||
public class Artist
|
||||
{
|
||||
public string country { get; set; }
|
||||
|
||||
public List<string> ipis { get; set; }
|
||||
|
||||
public Area area { get; set; }
|
||||
|
||||
[JsonProperty(PropertyName = "sort-name")]
|
||||
public string sortname { get; set; }
|
||||
|
||||
public string name { get; set; }
|
||||
|
||||
public string disambiguation { get; set; }
|
||||
|
||||
public LifeSpan lifespan { get; set; }
|
||||
|
||||
public List<Release> releases { get; set; }
|
||||
|
||||
public object end_area { get; set; }
|
||||
|
||||
public string id { get; set; }
|
||||
|
||||
public string type { get; set; }
|
||||
|
||||
public Begin_Area begin_area { get; set; }
|
||||
|
||||
public string gender { get; set; }
|
||||
|
||||
public List<Alias> aliases { get; set; }
|
||||
|
||||
public List<Tag> tags { get; set; }
|
||||
|
||||
[JsonProperty(PropertyName = "isni-list")]
|
||||
public List<string> isnis { get; set; }
|
||||
}
|
||||
|
||||
public class Area
|
||||
{
|
||||
public string sortname { get; set; }
|
||||
public string id { get; set; }
|
||||
public string name { get; set; }
|
||||
public string disambiguation { get; set; }
|
||||
public List<string> iso31661codes { get; set; }
|
||||
}
|
||||
|
||||
public class LifeSpan
|
||||
{
|
||||
public bool ended { get; set; }
|
||||
|
||||
public string begin { get; set; }
|
||||
|
||||
public string end { get; set; }
|
||||
}
|
||||
|
||||
public class Begin_Area
|
||||
{
|
||||
public string disambiguation { get; set; }
|
||||
|
||||
public List<string> iso_3166_3_codes { get; set; }
|
||||
|
||||
public string sortname { get; set; }
|
||||
|
||||
public string name { get; set; }
|
||||
|
||||
public string id { get; set; }
|
||||
|
||||
public List<string> iso_3166_2_codes { get; set; }
|
||||
|
||||
public List<string> iso_3166_1_codes { get; set; }
|
||||
}
|
||||
|
||||
public class ReleaseBrowseResult
|
||||
{
|
||||
[JsonProperty(PropertyName = "release-count")]
|
||||
public int? releasecount { get; set; }
|
||||
|
||||
public List<Release> releases { get; set; }
|
||||
|
||||
[JsonProperty(PropertyName = "release-offset")]
|
||||
public int? releaseoffset { get; set; }
|
||||
}
|
||||
|
||||
[DebuggerDisplay("title: {title}, date: {date}")]
|
||||
public class Release
|
||||
{
|
||||
public string country { get; set; }
|
||||
|
||||
public TextRepresentation textrepresentation { get; set; }
|
||||
|
||||
public string status { get; set; }
|
||||
|
||||
public string date { get; set; }
|
||||
|
||||
[JsonProperty(PropertyName = "cover-art-archive")]
|
||||
public CoverArtArchive coverartarchive { get; set; }
|
||||
|
||||
public string barcode { get; set; }
|
||||
|
||||
[JsonProperty(PropertyName = "release-events")]
|
||||
public List<ReleaseEvents> releaseevents { get; set; }
|
||||
|
||||
public string packaging { get; set; }
|
||||
|
||||
public string disambiguation { get; set; }
|
||||
|
||||
public List<Medium> media { get; set; }
|
||||
|
||||
public string id { get; set; }
|
||||
|
||||
public string title { get; set; }
|
||||
|
||||
public string asin { get; set; }
|
||||
|
||||
public string quality { get; set; }
|
||||
public string coverThumbnailUrl { get; set; }
|
||||
public List<string> imageUrls { get; set; }
|
||||
|
||||
[JsonProperty(PropertyName = "label-info")]
|
||||
public List<LabelInfo> labelinfo { get; set; }
|
||||
|
||||
public List<Relation> relations { get; set; }
|
||||
public List<object> aliases { get; set; }
|
||||
public ReleaseGroup releasegroup { get; set; }
|
||||
}
|
||||
|
||||
public class TextRepresentation
|
||||
{
|
||||
public string language { get; set; }
|
||||
public string script { get; set; }
|
||||
}
|
||||
|
||||
public class CoverArtArchive
|
||||
{
|
||||
public int? count { get; set; }
|
||||
|
||||
public bool front { get; set; }
|
||||
|
||||
public bool artwork { get; set; }
|
||||
|
||||
public bool back { get; set; }
|
||||
|
||||
public bool darkened { get; set; }
|
||||
}
|
||||
|
||||
public class ReleaseEvents
|
||||
{
|
||||
public Area area { get; set; }
|
||||
|
||||
public string date { get; set; }
|
||||
}
|
||||
|
||||
public class Medium
|
||||
{
|
||||
public int? trackoffset { get; set; }
|
||||
|
||||
public List<Track> tracks { get; set; }
|
||||
|
||||
[JsonProperty(PropertyName = "track-count")]
|
||||
public short? trackcount { get; set; }
|
||||
|
||||
public object format { get; set; }
|
||||
|
||||
public int? position { get; set; }
|
||||
|
||||
public string title { get; set; }
|
||||
}
|
||||
|
||||
public class Track
|
||||
{
|
||||
public int? length { get; set; }
|
||||
|
||||
public string position { get; set; }
|
||||
|
||||
public string number { get; set; }
|
||||
|
||||
public Recording recording { get; set; }
|
||||
|
||||
public string title { get; set; }
|
||||
|
||||
public string id { get; set; }
|
||||
}
|
||||
|
||||
public class Recording
|
||||
{
|
||||
public bool video { get; set; }
|
||||
public string id { get; set; }
|
||||
public int? length { get; set; }
|
||||
public string disambiguation { get; set; }
|
||||
public string title { get; set; }
|
||||
public List<Alias> aliases { get; set; }
|
||||
}
|
||||
|
||||
public class BeginArea
|
||||
{
|
||||
public string id { get; set; }
|
||||
|
||||
public string name { get; set; }
|
||||
|
||||
public string sortname { get; set; }
|
||||
}
|
||||
|
||||
public class Alias
|
||||
{
|
||||
[JsonProperty(PropertyName = "sort-name")]
|
||||
public string sortname { get; set; }
|
||||
|
||||
public string name { get; set; }
|
||||
|
||||
public object locale { get; set; }
|
||||
|
||||
public object type { get; set; }
|
||||
|
||||
public object primary { get; set; }
|
||||
|
||||
public object begindate { get; set; }
|
||||
|
||||
public object enddate { get; set; }
|
||||
}
|
||||
|
||||
public class Tag
|
||||
{
|
||||
public int? count { get; set; }
|
||||
|
||||
public string name { get; set; }
|
||||
}
|
||||
|
||||
public class ReleaseGroup
|
||||
{
|
||||
public List<object> secondarytypes { get; set; }
|
||||
public string primarytype { get; set; }
|
||||
public string title { get; set; }
|
||||
public List<object> aliases { get; set; }
|
||||
public string id { get; set; }
|
||||
public string disambiguation { get; set; }
|
||||
public string firstreleasedate { get; set; }
|
||||
}
|
||||
|
||||
public class Relation
|
||||
{
|
||||
public object begin { get; set; }
|
||||
public string targetcredit { get; set; }
|
||||
public string type { get; set; }
|
||||
public Url url { get; set; }
|
||||
public string typeid { get; set; }
|
||||
public string sourcecredit { get; set; }
|
||||
public List<object> attributes { get; set; }
|
||||
public AttributeValues attributevalues { get; set; }
|
||||
public string direction { get; set; }
|
||||
public object end { get; set; }
|
||||
public bool ended { get; set; }
|
||||
public string targettype { get; set; }
|
||||
}
|
||||
|
||||
public class Url
|
||||
{
|
||||
public string resource { get; set; }
|
||||
public string id { get; set; }
|
||||
}
|
||||
|
||||
public class AttributeValues
|
||||
{
|
||||
}
|
||||
|
||||
public class LabelInfo
|
||||
{
|
||||
[JsonProperty(PropertyName = "catalog-number")]
|
||||
public string catalognumber { get; set; }
|
||||
|
||||
public Label label { get; set; }
|
||||
}
|
||||
|
||||
public class Label
|
||||
{
|
||||
public string name { get; set; }
|
||||
|
||||
[JsonProperty(PropertyName = "label-code")]
|
||||
public int? labelcode { get; set; }
|
||||
|
||||
public string id { get; set; }
|
||||
|
||||
[JsonProperty(PropertyName = "sort-name")]
|
||||
public string sortname { get; set; }
|
||||
|
||||
public List<Alias> aliases { get; set; }
|
||||
public string disambiguation { get; set; }
|
||||
}
|
||||
}
|
|
@ -0,0 +1,499 @@
|
|||
using Microsoft.Extensions.Configuration;
|
||||
using Roadie.Library.Caching;
|
||||
using Roadie.Library.Data;
|
||||
using Roadie.Library.Extensions;
|
||||
using Roadie.Library.Logging;
|
||||
using Roadie.Library.MetaData.Audio;
|
||||
using Roadie.Library.SearchEngines.MetaData;
|
||||
using Roadie.Library.Utility;
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Roadie.Library.MetaData.MusicBrainz
|
||||
{
|
||||
public class MusicBrainzProvider : MetaDataProviderBase, IArtistSearchEngine, IReleaseSearchEngine
|
||||
{
|
||||
public override bool IsEnabled
|
||||
{
|
||||
get
|
||||
{
|
||||
return this.Configuration.GetValue("Integrations:MusicBrainzProviderEnabled", true);
|
||||
}
|
||||
}
|
||||
|
||||
public MusicBrainzProvider(IConfiguration configuration, ICacheManager cacheManager, ILogger logger) : base(configuration, cacheManager, logger)
|
||||
{
|
||||
}
|
||||
|
||||
public async Task<CoverArtArchivesResult> CoverArtForMusicBrainzReleaseById(string musicBrainzId)
|
||||
{
|
||||
return await MusicBrainzRequestHelper.GetAsync<CoverArtArchivesResult>(MusicBrainzRequestHelper.CreateCoverArtReleaseUrl(musicBrainzId));
|
||||
}
|
||||
|
||||
public async Task<Release> MusicBrainzReleaseById(string musicBrainzId)
|
||||
{
|
||||
if (string.IsNullOrEmpty(musicBrainzId))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
Release release = null;
|
||||
try
|
||||
{
|
||||
var artistCacheKey = string.Format("uri:musicbrainz:MusicBrainzReleaseById:{0}", musicBrainzId);
|
||||
release = this.CacheManager.Get<Release>(artistCacheKey);
|
||||
if (release == null)
|
||||
{
|
||||
release = await MusicBrainzRequestHelper.GetAsync<Release>(MusicBrainzRequestHelper.CreateLookupUrl("release", musicBrainzId, "labels+aliases+recordings+release-groups+media+url-rels"));
|
||||
if (release != null)
|
||||
{
|
||||
var coverUrls = await this.CoverArtForMusicBrainzReleaseById(musicBrainzId);
|
||||
if (coverUrls != null)
|
||||
{
|
||||
var frontCover = coverUrls.images.FirstOrDefault(i => i.front);
|
||||
release.imageUrls = coverUrls.images.Select(x => x.image).ToList();
|
||||
if (frontCover != null)
|
||||
{
|
||||
release.coverThumbnailUrl = frontCover.image;
|
||||
release.imageUrls = release.imageUrls.Where(x => x != release.coverThumbnailUrl).ToList();
|
||||
}
|
||||
}
|
||||
this.CacheManager.Add(artistCacheKey, release);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (HttpRequestException)
|
||||
{
|
||||
}
|
||||
if (release == null)
|
||||
{
|
||||
this.Logger.Warning("MusicBrainzReleaseById: MusicBrainzId [{0}], No MusicBrainz Release Found", musicBrainzId);
|
||||
}
|
||||
return release;
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<AudioMetaData>> MusicBrainzReleaseTracks(string artistName, string releaseTitle)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (string.IsNullOrEmpty(artistName) && string.IsNullOrEmpty(releaseTitle))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
// Find the Artist
|
||||
var artistCacheKey = string.Format("uri:musicbrainz:artist:{0}", artistName);
|
||||
var artistSearch = await this.PerformArtistSearch(artistName, 1);
|
||||
if (!artistSearch.IsSuccess)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
var artist = artistSearch.Data.First();
|
||||
|
||||
if (artist == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
var ReleaseCacheKey = string.Format("uri:musicbrainz:release:{0}", releaseTitle);
|
||||
var release = this.CacheManager.Get<Release>(ReleaseCacheKey);
|
||||
if (release == null)
|
||||
{
|
||||
// Now Get Artist Details including Releases
|
||||
var ReleaseResult = artist.Releases.FirstOrDefault(x => x.ReleaseTitle.Equals(releaseTitle, StringComparison.InvariantCultureIgnoreCase));
|
||||
if (ReleaseResult == null)
|
||||
{
|
||||
ReleaseResult = artist.Releases.FirstOrDefault(x => x.ReleaseTitle.EndsWith(releaseTitle, StringComparison.InvariantCultureIgnoreCase));
|
||||
|
||||
if (ReleaseResult == null)
|
||||
{
|
||||
ReleaseResult = artist.Releases.FirstOrDefault(x => x.ReleaseTitle.StartsWith(releaseTitle, StringComparison.InvariantCultureIgnoreCase));
|
||||
if (ReleaseResult == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Now get The Release Details
|
||||
release = await MusicBrainzRequestHelper.GetAsync<Release>(MusicBrainzRequestHelper.CreateLookupUrl("release", ReleaseResult.MusicBrainzId, "recordings"));
|
||||
if (release == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
this.CacheManager.Add(ReleaseCacheKey, release);
|
||||
}
|
||||
|
||||
var result = new List<AudioMetaData>();
|
||||
foreach (var media in release.media)
|
||||
{
|
||||
foreach (var track in media.tracks)
|
||||
{
|
||||
int date = 0;
|
||||
if (!string.IsNullOrEmpty(release.date))
|
||||
{
|
||||
if (release.date.Length > 4)
|
||||
{
|
||||
DateTime ReleaseDate = DateTime.MinValue;
|
||||
if (DateTime.TryParse(release.date, out ReleaseDate))
|
||||
{
|
||||
date = ReleaseDate.Year;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
int.TryParse(release.date, out date);
|
||||
}
|
||||
}
|
||||
result.Add(new AudioMetaData
|
||||
{
|
||||
ReleaseMusicBrainzId = release.id,
|
||||
MusicBrainzId = track.id,
|
||||
Artist = artist.ArtistName,
|
||||
Release = release.title,
|
||||
Title = track.title,
|
||||
Time = track.length.HasValue ? (TimeSpan?)TimeSpan.FromMilliseconds(track.length.Value) : null,
|
||||
TrackNumber = SafeParser.ToNumber<short?>(track.position ?? track.number) ?? 0,
|
||||
Disk = media.position,
|
||||
Year = date > 0 ? (int?)date : null,
|
||||
TotalTrackNumbers = media.trackcount,
|
||||
//tagFile.Tag.Pictures.Select(x => new AudoMetaDataImage
|
||||
//{
|
||||
// Data = x.Data.Data,
|
||||
// Description = x.Description,
|
||||
// MimeType = x.MimeType,
|
||||
// Type = (AudioMetaDataImageType)x.Type
|
||||
//}).ToArray()
|
||||
});
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public async Task<OperationResult<IEnumerable<ArtistSearchResult>>> PerformArtistSearch(string query, int resultsCount)
|
||||
{
|
||||
ArtistSearchResult result = null;
|
||||
try
|
||||
{
|
||||
this.Logger.Trace("MusicBrainzProvider:PerformArtistSearch:{0}", query);
|
||||
// Find the Artist
|
||||
var artistCacheKey = string.Format("uri:musicbrainz:ArtistSearchResult:{0}", query);
|
||||
result = this.CacheManager.Get<ArtistSearchResult>(artistCacheKey);
|
||||
if (result == null)
|
||||
{
|
||||
ArtistResult artistResult = null;
|
||||
try
|
||||
{
|
||||
artistResult = await MusicBrainzRequestHelper.GetAsync<ArtistResult>(MusicBrainzRequestHelper.CreateSearchTemplate("artist", query, resultsCount, 0));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this.Logger.Error(ex);
|
||||
}
|
||||
if (artistResult == null || artistResult.artists == null || artistResult.count < 1)
|
||||
{
|
||||
return new OperationResult<IEnumerable<ArtistSearchResult>>();
|
||||
}
|
||||
var a = artistResult.artists.First();
|
||||
var mbArtist = await MusicBrainzRequestHelper.GetAsync<Artist>(MusicBrainzRequestHelper.CreateLookupUrl("artist", artistResult.artists.First().id, "releases"));
|
||||
if (mbArtist == null)
|
||||
{
|
||||
return new OperationResult<IEnumerable<ArtistSearchResult>>();
|
||||
}
|
||||
result = new ArtistSearchResult
|
||||
{
|
||||
ArtistName = mbArtist.name,
|
||||
ArtistSortName = mbArtist.sortname,
|
||||
MusicBrainzId = mbArtist.id,
|
||||
ArtistType = mbArtist.type,
|
||||
IPIs = mbArtist.ipis,
|
||||
ISNIs = mbArtist.isnis
|
||||
};
|
||||
if (mbArtist.lifespan != null)
|
||||
{
|
||||
result.BeginDate = SafeParser.ToDateTime(mbArtist.lifespan.begin);
|
||||
result.EndDate = SafeParser.ToDateTime(mbArtist.lifespan.end);
|
||||
}
|
||||
if (a.aliases != null)
|
||||
{
|
||||
result.AlternateNames = a.aliases.Select(x => x.name).Distinct().ToArray();
|
||||
}
|
||||
if (a.tags != null)
|
||||
{
|
||||
result.Tags = a.tags.Select(x => x.name).Distinct().ToArray();
|
||||
}
|
||||
var mbFilteredReleases = new List<Release>();
|
||||
var filteredPlaces = new List<string> { "US", "WORLDWIDE", "XW", "GB" };
|
||||
foreach (var release in mbArtist.releases)
|
||||
{
|
||||
if (filteredPlaces.Contains((release.country ?? string.Empty).ToUpper()))
|
||||
{
|
||||
mbFilteredReleases.Add(release);
|
||||
}
|
||||
}
|
||||
result.Releases = new List<ReleaseSearchResult>();
|
||||
var bag = new ConcurrentBag<Release>();
|
||||
var filteredReleaseDetails = mbFilteredReleases.Select(async release =>
|
||||
{
|
||||
bag.Add(await this.MusicBrainzReleaseById(release.id));
|
||||
});
|
||||
await Task.WhenAll(filteredReleaseDetails);
|
||||
foreach (var mbRelease in bag.Where(x => x != null))
|
||||
{
|
||||
var release = new ReleaseSearchResult
|
||||
{
|
||||
MusicBrainzId = mbRelease.id,
|
||||
ReleaseTitle = mbRelease.title,
|
||||
ReleaseThumbnailUrl = mbRelease.coverThumbnailUrl
|
||||
};
|
||||
if (mbRelease.imageUrls != null)
|
||||
{
|
||||
release.ImageUrls = mbRelease.imageUrls;
|
||||
}
|
||||
if (mbRelease.releaseevents != null)
|
||||
{
|
||||
release.ReleaseDate = SafeParser.ToDateTime(mbRelease.releaseevents.First().date);
|
||||
}
|
||||
// Labels
|
||||
if (mbRelease.media != null)
|
||||
{
|
||||
var releaseMedias = new List<ReleaseMediaSearchResult>();
|
||||
foreach (var mbMedia in mbRelease.media)
|
||||
{
|
||||
var releaseMedia = new ReleaseMediaSearchResult
|
||||
{
|
||||
ReleaseMediaNumber = SafeParser.ToNumber<short?>(mbMedia.position),
|
||||
TrackCount = mbMedia.trackcount
|
||||
};
|
||||
if (mbMedia.tracks != null)
|
||||
{
|
||||
var releaseTracks = new List<TrackSearchResult>();
|
||||
foreach (var mbTrack in mbMedia.tracks)
|
||||
{
|
||||
releaseTracks.Add(new TrackSearchResult
|
||||
{
|
||||
MusicBrainzId = mbTrack.id,
|
||||
TrackNumber = SafeParser.ToNumber<short?>(mbTrack.number),
|
||||
Title = mbTrack.title,
|
||||
Duration = mbTrack.length
|
||||
});
|
||||
}
|
||||
releaseMedia.Tracks = releaseTracks;
|
||||
}
|
||||
releaseMedias.Add(releaseMedia);
|
||||
}
|
||||
release.ReleaseMedia = releaseMedias;
|
||||
}
|
||||
|
||||
result.Releases.Add(release);
|
||||
};
|
||||
this.CacheManager.Add(artistCacheKey, result);
|
||||
}
|
||||
}
|
||||
catch (HttpRequestException)
|
||||
{
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this.Logger.Error(ex);
|
||||
}
|
||||
if (result == null)
|
||||
{
|
||||
this.Logger.Warning("MusicBrainzArtist: ArtistName [{0}], No MusicBrainz Artist Found", query);
|
||||
}
|
||||
else
|
||||
{
|
||||
this.Logger.Trace("MusicBrainzArtist: Result [{0}]", query, result.ToString());
|
||||
}
|
||||
return new OperationResult<IEnumerable<ArtistSearchResult>>
|
||||
{
|
||||
IsSuccess = result != null,
|
||||
Data = new ArtistSearchResult[] { result }
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<OperationResult<IEnumerable<ReleaseSearchResult>>> PerformReleaseSearch(string artistName, string query, int resultsCount)
|
||||
{
|
||||
ReleaseSearchResult result = null;
|
||||
try
|
||||
{
|
||||
var releaseInfosForArtist = await this.ReleasesForArtist(artistName);
|
||||
if (releaseInfosForArtist != null)
|
||||
{
|
||||
var releaseInfo = releaseInfosForArtist.FirstOrDefault(x => x.title.Equals(query, StringComparison.OrdinalIgnoreCase));
|
||||
if (releaseInfo != null)
|
||||
{
|
||||
var mbRelease = await this.MusicBrainzReleaseById(releaseInfo.id);
|
||||
if (mbRelease != null)
|
||||
{
|
||||
result = new ReleaseSearchResult
|
||||
{
|
||||
ReleaseDate = mbRelease.releasegroup != null ? SafeParser.ToDateTime(mbRelease.releasegroup.firstreleasedate) : null,
|
||||
ReleaseTitle = mbRelease.title,
|
||||
MusicBrainzId = mbRelease.id,
|
||||
ReleaseType = mbRelease.releasegroup != null ? mbRelease.releasegroup.primarytype : null,
|
||||
};
|
||||
if (mbRelease.labelinfo != null)
|
||||
{
|
||||
var releaseLabels = new List<ReleaseLabelSearchResult>();
|
||||
foreach (var mbLabel in mbRelease.labelinfo)
|
||||
{
|
||||
releaseLabels.Add(new ReleaseLabelSearchResult
|
||||
{
|
||||
CatalogNumber = mbLabel.catalognumber,
|
||||
Label = new LabelSearchResult
|
||||
{
|
||||
LabelName = mbLabel.label.name,
|
||||
MusicBrainzId = mbLabel.label.id,
|
||||
LabelSortName = mbLabel.label.sortname,
|
||||
AlternateNames = mbLabel.label.aliases.Select(x => x.name).ToList()
|
||||
}
|
||||
});
|
||||
}
|
||||
result.ReleaseLabel = releaseLabels;
|
||||
}
|
||||
if (mbRelease.media != null)
|
||||
{
|
||||
var releaseMedia = new List<ReleaseMediaSearchResult>();
|
||||
foreach (var mbMedia in mbRelease.media.OrderBy(x => x.position))
|
||||
{
|
||||
var mediaTracks = new List<TrackSearchResult>();
|
||||
short trackLooper = 0;
|
||||
foreach (var mbTrack in mbMedia.tracks.OrderBy(x => x.position))
|
||||
{
|
||||
trackLooper++;
|
||||
mediaTracks.Add(new TrackSearchResult
|
||||
{
|
||||
Title = mbTrack.title,
|
||||
TrackNumber = trackLooper,
|
||||
Duration = mbTrack.length,
|
||||
MusicBrainzId = mbTrack.id,
|
||||
AlternateNames = mbTrack.recording != null && mbTrack.recording.aliases != null ? mbTrack.recording.aliases.Select(x => x.name).ToList() : null
|
||||
});
|
||||
}
|
||||
releaseMedia.Add(new ReleaseMediaSearchResult
|
||||
{
|
||||
ReleaseMediaNumber = SafeParser.ToNumber<short?>(mbMedia.position),
|
||||
ReleaseMediaSubTitle = mbMedia.title,
|
||||
TrackCount = SafeParser.ToNumber<short?>(mbMedia.trackcount),
|
||||
Tracks = mediaTracks
|
||||
});
|
||||
}
|
||||
result.ReleaseMedia = releaseMedia;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this.Logger.Error(ex, ex.Serialize());
|
||||
}
|
||||
if (result == null)
|
||||
{
|
||||
this.Logger.Warning("MusicBrainzArtist: ArtistName [{0}], ReleaseTitle [{0}], No MusicBrainz Release Found", artistName, query);
|
||||
}
|
||||
else
|
||||
{
|
||||
this.Logger.Trace("MusicBrainzArtist: Result [{0}]", query, result.ToString());
|
||||
}
|
||||
return new OperationResult<IEnumerable<ReleaseSearchResult>>
|
||||
{
|
||||
IsSuccess = result != null,
|
||||
Data = new ReleaseSearchResult[] { result }
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<Data.Release> ReleaseForMusicBrainzReleaseById(string musicBrainzId)
|
||||
{
|
||||
var release = await MusicBrainzReleaseById(musicBrainzId);
|
||||
if (release == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
var media = release.media.First();
|
||||
if (media == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var result = new Data.Release
|
||||
{
|
||||
Title = release.title.ToTitleCase(false),
|
||||
ReleaseDate = SafeParser.ToDateTime(release.date),
|
||||
MusicBrainzId = release.id
|
||||
};
|
||||
|
||||
var releaseMedia = new Data.ReleaseMedia
|
||||
{
|
||||
Tracks = media.tracks.Select(m => new Data.Track
|
||||
{
|
||||
TrackNumber = SafeParser.ToNumber<short>(m.position ?? m.number),
|
||||
Title = m.title.ToTitleCase(false),
|
||||
MusicBrainzId = m.id,
|
||||
}).ToList()
|
||||
};
|
||||
result.Medias = new List<ReleaseMedia> { releaseMedia };
|
||||
return result;
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<Release>> ReleasesForArtist(string artist, string artistMusicBrainzId = null)
|
||||
{
|
||||
try
|
||||
{
|
||||
var artistSearch = await this.PerformArtistSearch(artist, 1);
|
||||
if (artistSearch == null || !artistSearch.IsSuccess)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
var mbArtist = artistSearch.Data.First();
|
||||
if (string.IsNullOrEmpty(artistMusicBrainzId))
|
||||
{
|
||||
if (mbArtist == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
artistMusicBrainzId = mbArtist.MusicBrainzId;
|
||||
}
|
||||
var cacheKey = string.Format("uri:musicbrainz:ReleasesForArtist:{0}", artistMusicBrainzId);
|
||||
var result = this.CacheManager.Get<List<Release>>(cacheKey);
|
||||
if (result == null)
|
||||
{
|
||||
var pageSize = 50;
|
||||
var page = 0;
|
||||
var url = MusicBrainzRequestHelper.CreateArtistBrowseTemplate(artistMusicBrainzId, pageSize, 0);
|
||||
var mbReleaseBrowseResult = await MusicBrainzRequestHelper.GetAsync<ReleaseBrowseResult>(url);
|
||||
var totalReleases = mbReleaseBrowseResult != null ? mbReleaseBrowseResult.releasecount : 0;
|
||||
var totalPages = Math.Ceiling((decimal)totalReleases / (decimal)pageSize);
|
||||
result = new List<Release>();
|
||||
do
|
||||
{
|
||||
if (mbReleaseBrowseResult != null)
|
||||
{
|
||||
result.AddRange(mbReleaseBrowseResult.releases);
|
||||
}
|
||||
page++;
|
||||
mbReleaseBrowseResult = await MusicBrainzRequestHelper.GetAsync<ReleaseBrowseResult>(MusicBrainzRequestHelper.CreateArtistBrowseTemplate(artistMusicBrainzId, pageSize, pageSize * page));
|
||||
} while (page < totalPages);
|
||||
result = result.OrderBy(x => x.date).ThenBy(x => x.title).ToList();
|
||||
this.CacheManager.Add(cacheKey, result);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
catch (HttpRequestException)
|
||||
{
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this.Logger.Error(ex);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,71 @@
|
|||
using Roadie.Library.Utility;
|
||||
using System;
|
||||
using System.Net;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Roadie.Library.MetaData.MusicBrainz
|
||||
{
|
||||
public static class MusicBrainzRequestHelper
|
||||
{
|
||||
private const int MaxRetries = 6;
|
||||
private const string WebServiceUrl = "http://musicbrainz.org/ws/2/";
|
||||
private const string LookupTemplate = "{0}/{1}/?inc={2}&fmt=json&limit=100";
|
||||
private const string SearchTemplate = "{0}?query={1}&limit={2}&offset={3}&fmt=json";
|
||||
private const string ReleaseBrowseTemplate = "release?artist={0}&limit={1}&offset={2}&fmt=json";
|
||||
|
||||
internal async static Task<T> GetAsync<T>(string url, bool withoutMetadata = true)
|
||||
{
|
||||
var tryCount = 0;
|
||||
T result = default(T);
|
||||
while (tryCount < MaxRetries && result == null)
|
||||
{
|
||||
try
|
||||
{
|
||||
using (var webClient = new WebClient())
|
||||
{
|
||||
webClient.Headers.Add("user-agent", WebHelper.UserAgent);
|
||||
result = Newtonsoft.Json.JsonConvert.DeserializeObject<T>(await webClient.DownloadStringTaskAsync(new Uri(url)));
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
Thread.Sleep(100);
|
||||
}
|
||||
finally
|
||||
{
|
||||
tryCount++;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a webservice lookup template.
|
||||
/// </summary>
|
||||
internal static string CreateLookupUrl(string entity, string mbid, string inc)
|
||||
{
|
||||
return string.Format("{0}{1}", WebServiceUrl, string.Format(LookupTemplate, entity, mbid, inc));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a webservice search template.
|
||||
/// </summary>
|
||||
internal static string CreateSearchTemplate(string entity, string query, int limit, int offset)
|
||||
{
|
||||
query = Uri.EscapeUriString(query);
|
||||
|
||||
return string.Format("{0}{1}", WebServiceUrl, string.Format(SearchTemplate, entity, query, limit, offset));
|
||||
}
|
||||
|
||||
internal static string CreateArtistBrowseTemplate(string id, int limit, int offset)
|
||||
{
|
||||
return string.Format("{0}{1}", WebServiceUrl, string.Format(ReleaseBrowseTemplate, id, limit, offset));
|
||||
}
|
||||
|
||||
internal static string CreateCoverArtReleaseUrl(string musicBrainzId)
|
||||
{
|
||||
return string.Format("http://coverartarchive.org/release/{0}", musicBrainzId);
|
||||
}
|
||||
}
|
||||
}
|