Generic slugify working (was it worth it? Fuck no)

This commit is contained in:
Fergal Moran
2018-05-12 03:26:34 +01:00
parent d5c9df78cb
commit 33f8dde3b9
26 changed files with 904 additions and 104 deletions

Binary file not shown.

View File

@@ -113,11 +113,13 @@ namespace PodNoms.Api.Controllers {
entry.Podcast = podcast;
entry.Processed = false;
_repository.AddOrUpdate(entry);
await _unitOfWork.CompleteAsync();
bool succeeded = await _unitOfWork.CompleteAsync();
if (succeeded) {
_processEntry(entry);
var result = _mapper.Map<PodcastEntry, PodcastEntryViewModel>(entry);
return result;
}
}
} else if (status == AudioType.Playlist && YouTubeParser.ValidateUrl(item.SourceUrl)) {
entry.ProcessingStatus = ProcessingStatus.Deferred;
return Accepted(entry);

View File

@@ -0,0 +1,466 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using PodNoms.Api.Persistence;
namespace PodNoms.Api.Migrations
{
[DbContext(typeof(PodNomsDbContext))]
[Migration("20180511192735_AddUIDToBase")]
partial class AddUIDToBase
{
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "2.1.0-rc1-32029")
.HasAnnotation("Relational:MaxIdentifierLength", 128)
.HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd();
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken();
b.Property<string>("Name")
.HasMaxLength(256);
b.Property<string>("NormalizedName")
.HasMaxLength(256);
b.HasKey("Id");
b.HasIndex("NormalizedName")
.IsUnique()
.HasName("RoleNameIndex")
.HasFilter("[NormalizedName] IS NOT NULL");
b.ToTable("AspNetRoles");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
b.Property<string>("ClaimType");
b.Property<string>("ClaimValue");
b.Property<string>("RoleId")
.IsRequired();
b.HasKey("Id");
b.HasIndex("RoleId");
b.ToTable("AspNetRoleClaims");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
b.Property<string>("ClaimType");
b.Property<string>("ClaimValue");
b.Property<string>("UserId")
.IsRequired();
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("AspNetUserClaims");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
{
b.Property<string>("LoginProvider");
b.Property<string>("ProviderKey");
b.Property<string>("ProviderDisplayName");
b.Property<string>("UserId")
.IsRequired();
b.HasKey("LoginProvider", "ProviderKey");
b.HasIndex("UserId");
b.ToTable("AspNetUserLogins");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
{
b.Property<string>("UserId");
b.Property<string>("RoleId");
b.HasKey("UserId", "RoleId");
b.HasIndex("RoleId");
b.ToTable("AspNetUserRoles");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
{
b.Property<string>("UserId");
b.Property<string>("LoginProvider");
b.Property<string>("Name");
b.Property<string>("Value");
b.HasKey("UserId", "LoginProvider", "Name");
b.ToTable("AspNetUserTokens");
});
modelBuilder.Entity("PodNoms.Api.Models.ChatMessage", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
b.Property<DateTime>("CreateDate")
.ValueGeneratedOnAddOrUpdate();
b.Property<string>("FromUserId");
b.Property<DateTime?>("MessageSeen");
b.Property<Guid>("NewId")
.ValueGeneratedOnAdd();
b.Property<string>("ToUserId");
b.Property<string>("Uid");
b.Property<DateTime>("UpdateDate")
.ValueGeneratedOnAddOrUpdate();
b.HasKey("Id");
b.HasIndex("FromUserId");
b.HasIndex("ToUserId");
b.ToTable("ChatMessages");
});
modelBuilder.Entity("PodNoms.Api.Models.ParsedPlaylistItem", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
b.Property<DateTime>("CreateDate")
.ValueGeneratedOnAddOrUpdate();
b.Property<bool>("IsProcessed");
b.Property<Guid>("NewId")
.ValueGeneratedOnAdd();
b.Property<int>("PlaylistId");
b.Property<string>("Uid");
b.Property<DateTime>("UpdateDate")
.ValueGeneratedOnAddOrUpdate();
b.Property<string>("VideoId");
b.Property<string>("VideoType");
b.HasKey("Id");
b.HasIndex("PlaylistId");
b.ToTable("ParsedPlaylistItems");
});
modelBuilder.Entity("PodNoms.Api.Models.Playlist", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
b.Property<DateTime>("CreateDate")
.ValueGeneratedOnAddOrUpdate();
b.Property<Guid>("NewId")
.ValueGeneratedOnAdd();
b.Property<int>("PodcastId");
b.Property<string>("SourceUrl");
b.Property<string>("Uid");
b.Property<DateTime>("UpdateDate")
.ValueGeneratedOnAddOrUpdate();
b.HasKey("Id");
b.HasIndex("PodcastId");
b.ToTable("Playlists");
});
modelBuilder.Entity("PodNoms.Api.Models.Podcast", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
b.Property<string>("AppUserId");
b.Property<DateTime>("CreateDate")
.ValueGeneratedOnAddOrUpdate();
b.Property<string>("Description");
b.Property<Guid>("NewId")
.ValueGeneratedOnAdd();
b.Property<string>("Slug");
b.Property<string>("TemporaryImageUrl");
b.Property<string>("Title");
b.Property<string>("Uid");
b.Property<DateTime>("UpdateDate")
.ValueGeneratedOnAddOrUpdate();
b.HasKey("Id");
b.HasIndex("AppUserId");
b.ToTable("Podcasts");
});
modelBuilder.Entity("PodNoms.Api.Models.PodcastEntry", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
b.Property<long>("AudioFileSize");
b.Property<float>("AudioLength");
b.Property<string>("AudioUrl");
b.Property<string>("Author");
b.Property<DateTime>("CreateDate")
.ValueGeneratedOnAddOrUpdate();
b.Property<string>("Description");
b.Property<string>("ImageUrl");
b.Property<Guid>("NewId")
.ValueGeneratedOnAdd();
b.Property<int?>("PlaylistId");
b.Property<int>("PodcastId");
b.Property<bool>("Processed");
b.Property<string>("ProcessingPayload");
b.Property<int>("ProcessingStatus");
b.Property<string>("SourceUrl");
b.Property<string>("Title");
b.Property<string>("Uid");
b.Property<DateTime>("UpdateDate")
.ValueGeneratedOnAddOrUpdate();
b.HasKey("Id");
b.HasIndex("PlaylistId");
b.HasIndex("PodcastId");
b.ToTable("PodcastEntries");
});
modelBuilder.Entity("PodNoms.Api.Services.Auth.ApplicationUser", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd();
b.Property<int>("AccessFailedCount");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken();
b.Property<string>("Email")
.HasMaxLength(256);
b.Property<bool>("EmailConfirmed");
b.Property<long?>("FacebookId");
b.Property<string>("FirstName");
b.Property<string>("LastName");
b.Property<bool>("LockoutEnabled");
b.Property<DateTimeOffset?>("LockoutEnd");
b.Property<string>("NormalizedEmail")
.HasMaxLength(256);
b.Property<string>("NormalizedUserName")
.HasMaxLength(256);
b.Property<string>("PasswordHash");
b.Property<string>("PhoneNumber");
b.Property<bool>("PhoneNumberConfirmed");
b.Property<string>("PictureUrl");
b.Property<string>("SecurityStamp");
b.Property<string>("Slug");
b.Property<bool>("TwoFactorEnabled");
b.Property<string>("UserName")
.HasMaxLength(256);
b.HasKey("Id");
b.HasIndex("NormalizedEmail")
.HasName("EmailIndex");
b.HasIndex("NormalizedUserName")
.IsUnique()
.HasName("UserNameIndex")
.HasFilter("[NormalizedUserName] IS NOT NULL");
b.ToTable("AspNetUsers");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole")
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
{
b.HasOne("PodNoms.Api.Services.Auth.ApplicationUser")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
{
b.HasOne("PodNoms.Api.Services.Auth.ApplicationUser")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole")
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade);
b.HasOne("PodNoms.Api.Services.Auth.ApplicationUser")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
{
b.HasOne("PodNoms.Api.Services.Auth.ApplicationUser")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("PodNoms.Api.Models.ChatMessage", b =>
{
b.HasOne("PodNoms.Api.Services.Auth.ApplicationUser", "FromUser")
.WithMany()
.HasForeignKey("FromUserId");
b.HasOne("PodNoms.Api.Services.Auth.ApplicationUser", "ToUser")
.WithMany()
.HasForeignKey("ToUserId");
});
modelBuilder.Entity("PodNoms.Api.Models.ParsedPlaylistItem", b =>
{
b.HasOne("PodNoms.Api.Models.Playlist", "Playlist")
.WithMany("ParsedPlaylistItems")
.HasForeignKey("PlaylistId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("PodNoms.Api.Models.Playlist", b =>
{
b.HasOne("PodNoms.Api.Models.Podcast", "Podcast")
.WithMany()
.HasForeignKey("PodcastId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("PodNoms.Api.Models.Podcast", b =>
{
b.HasOne("PodNoms.Api.Services.Auth.ApplicationUser", "AppUser")
.WithMany()
.HasForeignKey("AppUserId");
});
modelBuilder.Entity("PodNoms.Api.Models.PodcastEntry", b =>
{
b.HasOne("PodNoms.Api.Models.Playlist")
.WithMany("PodcastEntries")
.HasForeignKey("PlaylistId");
b.HasOne("PodNoms.Api.Models.Podcast", "Podcast")
.WithMany("PodcastEntries")
.HasForeignKey("PodcastId")
.OnDelete(DeleteBehavior.Cascade);
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -0,0 +1,164 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
namespace PodNoms.Api.Migrations {
public partial class AddUIDToBase : Migration {
protected override void Up(MigrationBuilder migrationBuilder) {
migrationBuilder.AddColumn<Guid>(
name: "NewId",
table: "Podcasts",
nullable: false,
defaultValue: new Guid("00000000-0000-0000-0000-000000000000"));
migrationBuilder.AddColumn<Guid>(
name: "NewId",
table: "PodcastEntries",
nullable: false,
defaultValue: new Guid("00000000-0000-0000-0000-000000000000"));
migrationBuilder.AddColumn<Guid>(
name: "NewId",
table: "Playlists",
nullable: false,
defaultValue: new Guid("00000000-0000-0000-0000-000000000000"));
migrationBuilder.AddColumn<string>(
name: "Uid",
table: "Playlists",
nullable: false,
defaultValue: System.Guid.NewGuid());
migrationBuilder.AddColumn<Guid>(
name: "NewId",
table: "ParsedPlaylistItems",
nullable: false,
defaultValue: new Guid("00000000-0000-0000-0000-000000000000"));
migrationBuilder.AddColumn<string>(
name: "Uid",
table: "ParsedPlaylistItems",
nullable: false,
defaultValue: System.Guid.NewGuid());
migrationBuilder.AddColumn<Guid>(
name: "NewId",
table: "ChatMessages",
nullable: false,
defaultValue: new Guid("00000000-0000-0000-0000-000000000000"));
migrationBuilder.AddColumn<string>(
name: "Uid",
table: "ChatMessages",
nullable: false,
defaultValue: System.Guid.NewGuid());
migrationBuilder.Sql("UPDATE Podcasts SET NewId = Uid WHERE NewId = '00000000-0000-0000-0000-000000000000' AND Uid IS NOT NULL");
migrationBuilder.Sql("UPDATE PodcastEntries SET NewId = Uid WHERE NewId = '00000000-0000-0000-0000-000000000000' AND Uid IS NOT NULL");
migrationBuilder.AlterColumn<DateTime>(
name: "CreateDate",
table: "Podcasts",
type: "datetime2",
nullable: false,
defaultValueSql: "getdate()",
oldClrType: typeof(DateTime));
migrationBuilder.AlterColumn<DateTime>(
name: "UpdateDate",
table: "Podcasts",
type: "datetime2",
nullable: false,
defaultValueSql: "getdate()",
oldClrType: typeof(DateTime));
migrationBuilder.AlterColumn<DateTime>(
name: "CreateDate",
table: "PodcastEntries",
type: "datetime2",
nullable: false,
defaultValueSql: "getdate()",
oldClrType: typeof(DateTime));
migrationBuilder.AlterColumn<DateTime>(
name: "UpdateDate",
table: "PodcastEntries",
type: "datetime2",
nullable: false,
defaultValueSql: "getdate()",
oldClrType: typeof(DateTime));
migrationBuilder.AlterColumn<DateTime>(
name: "CreateDate",
table: "Playlists",
type: "datetime2",
nullable: false,
defaultValueSql: "getdate()",
oldClrType: typeof(DateTime));
migrationBuilder.AlterColumn<DateTime>(
name: "UpdateDate",
table: "Playlists",
type: "datetime2",
nullable: false,
defaultValueSql: "getdate()",
oldClrType: typeof(DateTime));
migrationBuilder.AlterColumn<DateTime>(
name: "CreateDate",
table: "ParsedPlaylistItems",
type: "datetime2",
nullable: false,
defaultValueSql: "getdate()",
oldClrType: typeof(DateTime));
migrationBuilder.AlterColumn<DateTime>(
name: "UpdateDate",
table: "ParsedPlaylistItems",
type: "datetime2",
nullable: false,
defaultValueSql: "getdate()",
oldClrType: typeof(DateTime));
migrationBuilder.AlterColumn<DateTime>(
name: "CreateDate",
table: "ChatMessages",
type: "datetime2",
nullable: false,
defaultValueSql: "getdate()",
oldClrType: typeof(DateTime));
migrationBuilder.AlterColumn<DateTime>(
name: "UpdateDate",
table: "ChatMessages",
type: "datetime2",
nullable: false,
defaultValueSql: "getdate()",
oldClrType: typeof(DateTime));
}
protected override void Down(MigrationBuilder migrationBuilder) {
migrationBuilder.DropColumn(
name: "NewId",
table: "Podcasts");
migrationBuilder.DropColumn(
name: "NewId",
table: "PodcastEntries");
migrationBuilder.DropColumn(
name: "NewId",
table: "Playlists");
migrationBuilder.DropColumn(
name: "Uid",
table: "Playlists");
migrationBuilder.DropColumn(
name: "NewId",
table: "ParsedPlaylistItems");
migrationBuilder.DropColumn(
name: "Uid",
table: "ParsedPlaylistItems");
migrationBuilder.DropColumn(
name: "NewId",
table: "ChatMessages");
migrationBuilder.DropColumn(
name: "Uid",
table: "ChatMessages");
}
}
}

View File

@@ -135,15 +135,22 @@ namespace PodNoms.Api.Migrations
.ValueGeneratedOnAdd()
.HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
b.Property<DateTime>("CreateDate");
b.Property<DateTime>("CreateDate")
.ValueGeneratedOnAddOrUpdate();
b.Property<string>("FromUserId");
b.Property<DateTime?>("MessageSeen");
b.Property<Guid>("NewId")
.ValueGeneratedOnAdd();
b.Property<string>("ToUserId");
b.Property<DateTime>("UpdateDate");
b.Property<string>("Uid");
b.Property<DateTime>("UpdateDate")
.ValueGeneratedOnAddOrUpdate();
b.HasKey("Id");
@@ -160,13 +167,20 @@ namespace PodNoms.Api.Migrations
.ValueGeneratedOnAdd()
.HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
b.Property<DateTime>("CreateDate");
b.Property<DateTime>("CreateDate")
.ValueGeneratedOnAddOrUpdate();
b.Property<bool>("IsProcessed");
b.Property<Guid>("NewId")
.ValueGeneratedOnAdd();
b.Property<int>("PlaylistId");
b.Property<DateTime>("UpdateDate");
b.Property<string>("Uid");
b.Property<DateTime>("UpdateDate")
.ValueGeneratedOnAddOrUpdate();
b.Property<string>("VideoId");
@@ -185,13 +199,20 @@ namespace PodNoms.Api.Migrations
.ValueGeneratedOnAdd()
.HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
b.Property<DateTime>("CreateDate");
b.Property<DateTime>("CreateDate")
.ValueGeneratedOnAddOrUpdate();
b.Property<Guid>("NewId")
.ValueGeneratedOnAdd();
b.Property<int>("PodcastId");
b.Property<string>("SourceUrl");
b.Property<DateTime>("UpdateDate");
b.Property<string>("Uid");
b.Property<DateTime>("UpdateDate")
.ValueGeneratedOnAddOrUpdate();
b.HasKey("Id");
@@ -208,10 +229,14 @@ namespace PodNoms.Api.Migrations
b.Property<string>("AppUserId");
b.Property<DateTime>("CreateDate");
b.Property<DateTime>("CreateDate")
.ValueGeneratedOnAddOrUpdate();
b.Property<string>("Description");
b.Property<Guid>("NewId")
.ValueGeneratedOnAdd();
b.Property<string>("Slug");
b.Property<string>("TemporaryImageUrl");
@@ -220,7 +245,8 @@ namespace PodNoms.Api.Migrations
b.Property<string>("Uid");
b.Property<DateTime>("UpdateDate");
b.Property<DateTime>("UpdateDate")
.ValueGeneratedOnAddOrUpdate();
b.HasKey("Id");
@@ -243,12 +269,16 @@ namespace PodNoms.Api.Migrations
b.Property<string>("Author");
b.Property<DateTime>("CreateDate");
b.Property<DateTime>("CreateDate")
.ValueGeneratedOnAddOrUpdate();
b.Property<string>("Description");
b.Property<string>("ImageUrl");
b.Property<Guid>("NewId")
.ValueGeneratedOnAdd();
b.Property<int?>("PlaylistId");
b.Property<int>("PodcastId");
@@ -265,7 +295,8 @@ namespace PodNoms.Api.Migrations
b.Property<string>("Uid");
b.Property<DateTime>("UpdateDate");
b.Property<DateTime>("UpdateDate")
.ValueGeneratedOnAddOrUpdate();
b.HasKey("Id");

View File

@@ -0,0 +1,13 @@
using System;
namespace PodNoms.Api.Models.Annotations {
[System.AttributeUsage(System.AttributeTargets.Property)]
public class SlugFieldAttribute : Attribute {
private readonly string _sourceField;
public string SourceField => _sourceField;
public SlugFieldAttribute(string sourceField) {
this._sourceField = sourceField;
}
}
}

View File

@@ -2,26 +2,22 @@ using System;
using System.ComponentModel.DataAnnotations.Schema;
namespace PodNoms.Api.Models {
public class BaseModel : IEntity {
public class BaseEntity : IEntity {
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int Id { get; set; }
//TODO: Remove once migration is complete
public string Uid { get; set; }
// TODO replace Id with below
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public Guid NewId { get; set; }
[DatabaseGenerated(DatabaseGeneratedOption.Computed)]
public DateTime CreateDate { get; set; }
public DateTime CreateDate { get; set; } = DateTime.UtcNow;
[DatabaseGenerated(DatabaseGeneratedOption.Computed)]
public DateTime UpdateDate { get; set; }
public DateTime UpdateDate { get; set; } = DateTime.UtcNow;
public string ExposedUid { get => NewId.ToString(); }
}
public interface IEntity {
int Id { get; set; }
DateTime CreateDate { get; set; }
DateTime UpdateDate { get; set; }
}
}

View File

@@ -5,7 +5,7 @@ using Microsoft.Extensions.Options;
using PodNoms.Api.Services.Auth;
namespace PodNoms.Api.Models {
public class ChatMessage : BaseModel, IEntity {
public class ChatMessage : BaseEntity, IEntity {
public ApplicationUser FromUser { get; set; }
public ApplicationUser ToUser { get; set; }

9
server/Models/IEntity.cs Normal file
View File

@@ -0,0 +1,9 @@
using System;
namespace PodNoms.Api.Models {
public interface IEntity {
int Id { get; set; }
DateTime CreateDate { get; set; }
DateTime UpdateDate { get; set; }
}
}

View File

@@ -0,0 +1,67 @@
using System;
using System.Collections.Generic;
using System.Data;
using System.Linq;
using System.Reflection;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.ChangeTracking;
using Microsoft.Extensions.Logging;
using PodNoms.Api.Models.Annotations;
using PodNoms.Api.Utils.Extensions;
namespace PodNoms.Api.Models {
public interface ISluggedEntity {
string Slug { get; set; }
}
public static class SluggedEntityExtensions {
private class ProxySluggedModel : ISluggedEntity {
public string Slug { get; set; }
}
public static IEnumerable<T> Select<T>(this IDataReader reader,
Func<IDataReader, T> projection) {
while (reader.Read()) {
yield return projection(reader);
}
}
public static IEnumerable<T> ExecSQL<T>(this DbContext context, string query) where T : class, ISluggedEntity, new() {
using (var command = context.Database.GetDbConnection().CreateCommand()) {
command.CommandText = query;
command.CommandType = CommandType.Text;
context.Database.OpenConnection();
using (var reader = command.ExecuteReader()){
var result = reader.Select<T>(r => new T {
Slug = r["Slug"] is DBNull ? string.Empty : r["Slug"].ToString()
});
return result.ToList();
}
}
}
public static string GetSlug(this ISluggedEntity entity, DbContext context, ILogger logger = null) {
try {
var property = entity.GetType().GetProperties()
.Where(prop => Attribute.IsDefined(prop, typeof(SlugFieldAttribute)))
.FirstOrDefault();
if (property != null) {
var attribute = property.GetCustomAttributes(typeof(SlugFieldAttribute), false)
.FirstOrDefault();
Type t = entity.GetType();
var tableName = context.Model.FindEntityType(t).SqlServer().TableName;
if (!string.IsNullOrEmpty(tableName)) {
var sourceField = (attribute as SlugFieldAttribute).SourceField;
var slugSource = entity.GetType().GetProperty("Title").GetValue(entity, null).ToString();
var source = context.ExecSQL<ProxySluggedModel>($"SELECT Slug FROM {tableName}")
.Select(m => m.Slug);
return slugSource.Slugify(source);
}
}
} catch (Exception ex) {
logger?.LogError($"Error slugifying {entity.GetType().Name} - {ex.Message}");
}
return string.Empty;
}
}
}

View File

@@ -1,7 +1,7 @@
using System.Collections.Generic;
namespace PodNoms.Api.Models {
public class Playlist : BaseModel {
public class Playlist : BaseEntity {
//TODO: Update this to use concrete model
public int PodcastId { get; set; }
public string SourceUrl { get; set; }
@@ -12,7 +12,7 @@ namespace PodNoms.Api.Models {
ParsedPlaylistItems = new List<ParsedPlaylistItem>();
}
}
public class ParsedPlaylistItem : BaseModel {
public class ParsedPlaylistItem : BaseEntity {
public string VideoId { get; set; }
public string VideoType { get; set; }
public bool IsProcessed { get; set; }

View File

@@ -1,13 +1,16 @@
using System.Collections.Generic;
using System.Collections.ObjectModel;
using Microsoft.Extensions.Options;
using PodNoms.Api.Models.Annotations;
using PodNoms.Api.Services.Auth;
namespace PodNoms.Api.Models {
public class Podcast : BaseModel {
public class Podcast : BaseEntity, ISluggedEntity {
public ApplicationUser AppUser { get; set; }
public string Title { get; set; }
public string Description { get; set; }
[SlugField(sourceField: "Title")]
public string Slug { get; set; }
public string TemporaryImageUrl { get; set; }
public List<PodcastEntry> PodcastEntries { get; set; }

View File

@@ -10,7 +10,7 @@ namespace PodNoms.Api.Models {
Failed, //5
Deferred //6
}
public class PodcastEntry : BaseModel {
public class PodcastEntry : BaseEntity {
public string Author { get; set; }
public string Title { get; set; }

View File

@@ -5,6 +5,6 @@ namespace PodNoms.Api.Persistence
{
public interface IUnitOfWork
{
Task CompleteAsync();
Task<bool> CompleteAsync();
}
}

View File

@@ -1,51 +1,64 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.ChangeTracking;
using Microsoft.EntityFrameworkCore.Design;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using PodNoms.Api.Models;
using PodNoms.Api.Models.Annotations;
using PodNoms.Api.Services.Auth;
using PodNoms.Api.Utils.Extensions;
namespace PodNoms.Api.Persistence {
public class PodNomsDbContext : IdentityDbContext<ApplicationUser> {
public PodNomsDbContext(DbContextOptions<PodNomsDbContext> options) : base(options) { }
private readonly ILogger<PodNomsDbContext> _logger;
public PodNomsDbContext(DbContextOptions<PodNomsDbContext> options, ILogger<PodNomsDbContext> logger) : base(options) {
Database.SetCommandTimeout(360);
this._logger = logger;
}
protected override void OnModelCreating(ModelBuilder modelBuilder) {
base.OnModelCreating(modelBuilder);
foreach (var pb in __getColumn(modelBuilder, "CreateDate")) {
pb.ValueGeneratedOnAdd()
.HasDefaultValueSql("getdate()");
}
foreach (var pb in __getColumn(modelBuilder, "UpdateDate")) {
pb.ValueGeneratedOnAddOrUpdate()
.HasDefaultValueSql("getdate()");
}
foreach (var pb in __getColumn(modelBuilder, "NewId")) {
pb.ValueGeneratedOnAdd()
.HasDefaultValueSql("newsequentialid()");
}
}
private IEnumerable<PropertyBuilder> __getColumn(ModelBuilder modelBuilder, string columnName) {
return modelBuilder.Model
.GetEntityTypes()
.SelectMany(t => t.GetProperties())
.Where(p => p.Name == columnName)
.Select(p => modelBuilder.Entity(p.DeclaringEntityType.ClrType).Property(p.Name));
}
public override int SaveChanges() {
_addTimestamps();
return base.SaveChanges();
}
public override Task<int> SaveChangesAsync(bool acceptAllChangesOnSuccess, CancellationToken cancellationToken = default(System.Threading.CancellationToken)) {
_addTimestamps();
return base.SaveChangesAsync(acceptAllChangesOnSuccess, cancellationToken);
}
public override Task<int> SaveChangesAsync(CancellationToken cancellationToken = default) {
_addTimestamps();
return base.SaveChangesAsync(cancellationToken);
}
void _addTimestamps() {
var modifiedEntries = ChangeTracker.Entries()
.Where(x => (x.State == EntityState.Added || x.State == EntityState.Modified));
var now = DateTime.Now;
foreach (var entry in modifiedEntries) {
var entity = entry.Entity as BaseModel;
if (entity != null) {
if (entry.State == EntityState.Added) {
entity.CreateDate = now;
}
entity.UpdateDate = now;
}
}
}
public DbSet<Podcast> Podcasts { get; set; }
public DbSet<PodcastEntry> PodcastEntries { get; set; }
public DbSet<Playlist> Playlists { get; set; }

View File

@@ -2,6 +2,7 @@ using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using PodNoms.Api.Models;
namespace PodNoms.Api.Persistence {
@@ -12,7 +13,7 @@ namespace PodNoms.Api.Persistence {
Task<IEnumerable<ChatMessage>> GetAllChats(string userId);
}
public class ChatRepository : GenericRepository<ChatMessage>, IChatRepository {
public ChatRepository(PodNomsDbContext context) : base(context) {
public ChatRepository(PodNomsDbContext context, ILogger<ChatRepository> logger) : base(context, logger) {
}

View File

@@ -2,6 +2,7 @@ using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using PodNoms.Api.Models;
@@ -12,7 +13,7 @@ namespace PodNoms.Api.Persistence {
Task<PodcastEntry> GetByUidAsync(string uid);
}
public class EntryRepository : GenericRepository<PodcastEntry>, IEntryRepository {
public EntryRepository(PodNomsDbContext context) : base(context) {
public EntryRepository(PodNomsDbContext context, ILogger<EntryRepository> logger) : base(context, logger) {
}
public async Task<IEnumerable<PodcastEntry>> GetAllForSlugAsync(string podcastSlug) {
var entries = await GetAll()

View File

@@ -4,7 +4,9 @@ using System.Linq;
using System.Linq.Expressions;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using PodNoms.Api.Models;
using PodNoms.Api.Models.Annotations;
namespace PodNoms.Api.Persistence {
public interface IRepository<TEntity> where TEntity : class, IEntity {
@@ -18,9 +20,11 @@ namespace PodNoms.Api.Persistence {
public abstract class GenericRepository<TEntity> : IRepository<TEntity> where TEntity : class, IEntity {
private PodNomsDbContext _context;
private readonly ILogger<GenericRepository<TEntity>> _logger;
public GenericRepository(PodNomsDbContext context) {
public GenericRepository(PodNomsDbContext context, ILogger<GenericRepository<TEntity>> logger) {
_context = context;
_logger = logger;
}
public PodNomsDbContext GetContext() {
@@ -36,13 +40,16 @@ namespace PodNoms.Api.Persistence {
}
public TEntity Create(TEntity entity) {
if (entity is ISluggedEntity) {
(entity as ISluggedEntity).Slug = (entity as ISluggedEntity).GetSlug(_context, _logger);
}
var ret = _context.Set<TEntity>().Add(entity);
return ret as TEntity;
return entity;
}
public TEntity Update(TEntity entity) {
var ret = _context.Set<TEntity>().Update(entity);
return ret as TEntity;
return entity;
}
public TEntity AddOrUpdate(TEntity entity) {

View File

@@ -2,6 +2,7 @@ using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using PodNoms.Api.Models;
namespace PodNoms.Api.Persistence {
@@ -10,7 +11,7 @@ namespace PodNoms.Api.Persistence {
Task<List<ParsedPlaylistItem>> GetUnprocessedItems();
}
public class PlaylistRepository : GenericRepository<Playlist>, IPlaylistRepository {
public PlaylistRepository(PodNomsDbContext context) : base(context) {
public PlaylistRepository(PodNomsDbContext context, ILogger<PlaylistRepository> logger) : base(context, logger) {
}
public async Task<ParsedPlaylistItem> GetParsedItem(string itemId, int playlistId) {

View File

@@ -2,6 +2,7 @@ using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using PodNoms.Api.Models;
namespace PodNoms.Api.Persistence {
@@ -10,7 +11,7 @@ namespace PodNoms.Api.Persistence {
Task<IEnumerable<Podcast>> GetAllForUserAsync(string userId);
}
public class PodcastRepository : GenericRepository<Podcast>, IPodcastRepository {
public PodcastRepository(PodNomsDbContext context) : base(context) {
public PodcastRepository(PodNomsDbContext context, ILogger<PodcastRepository> logger) : base(context, logger) {
}
public async Task<Podcast> GetAsync(string id, string slug) {
var ret = await GetAll()

View File

@@ -11,13 +11,15 @@ namespace PodNoms.Api.Persistence {
this._logger = logger;
this._context = context;
}
public async Task CompleteAsync() {
public async Task<bool> CompleteAsync() {
try {
await Task.FromResult<object>(null);
await _context.SaveChangesAsync();
return true;
} catch (DbUpdateException dbe) {
this._logger.LogError($"Error completing unit of work: {dbe.Message}\n{dbe.InnerException.Message}");
}
return false;
}
}
}

View File

@@ -7,7 +7,9 @@ using System.Threading.Tasks;
using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using PodNoms.Api.Providers;
namespace PodNoms.Api {
public class Program {
@@ -15,21 +17,17 @@ namespace PodNoms.Api {
Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") == EnvironmentName.Development;
public static void Main(string[] args) {
CreateWebHostBuilder(args).Build().Run();
var host = BuildWebHost(args).Build();
host.MigrateDatabase(isDevelopment);
host.Run();
}
public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
public static IWebHostBuilder BuildWebHost(string[] args) =>
WebHost.CreateDefaultBuilder(args)
.UseStartup<Startup>()
.UseUrls("http://0.0.0.0:5000")
.UseKestrel(options => {
options.Limits.MaxRequestBodySize = 1073741824; //1Gb
// if (isDevelopment)
// {
// options.Listen(IPAddress.Any, 5001, listenOptions =>
// {
// listenOptions.UseHttps("certs/dev2.podnoms.com.pfx", "secret");
// });
// }
options.Limits.MaxRequestBodySize = 1073741824;
});
}
}

View File

@@ -0,0 +1,20 @@
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using PodNoms.Api.Persistence;
namespace PodNoms.Api.Providers {
public static class WebHostExtensions {
public static IWebHost MigrateDatabase(this IWebHost webHost, bool isDevelopment) {
if (isDevelopment) {
var serviceScopeFactory = (IServiceScopeFactory)webHost.Services.GetService(typeof(IServiceScopeFactory));
using (var scope = serviceScopeFactory.CreateScope()) {
var services = scope.ServiceProvider;
var dbContext = services.GetRequiredService<PodNomsDbContext>();
// dbContext.Database.EnsureDeleted();
dbContext.Database.EnsureCreated();
}
}
return webHost;
}
}
}

View File

@@ -1,11 +1,11 @@
using System;
using System;
using Hangfire;
namespace PodNoms.Api.Services.Processor {
public class ServiceProviderActivator : JobActivator {
public class HangfireActivator : JobActivator {
private readonly IServiceProvider _serviceProvider;
public ServiceProviderActivator(IServiceProvider serviceProvider) {
public HangfireActivator(IServiceProvider serviceProvider) {
_serviceProvider = serviceProvider;
}

View File

@@ -49,11 +49,13 @@ using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.AspNetCore.Identity.UI.Services;
using PodNoms.Api.Utils.RemoteParsers;
using PodNoms.Api.Services.Slack;
using System.Threading;
namespace PodNoms.Api {
public class Startup {
private const string SecretKey = "QGfaEMNASkNMGLKA3LjgPdkPfFEy3n40"; // todo: get this from somewhere secure
private readonly SymmetricSecurityKey _signingKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(SecretKey));
private static Mutex mutex = new Mutex();
public IConfiguration Configuration { get; }
public Startup(IConfiguration configuration) {
@@ -80,7 +82,7 @@ namespace PodNoms.Api {
}
public void ConfigureDevelopmentServices(IServiceCollection services) {
services.AddDbContext<PodNomsDbContext>(options => {
options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection"));
options.UseSqlServer(Configuration.GetConnectionString("PlaygroundConnection"));
options.EnableSensitiveDataLogging(true);
});
@@ -117,9 +119,12 @@ namespace PodNoms.Api {
options.MultipartBodyLengthLimit = long.MaxValue;
});
mutex.WaitOne();
Mapper.Reset();
services.AddAutoMapper(e => {
e.AddProfile(new MappingProvider(Configuration));
});
mutex.ReleaseMutex();
services.AddHttpClient("mixcloud", c => {
c.BaseAddress = new Uri("https://api.mixcloud.com/");
@@ -259,9 +264,20 @@ namespace PodNoms.Api {
Encoding.RegisterProvider(instance);
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env,
ILoggerFactory loggerFactory, IServiceProvider serviceProvider) {
ILoggerFactory loggerFactory, IServiceProvider serviceProvider, IApplicationLifetime lifetime) {
lifetime.ApplicationStarted.Register(() => {
if (env.IsDevelopment()) {
var p = new System.Diagnostics.Process();
p.StartInfo.UseShellExecute = false;
p.StartInfo.RedirectStandardOutput = true;
p.StartInfo.RedirectStandardError = true;
p.StartInfo.FileName = "/usr/bin/play";
p.StartInfo.Arguments = "/home/fergalm/dev/podnoms/server/.working/tada.mp3";
p.Start();
}
});
if (env.IsDevelopment()) {
app.UseDeveloperExceptionPage();
@@ -273,13 +289,12 @@ namespace PodNoms.Api {
// app.UseHttpsRedirection();
app.UseStaticFiles();
GlobalConfiguration.Configuration.UseActivator(new ServiceProviderActivator(serviceProvider));
if ((env.IsProduction() || true)) {
app.UseHangfireServer();
app.UseHangfireDashboard("/hangfire", new DashboardOptions {
Authorization = new[] { new HangFireAuthorizationFilter() }
});
GlobalConfiguration.Configuration.UseActivator(new HangfireActivator(serviceProvider));
}
app.UseForwardedHeaders(new ForwardedHeadersOptions {

View File

@@ -3,12 +3,9 @@ using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
namespace PodNoms.Api.Utils.Extensions
{
public static class StringExtensions
{
public static string StripNonXMLChars(this string str, float xmlVersion = 1.1f)
{
namespace PodNoms.Api.Utils.Extensions {
public static class StringExtensions {
public static string StripNonXMLChars(this string str, float xmlVersion = 1.1f) {
if (string.IsNullOrEmpty(str))
return string.Empty;
const string patternVersion1_0 = @"&#x((10?|[2-F])FFF[EF]|FDD[0-9A-F]|7F|8[0-46-9A-F]9[0-9A-F]);";
@@ -26,19 +23,16 @@ namespace PodNoms.Api.Utils.Extensions
return result;
}
public static string RemoveNonAlphaChars(this string str)
{
public static string RemoveNonAlphaChars(this string str) {
Regex rgx = new Regex("[^a-zA-Z0-9 -]");
return rgx.Replace(str, "");
}
public static string RemoveInvalidUrlChars(this string str)
{
public static string RemoveInvalidUrlChars(this string str) {
string regexSearch = new string(Path.GetInvalidFileNameChars()) + new string(Path.GetInvalidPathChars());
Regex r = new Regex(string.Format("[{0}]", Regex.Escape(regexSearch)));
return r.Replace(str, "");
}
public static string Slugify(this string phrase, IEnumerable<string> source)
{
public static string Slugify(this string phrase, IEnumerable<string> source) {
string str = phrase.RemoveAccent().ToLower().RemoveInvalidUrlChars().RemoveNonAlphaChars();
// invalid chars
str = Regex.Replace(str, @"[^a-z0-9\s-]", "");
@@ -51,26 +45,22 @@ namespace PodNoms.Api.Utils.Extensions
str = str.Replace(" ", "");
var count = 1;
while (source != null &&
!string.IsNullOrEmpty(source.Where(e => e == str).Select(e => e).DefaultIfEmpty("").FirstOrDefault()))
{
str = $"{str}_{count++}";
var origStr = str;
while (source != null && source.Count() != 0 &&
!string.IsNullOrEmpty(source.Where(e => e == str).Select(e => e).DefaultIfEmpty("").FirstOrDefault())) {
str = $"{origStr}-{count++}";
}
return str;
}
public static string RemoveAccent(this string txt)
{
public static string RemoveAccent(this string txt) {
byte[] bytes = System.Text.Encoding.GetEncoding("Cyrillic").GetBytes(txt);
return System.Text.Encoding.ASCII.GetString(bytes);
}
public static string UrlParse(this string url, params string[] parts)
{
public static string UrlParse(this string url, params string[] parts) {
url = url.TrimEnd('/');
foreach (var u in parts)
{
foreach (var u in parts) {
url = string.Format("{0}/{1}", url, u.TrimStart('/'));
}
return url;