This commit is contained in:
Steven Hildreth 2018-11-03 16:21:36 -05:00
parent 382156d136
commit 341267cacc
116 changed files with 11928 additions and 52 deletions

View file

@ -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>();

View file

@ -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;
}
}
}

View file

@ -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>();

View file

@ -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")

View file

@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

BIN
RoadieApi/Images/label.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

BIN
RoadieApi/Images/track.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

BIN
RoadieApi/Images/user.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

View file

@ -6,6 +6,7 @@
</PropertyGroup>
<ItemGroup>
<Folder Include="Images\" />
<Folder Include="wwwroot\" />
</ItemGroup>

View 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);
}
}
}

View file

@ -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");

View file

@ -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",

View file

@ -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)

View file

@ -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

View file

@ -1,6 +1,6 @@
using System.Collections.Generic;
namespace roadie.Library.Setttings
namespace Roadie.Library.Setttings
{
public interface IRoadieSettings
{

View file

@ -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
{

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -16,7 +16,7 @@ namespace Roadie.Library.Data
[Column("genreId")]
[Required]
public int GenreId { get; set; }
public int? GenreId { get; set; }
[Column("id")]
[Key]

View 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);
}
}
}

View file

@ -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;
}
}
}

View file

@ -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);
}
}

View 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();
}
}
}

View file

@ -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; }
}
}

View 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);
}
}
}
}

View file

@ -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; }

View file

@ -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]

View file

@ -10,7 +10,7 @@ namespace Roadie.Library.Data
[Column("genreId")]
[Required]
public int GenreId { get; set; }
public int? GenreId { get; set; }
[Column("id")]
[Key]

View 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);
}
}
}

View file

@ -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]

View 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);
}
}
}

View 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);
}
}

View 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;
}
}
}
}

View 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);
}
}
}

View 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);
}
}
}

View 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;
}
}
}

View 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;
}
}
}

View 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);
}
}
}

View 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);
}
}
}

View 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;
}
}
}

View 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"
}
}
}

File diff suppressed because it is too large Load diff

View 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);
}
}
}

View 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; }
}
}

View 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;
}
}
}

View 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
};
}
}
}

View 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
};
}
}
}

File diff suppressed because it is too large Load diff

View 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;
}
}
}

View 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);
}
}

View 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;
}
}
}

View 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;
}
}
}

View file

@ -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; }

View file

@ -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; }

View 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
}
}

View 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();
}
}
}
}

View 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();
}
}
}
}

View 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);
}
}

View 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);
}
}
}

View 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>();
}
}
}

View 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>();
}
}
}

View 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;
}
}
}

View 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;
}
}
}

View 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;
}
}
}

View file

@ -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>

View 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; }
}
}

View 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;
}
}
}

View 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);
}
}

View file

@ -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; }
}
}

View 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 }
};
}
}
}

View 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();
}
}
}

View 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;
}
}
}

View 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; }
}
}

View 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; }
}
}

View 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; }
}
}

View file

@ -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;
}
}
}

View file

@ -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; }
}
}

View file

@ -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,
}
}

View file

@ -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
}

View 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 }
};
}
}
}

View 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; }
}
}

View 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);
}
}
}

View 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);
}
}

View 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
};
}
}
}

View 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);
}
}

View 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);
}
}

View 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; }
}
}

View 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()
{
}
}
}

View 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;
}
}
}

View 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;
}
}
}
}

View file

@ -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; }
}
}

View 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; }
}
}

View file

@ -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;
}
}
}

View file

@ -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);
}
}
}

Some files were not shown because too many files have changed in this diff Show more