YouTube working, Mixcloud is a cunt

This commit is contained in:
Fergal Moran
2018-05-07 20:54:39 +01:00
parent 83a9df7048
commit ba2b10ee78
33 changed files with 1856 additions and 170 deletions

View File

@@ -23,6 +23,7 @@
<div class="block-content">
<button class="btn btn-primary" (click)="processOrphans()">Process Orphans</button>
<button class="btn btn-primary" (click)="processPlaylists()">Process Playlists</button>
<button class="btn btn-primary" (click)="processPlaylistItems()">Process Playlist Items</button>
<button class="btn btn-primary" (click)="updateYouTubeDl()">Update Youtube Downloader</button>
</div>
</div>

View File

@@ -54,6 +54,13 @@ export class DebugComponent implements OnInit {
console.log('debug.component.ts', 'processPlaylists', e)
);
}
processPlaylistItems() {
this._jobsService
.processPlaylistItems()
.subscribe((e) =>
console.log('debug.component.ts', 'processPlaylists', e)
);
}
updateYouTubeDl() {
this._jobsService
.updateYouTubeDl()

View File

@@ -18,6 +18,11 @@ export class JobsService {
environment.API_HOST + '/job/processplaylists'
);
}
processPlaylistItems(): Observable<Response> {
return this._http.get<Response>(
environment.API_HOST + '/job/processplaylistitems'
);
}
updateYouTubeDl(): Observable<Response> {
return this._http.get<Response>(
environment.API_HOST + '/job/updateyoutubedl'

View File

@@ -5,6 +5,7 @@ using System.Linq;
using System.Threading.Tasks;
using AutoMapper;
using Hangfire;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
@@ -32,6 +33,7 @@ namespace PodNoms.Api.Controllers {
private readonly IUnitOfWork _unitOfWork;
private readonly IMapper _mapper;
private readonly IUrlProcessService _processor;
private readonly IHostingEnvironment _hostingEnvironment;
private readonly ILogger _logger;
private readonly AudioFileStorageSettings _audioFileStorageSettings;
private readonly StorageSettings _storageSettings;
@@ -43,6 +45,7 @@ namespace PodNoms.Api.Controllers {
IConfiguration options,
IUrlProcessService processor, ILoggerFactory logger,
UserManager<ApplicationUser> userManager,
IHostingEnvironment hostingEnvironment,
IHttpContextAccessor contextAccessor) : base(contextAccessor, userManager) {
this._logger = logger.CreateLogger<EntryController>();
this._podcastRepository = podcastRepository;
@@ -53,6 +56,7 @@ namespace PodNoms.Api.Controllers {
this._audioFileStorageSettings = audioFileStorageSettings.Value;
this._mapper = mapper;
this._processor = processor;
this._hostingEnvironment = hostingEnvironment;
}
private void _processEntry(PodcastEntry entry) {
@@ -111,7 +115,7 @@ namespace PodNoms.Api.Controllers {
var result = _mapper.Map<PodcastEntry, PodcastEntryViewModel>(entry);
return result;
}
} else if (status == AudioType.Playlist) {
} else if (status == AudioType.Playlist && _hostingEnvironment.IsDevelopment()) {
entry.ProcessingStatus = ProcessingStatus.Deferred;
return Accepted(entry);
}

View File

@@ -19,6 +19,11 @@ namespace PodNoms.Api.Controllers {
var infoJobId = BackgroundJob.Enqueue<ProcessPlaylistsJob>(service => service.Execute());
return Ok();
}
[HttpGet("processplaylistitems")]
public IActionResult ProcessPlaylistItems() {
var infoJobId = BackgroundJob.Enqueue<ProcessPlaylistItemJob>(service => service.Execute());
return Ok();
}
[HttpGet("updateyoutubedl")]
public IActionResult UpdateYouTubeDl() {
var infoJobId = BackgroundJob.Enqueue<UpdateYouTubeDlJob>(service => service.Execute());

View File

@@ -0,0 +1,390 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Metadata.Internal;
using Microsoft.EntityFrameworkCore.Migrations;
using PodNoms.Api.Persistence;
namespace PodNoms.Api.Migrations
{
[DbContext(typeof(PodnomsDbContext))]
[Migration("20180507153433_ParsedPlaylistVideos")]
partial class ParsedPlaylistVideos
{
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "2.1.0-preview2-30571")
.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();
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();
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.ParsedPlaylistVideo", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<DateTime>("CreateDate");
b.Property<bool>("IsProcessed");
b.Property<int>("PlaylistId");
b.Property<DateTime>("UpdateDate");
b.Property<string>("VideoId");
b.HasKey("Id");
b.HasIndex("PlaylistId");
b.ToTable("ParsedPlaylistVideos");
});
modelBuilder.Entity("PodNoms.Api.Models.Playlist", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<DateTime>("CreateDate");
b.Property<int>("PodcastId");
b.Property<string>("SourceUrl");
b.Property<DateTime>("UpdateDate");
b.HasKey("Id");
b.HasIndex("PodcastId");
b.ToTable("Playlists");
});
modelBuilder.Entity("PodNoms.Api.Models.Podcast", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<string>("AppUserId");
b.Property<DateTime>("CreateDate");
b.Property<string>("Description");
b.Property<string>("Slug");
b.Property<string>("TemporaryImageUrl");
b.Property<string>("Title");
b.Property<string>("Uid");
b.Property<DateTime>("UpdateDate");
b.HasKey("Id");
b.HasIndex("AppUserId");
b.ToTable("Podcasts");
});
modelBuilder.Entity("PodNoms.Api.Models.PodcastEntry", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<long>("AudioFileSize");
b.Property<float>("AudioLength");
b.Property<string>("AudioUrl");
b.Property<string>("Author");
b.Property<DateTime>("CreateDate");
b.Property<string>("Description");
b.Property<string>("ImageUrl");
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");
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.ParsedPlaylistVideo", b =>
{
b.HasOne("PodNoms.Api.Models.Playlist", "Playlist")
.WithMany("ParsedPlaylistVideos")
.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,75 @@
using System;
using System.Collections.Generic;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;
namespace PodNoms.Api.Migrations
{
public partial class ParsedPlaylistVideos : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AlterColumn<DateTime>(
name: "CreateDate",
table: "Podcasts",
nullable: false,
oldClrType: typeof(DateTime),
oldDefaultValueSql: "getdate()");
migrationBuilder.AlterColumn<DateTime>(
name: "CreateDate",
table: "PodcastEntries",
nullable: false,
oldClrType: typeof(DateTime),
oldDefaultValueSql: "getdate()");
migrationBuilder.CreateTable(
name: "ParsedPlaylistVideos",
columns: table => new
{
CreateDate = table.Column<DateTime>(nullable: false),
UpdateDate = table.Column<DateTime>(nullable: false),
Id = table.Column<int>(nullable: false)
.Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn),
VideoId = table.Column<string>(nullable: true),
IsProcessed = table.Column<bool>(nullable: false),
PlaylistId = table.Column<int>(nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_ParsedPlaylistVideos", x => x.Id);
table.ForeignKey(
name: "FK_ParsedPlaylistVideos_Playlists_PlaylistId",
column: x => x.PlaylistId,
principalTable: "Playlists",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_ParsedPlaylistVideos_PlaylistId",
table: "ParsedPlaylistVideos",
column: "PlaylistId");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "ParsedPlaylistVideos");
migrationBuilder.AlterColumn<DateTime>(
name: "CreateDate",
table: "Podcasts",
nullable: false,
defaultValueSql: "getdate()",
oldClrType: typeof(DateTime));
migrationBuilder.AlterColumn<DateTime>(
name: "CreateDate",
table: "PodcastEntries",
nullable: false,
defaultValueSql: "getdate()",
oldClrType: typeof(DateTime));
}
}
}

View File

@@ -0,0 +1,390 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Metadata.Internal;
using Microsoft.EntityFrameworkCore.Migrations;
using PodNoms.Api.Persistence;
namespace PodNoms.Api.Migrations
{
[DbContext(typeof(PodnomsDbContext))]
[Migration("20180507155436_Rename_ParsedPlaylistVideos")]
partial class Rename_ParsedPlaylistVideos
{
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "2.1.0-preview2-30571")
.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();
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();
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.ParsedPlaylistItem", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<DateTime>("CreateDate");
b.Property<bool>("IsProcessed");
b.Property<int>("PlaylistId");
b.Property<DateTime>("UpdateDate");
b.Property<string>("VideoId");
b.HasKey("Id");
b.HasIndex("PlaylistId");
b.ToTable("ParsedPlaylistItems");
});
modelBuilder.Entity("PodNoms.Api.Models.Playlist", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<DateTime>("CreateDate");
b.Property<int>("PodcastId");
b.Property<string>("SourceUrl");
b.Property<DateTime>("UpdateDate");
b.HasKey("Id");
b.HasIndex("PodcastId");
b.ToTable("Playlists");
});
modelBuilder.Entity("PodNoms.Api.Models.Podcast", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<string>("AppUserId");
b.Property<DateTime>("CreateDate");
b.Property<string>("Description");
b.Property<string>("Slug");
b.Property<string>("TemporaryImageUrl");
b.Property<string>("Title");
b.Property<string>("Uid");
b.Property<DateTime>("UpdateDate");
b.HasKey("Id");
b.HasIndex("AppUserId");
b.ToTable("Podcasts");
});
modelBuilder.Entity("PodNoms.Api.Models.PodcastEntry", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<long>("AudioFileSize");
b.Property<float>("AudioLength");
b.Property<string>("AudioUrl");
b.Property<string>("Author");
b.Property<DateTime>("CreateDate");
b.Property<string>("Description");
b.Property<string>("ImageUrl");
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");
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.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,78 @@
using System;
using System.Collections.Generic;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;
namespace PodNoms.Api.Migrations
{
public partial class Rename_ParsedPlaylistVideos : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "ParsedPlaylistVideos");
migrationBuilder.CreateTable(
name: "ParsedPlaylistItems",
columns: table => new
{
CreateDate = table.Column<DateTime>(nullable: false),
UpdateDate = table.Column<DateTime>(nullable: false),
Id = table.Column<int>(nullable: false)
.Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn),
VideoId = table.Column<string>(nullable: true),
IsProcessed = table.Column<bool>(nullable: false),
PlaylistId = table.Column<int>(nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_ParsedPlaylistItems", x => x.Id);
table.ForeignKey(
name: "FK_ParsedPlaylistItems_Playlists_PlaylistId",
column: x => x.PlaylistId,
principalTable: "Playlists",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_ParsedPlaylistItems_PlaylistId",
table: "ParsedPlaylistItems",
column: "PlaylistId");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "ParsedPlaylistItems");
migrationBuilder.CreateTable(
name: "ParsedPlaylistVideos",
columns: table => new
{
Id = table.Column<int>(nullable: false)
.Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn),
CreateDate = table.Column<DateTime>(nullable: false),
IsProcessed = table.Column<bool>(nullable: false),
PlaylistId = table.Column<int>(nullable: false),
UpdateDate = table.Column<DateTime>(nullable: false),
VideoId = table.Column<string>(nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_ParsedPlaylistVideos", x => x.Id);
table.ForeignKey(
name: "FK_ParsedPlaylistVideos_Playlists_PlaylistId",
column: x => x.PlaylistId,
principalTable: "Playlists",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_ParsedPlaylistVideos_PlaylistId",
table: "ParsedPlaylistVideos",
column: "PlaylistId");
}
}
}

View File

@@ -0,0 +1,392 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Metadata.Internal;
using Microsoft.EntityFrameworkCore.Migrations;
using PodNoms.Api.Persistence;
namespace PodNoms.Api.Migrations
{
[DbContext(typeof(PodnomsDbContext))]
[Migration("20180507162159_AddVideoTypeToPlaylistItem")]
partial class AddVideoTypeToPlaylistItem
{
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "2.1.0-preview2-30571")
.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();
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();
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.ParsedPlaylistItem", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<DateTime>("CreateDate");
b.Property<bool>("IsProcessed");
b.Property<int>("PlaylistId");
b.Property<DateTime>("UpdateDate");
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();
b.Property<DateTime>("CreateDate");
b.Property<int>("PodcastId");
b.Property<string>("SourceUrl");
b.Property<DateTime>("UpdateDate");
b.HasKey("Id");
b.HasIndex("PodcastId");
b.ToTable("Playlists");
});
modelBuilder.Entity("PodNoms.Api.Models.Podcast", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<string>("AppUserId");
b.Property<DateTime>("CreateDate");
b.Property<string>("Description");
b.Property<string>("Slug");
b.Property<string>("TemporaryImageUrl");
b.Property<string>("Title");
b.Property<string>("Uid");
b.Property<DateTime>("UpdateDate");
b.HasKey("Id");
b.HasIndex("AppUserId");
b.ToTable("Podcasts");
});
modelBuilder.Entity("PodNoms.Api.Models.PodcastEntry", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<long>("AudioFileSize");
b.Property<float>("AudioLength");
b.Property<string>("AudioUrl");
b.Property<string>("Author");
b.Property<DateTime>("CreateDate");
b.Property<string>("Description");
b.Property<string>("ImageUrl");
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");
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.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,24 @@
using System;
using System.Collections.Generic;
using Microsoft.EntityFrameworkCore.Migrations;
namespace PodNoms.Api.Migrations
{
public partial class AddVideoTypeToPlaylistItem : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "VideoType",
table: "ParsedPlaylistItems",
nullable: true);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "VideoType",
table: "ParsedPlaylistItems");
}
}
}

View File

@@ -127,6 +127,30 @@ namespace PodNoms.Api.Migrations
b.ToTable("AspNetUserTokens");
});
modelBuilder.Entity("PodNoms.Api.Models.ParsedPlaylistItem", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<DateTime>("CreateDate");
b.Property<bool>("IsProcessed");
b.Property<int>("PlaylistId");
b.Property<DateTime>("UpdateDate");
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")
@@ -154,14 +178,11 @@ namespace PodNoms.Api.Migrations
b.Property<string>("AppUserId");
b.Property<DateTime>("CreateDate")
.ValueGeneratedOnAdd()
.HasDefaultValueSql("getdate()");
b.Property<DateTime>("CreateDate");
b.Property<string>("Description");
b.Property<string>("Slug")
.IsUnicode(true);
b.Property<string>("Slug");
b.Property<string>("TemporaryImageUrl");
@@ -191,9 +212,7 @@ namespace PodNoms.Api.Migrations
b.Property<string>("Author");
b.Property<DateTime>("CreateDate")
.ValueGeneratedOnAdd()
.HasDefaultValueSql("getdate()");
b.Property<DateTime>("CreateDate");
b.Property<string>("Description");
@@ -332,6 +351,14 @@ namespace PodNoms.Api.Migrations
.OnDelete(DeleteBehavior.Cascade);
});
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")

View File

@@ -3,5 +3,6 @@ namespace PodNoms.Api.Models {
public string Version { get; set; }
public string SiteUrl { get; set; }
public string RssUrl { get; set; }
public string GoogleApiKey { get; set; }
}
}

View File

@@ -7,5 +7,17 @@ namespace PodNoms.Api.Models {
public string SourceUrl { get; set; }
public Podcast Podcast { get; set; }
public List<PodcastEntry> PodcastEntries { get; set; }
public List<ParsedPlaylistItem> ParsedPlaylistItems { get; set; }
public Playlist() {
ParsedPlaylistItems = new List<ParsedPlaylistItem>();
}
}
public class ParsedPlaylistItem : BaseModel {
public int Id { get; set; }
public string VideoId { get; set; }
public string VideoType { get; set; }
public bool IsProcessed { get; set; }
public int PlaylistId { get; set; }
public Playlist Playlist { get; set; }
}
}

View File

@@ -7,5 +7,7 @@ namespace PodNoms.Api.Persistence {
Task<Playlist> GetAsync(int id);
Task<IEnumerable<Playlist>> GetAllAsync();
Task<Playlist> AddOrUpdateAsync(Playlist playlist);
Task<ParsedPlaylistItem> GetParsedItem(string itemId, int playlistId);
Task<List<ParsedPlaylistItem>> GetUnprocessedItems();
}
}

View File

@@ -1,4 +1,5 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using PodNoms.Api.Models;
@@ -18,7 +19,7 @@ namespace PodNoms.Api.Persistence {
return entry;
}
public async Task<IEnumerable<Playlist>> GetAllAsync() {
return await _context.Playlists.ToListAsync();
return await _context.Playlists.Include(p => p.ParsedPlaylistItems).ToListAsync();
}
public async Task<Playlist> AddOrUpdateAsync(Playlist playlist) {
if (playlist.Id != 0) {
@@ -30,5 +31,20 @@ namespace PodNoms.Api.Persistence {
}
return playlist;
}
public async Task<ParsedPlaylistItem> GetParsedItem(string itemId, int playlistId) {
return await _context.ParsedPlaylistItems
.Include(i => i.Playlist)
.Include(i => i.Playlist.Podcast)
.Include(i => i.Playlist.Podcast.AppUser)
.SingleOrDefaultAsync(i => i.VideoId == itemId && i.PlaylistId == playlistId);
}
public async Task<List<ParsedPlaylistItem>> GetUnprocessedItems() {
return await _context.ParsedPlaylistItems
.Where(p => p.IsProcessed == false)
.Include(i => i.Playlist)
.Include(i => i.Playlist.Podcast)
.Include(i => i.Playlist.Podcast.AppUser)
.ToListAsync();
}
}
}

View File

@@ -1,5 +1,8 @@
using System;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Design;
@@ -15,24 +18,37 @@ namespace PodNoms.Api.Persistence {
protected override void OnModelCreating(ModelBuilder modelBuilder) {
base.OnModelCreating(modelBuilder);
modelBuilder.Entity<Podcast>()
.Property(b => b.CreateDate)
.HasDefaultValueSql("getdate()");
modelBuilder.Entity<Podcast>()
.Property(b => b.Slug)
.IsUnicode(true);
modelBuilder.Entity<PodcastEntry>()
.Property(b => b.CreateDate)
.HasDefaultValueSql("getdate()");
}
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; }
public DbSet<ParsedPlaylistItem> ParsedPlaylistItems { get; set; }
}
}

View File

@@ -3,6 +3,7 @@
<TargetFramework>netcoreapp2.1</TargetFramework>
<UserSecretsId>aspnet-PodNoms.Api-1E27B6DE-BA4B-4F75-BBF8-CA34FB4D260A</UserSecretsId>
<LangVersion>latest</LangVersion>
<RootNamespace>PodNoms.Api</RootNamespace>
</PropertyGroup>
<ItemGroup>
<Folder Include="wwwroot\" />
@@ -13,10 +14,8 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="FluentValidation.AspNetCore" Version="7.5.2" />
<PackageReference Include="Google.Apis" Version="1.32.2" />
<PackageReference Include="Google.Apis.Auth" Version="1.32.2" />
<PackageReference Include="Google.Apis.Core" Version="1.32.2" />
<PackageReference Include="Google.Apis.Plus.v1" Version="1.32.2.1182" />
<PackageReference Include="Google.Apis.Plus.v1" Version="1.33.0.1209" />
<PackageReference Include="Google.Apis.YouTube.v3" Version="1.33.0.1217" />
<PackageReference Include="Hangfire" Version="1.6.17" />
<PackageReference Include="Hangfire.MemoryStorage" Version="1.5.2" />
<PackageReference Include="Lib.Net.Http.WebPush" Version="1.3.0" />

View File

@@ -54,12 +54,11 @@ namespace PodNoms.Api.Services.Downloader {
return $"{{\"Error\": \"{ex.Message}\"}}";
}
}
public async Task<AudioType> GetInfo() {
public AudioType GetInfo() {
var ret = AudioType.Invalid;
await Task.Run(() => {
var youtubeDl = new YoutubeDL();
youtubeDl.VideoUrl = this._url;
var info = youtubeDl.GetDownloadInfo();
var yt = new YoutubeDL();
yt.VideoUrl = this._url;
var info = yt.GetDownloadInfo();
if (info != null &&
(info.Errors.Count == 0 || info.VideoSize != null)) {
@@ -71,7 +70,6 @@ namespace PodNoms.Api.Services.Downloader {
ret = AudioType.Valid;
}
}
});
return ret;
}
@@ -89,10 +87,12 @@ namespace PodNoms.Api.Services.Downloader {
yt.StandardOutputEvent += (sender, output) => {
if (output.Contains("%")) {
var progress = _parseProgress(output);
Console.WriteLine($"Processing {progress.CurrentSpeed} {progress.ETA} {progress.Percentage}");
if (DownloadProgress != null) {
DownloadProgress(this, progress);
}
} else {
Console.WriteLine(output);
if (PostProcessing != null) {
PostProcessing(this, output);
}

View File

@@ -10,7 +10,6 @@ namespace PodNoms.Api.Services.Jobs {
public static void BootstrapJobs() {
RecurringJob.AddOrUpdate<ClearOrphanAudioJob>(x => x.Execute(), Cron.Daily(1));
RecurringJob.AddOrUpdate<UpdateYouTubeDlJob>(x => x.Execute(), Cron.Daily(1, 30));
// BackgroundJob.Schedule<ProcessPlaylistsJob>(x => x.Execute(), TimeSpan.FromSeconds(1));
}
}

View File

@@ -0,0 +1,80 @@
using System.Threading.Tasks;
using Hangfire;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using PodNoms.Api.Models;
using PodNoms.Api.Persistence;
using PodNoms.Api.Services.Downloader;
using PodNoms.Api.Services.Processor;
using PodNoms.Api.Utils.RemoteParsers;
namespace PodNoms.Api.Services.Jobs {
public class ProcessPlaylistItemJob : IJob {
public readonly IPlaylistRepository _playlistRepository;
public readonly IEntryRepository _entryRepository;
private readonly IAudioUploadProcessService _uploadService;
private readonly IConfiguration _options;
private readonly IPodcastRepository _podcastRepository;
private readonly ApplicationsSettings _applicationsSettings;
private readonly ILogger<ProcessPlaylistItemJob> _logger;
private readonly IUnitOfWork _unitOfWork;
public ProcessPlaylistItemJob(IPlaylistRepository playlistRepository, IEntryRepository entryRepository,
IAudioUploadProcessService uploadService, IConfiguration options,
IPodcastRepository podcastRepository, IOptions<ApplicationsSettings> applicationsSettings,
IUnitOfWork unitOfWork, ILogger<ProcessPlaylistItemJob> logger) {
this._unitOfWork = unitOfWork;
this._playlistRepository = playlistRepository;
this._entryRepository = entryRepository;
this._uploadService = uploadService;
this._options = options;
this._podcastRepository = podcastRepository;
this._applicationsSettings = applicationsSettings.Value;
this._logger = logger;
}
public async Task Execute() {
var items = await _playlistRepository.GetUnprocessedItems();
foreach (var item in items) {
await ExecuteForItem(item.VideoId, item.Playlist.Id);
}
}
public async Task ExecuteForItem(string itemId, int playlistId) {
var item = await _playlistRepository.GetParsedItem(itemId, playlistId);
if (item != null && !string.IsNullOrEmpty(item.VideoType) && item.VideoType.Equals("youtube")) {
var url = $"https://www.youtube.com/watch?v={item.VideoId}";
var downloader = new AudioDownloader(url, _applicationsSettings.Downloader);
var info = downloader.GetInfo();
if (info == AudioType.Valid) {
var podcast = await _podcastRepository.GetAsync(item.Playlist.PodcastId);
var uid = System.Guid.NewGuid().ToString();
var file = downloader.DownloadAudio(uid);
if (System.IO.File.Exists(file)) {
//we have the file so lets create the entry and ship to CDN
var entry = new PodcastEntry {
Title = downloader.Properties?.Title,
Uid = uid,
Description = downloader.Properties?.Description,
ProcessingStatus = ProcessingStatus.Uploading,
ImageUrl = downloader.Properties?.Thumbnail
};
podcast.PodcastEntries.Add(entry);
await _unitOfWork.CompleteAsync();
var uploaded = await _uploadService.UploadAudio(entry.Id, file);
if (uploaded) {
item.IsProcessed = true;
await _unitOfWork.CompleteAsync();
BackgroundJob.Enqueue<INotifyJobCompleteService>(
service => service.NotifyUser(entry.Podcast.AppUser.Id, "PodNoms", $"{entry.Title} has finished processing",
entry.Podcast.GetThumbnailUrl(
this._options.GetSection("Storage")["CdnUrl"],
this._options.GetSection("ImageFileStorageSettings")["ContainerName"])
));
}
}
} else {
_logger.LogError($"Processing playlist item {itemId} failed");
}
}
}
}
}

View File

@@ -1,13 +1,16 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Hangfire;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using NYoutubeDL.Models;
using PodNoms.Api.Models;
using PodNoms.Api.Persistence;
using PodNoms.Api.Services.Downloader;
using PodNoms.Api.Utils.RemoteParsers;
using static NYoutubeDL.Helpers.Enums;
namespace PodNoms.Api.Services.Jobs {
@@ -16,44 +19,51 @@ namespace PodNoms.Api.Services.Jobs {
public readonly IEntryRepository _entryRepository;
private readonly ApplicationsSettings _applicationsSettings;
private readonly ILogger<ProcessPlaylistsJob> _logger;
private readonly YouTubeParser _youTubeParser;
private readonly MixcloudParser _mixcloudParser;
private readonly IUnitOfWork _unitOfWork;
public ProcessPlaylistsJob(IPlaylistRepository playlistRepository,
IEntryRepository entryRepository, IOptions<ApplicationsSettings> applicationsSettings, ILoggerFactory logger) {
_playlistRepository = playlistRepository;
_entryRepository = entryRepository;
_applicationsSettings = applicationsSettings.Value;
_logger = logger.CreateLogger<ProcessPlaylistsJob>();
public ProcessPlaylistsJob(IPlaylistRepository playlistRepository, IEntryRepository entryRepository,
IUnitOfWork unitOfWork, IOptions<ApplicationsSettings> applicationsSettings,
ILoggerFactory logger, YouTubeParser youTubeParser, MixcloudParser mixcloudParser) {
this._unitOfWork = unitOfWork;
this._youTubeParser = youTubeParser;
this._mixcloudParser = mixcloudParser;
this._playlistRepository = playlistRepository;
this._entryRepository = entryRepository;
this._applicationsSettings = applicationsSettings.Value;
this._logger = logger.CreateLogger<ProcessPlaylistsJob>();
}
public async Task Execute() {
var playists = await _playlistRepository.GetAllAsync();
var playlists = await _playlistRepository.GetAllAsync();
var resultList = new List<ParsedItemResult>();
foreach (var playlist in playists) {
foreach (var playlist in playlists) {
var downloader = new AudioDownloader(playlist.SourceUrl, _applicationsSettings.Downloader);
var info = await downloader.GetInfo();
var info = downloader.GetInfo();
var id = ((PlaylistDownloadInfo)downloader.RawProperties).Id;
if (info == AudioType.Playlist && downloader.RawProperties is PlaylistDownloadInfo) {
var list = ((PlaylistDownloadInfo)downloader.RawProperties).Videos
.OrderByDescending(x => x.Id)
.Take(10);
StringBuilder br = new StringBuilder();
foreach (var item in list) {
_logger.LogDebug($"Processing: {item.Id} - {item.Url}");
br.Append($"Processing: {item.Id} - {item.Title}\n");
var outputDir = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());
Directory.CreateDirectory(outputDir);
var yt = new NYoutubeDL.YoutubeDL(playlist.SourceUrl);
yt.Options.PostProcessingOptions.ExtractAudio = true;
yt.Options.PostProcessingOptions.AudioFormat = AudioFormat.mp3;
yt.Options.VideoSelectionOptions.PlaylistItems = "1,2,3";
yt.Options.FilesystemOptions.Output = Path.Combine(outputDir, "%(playlist)s/%(playlist_index)s - %(title)s.%(ext)s");
var p = yt.Download();
p.WaitForExit();
if (_youTubeParser.ValidateUrl(playlist.SourceUrl)) {
var searchTerm = (playlist.SourceUrl.Contains("/user/")) ? "forUsername" : "id";
resultList = await _youTubeParser.GetPlaylistEntriesForId(id);
//make sure the items are sorted in ascending date order
//so they will be processed in the order they were created
} else if (_mixcloudParser.ValidateUrl(playlist.SourceUrl)) {
resultList = await _mixcloudParser.GetEntries(id);
}
_logger.LogDebug(br.ToString());
}
foreach (var item in resultList?.OrderBy(r => r.UploadDate)) {
if (!playlist.ParsedPlaylistItems.Any(p => p.VideoId == item.Id)) {
playlist.ParsedPlaylistItems.Add(new ParsedPlaylistItem {
VideoId = item.Id,
VideoType = item.VideoType
});
BackgroundJob.Enqueue<ProcessPlaylistItemJob>(service => service.ExecuteForItem(item.Id, playlist.Id));
}
}
await _unitOfWork.CompleteAsync();
}
}
}

View File

@@ -18,8 +18,7 @@
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
namespace NYoutubeDL.Models
{
namespace NYoutubeDL.Models {
#region Using
using System;
@@ -32,8 +31,7 @@ namespace NYoutubeDL.Models
/// <summary>
/// Class holding data about the current download, which is parsed from youtube-dl's standard output
/// </summary>
public class DownloadInfo : NotifyPropertyChangedEx
{
public class DownloadInfo : NotifyPropertyChangedEx {
protected const string ALREADY = "already";
protected const string DOWNLOADRATESTRING = "iB/s";
@@ -51,7 +49,7 @@ namespace NYoutubeDL.Models
private string eta;
private string status = Enums.DownloadStatus.WAITING.ToString();
private string id;
private string title;
private int videoProgress;
@@ -61,8 +59,7 @@ namespace NYoutubeDL.Models
/// <summary>
/// The current download rate
/// </summary>
public string DownloadRate
{
public string DownloadRate {
get => this.downloadRate;
set => this.SetField(ref this.downloadRate, value);
}
@@ -75,8 +72,7 @@ namespace NYoutubeDL.Models
/// <summary>
/// The current download's estimated time remaining
/// </summary>
public string Eta
{
public string Eta {
get => this.eta;
set => this.SetField(ref this.eta, value);
}
@@ -84,29 +80,30 @@ namespace NYoutubeDL.Models
/// <summary>
/// The status of the video currently downloading
/// </summary>
public string Status
{
public string Status {
get => this.status;
set
{
set {
if (!this.status.Equals(Enums.DownloadStatus.ERROR.ToString()) &&
!this.status.Equals(Enums.DownloadStatus.WARNING.ToString()))
{
!this.status.Equals(Enums.DownloadStatus.WARNING.ToString())) {
this.SetField(ref this.status, value);
}
else if (value.Equals(Enums.DownloadStatus.ERROR.ToString()) &&
this.status.Equals(Enums.DownloadStatus.WARNING.ToString()))
{
} else if (value.Equals(Enums.DownloadStatus.ERROR.ToString()) &&
this.status.Equals(Enums.DownloadStatus.WARNING.ToString())) {
this.SetField(ref this.status, value);
}
}
}
/// <summary>
/// The id of the playlist
/// </summary>
public string Id {
get => this.id;
set => this.SetField(ref this.id, value);
}
/// <summary>
/// The title of the video currently downloading
/// </summary>
public string Title
{
public string Title {
get => this.title;
set => this.SetField(ref this.title, value);
}
@@ -114,23 +111,16 @@ namespace NYoutubeDL.Models
/// <summary>
/// The current download progresss
/// </summary>
public int VideoProgress
{
public int VideoProgress {
get => this.videoProgress;
set
{
set {
this.SetField(ref this.videoProgress, value);
if (value == 0)
{
if (value == 0) {
this.Status = Enums.DownloadStatus.WAITING.ToString();
}
else if (value == 100)
{
} else if (value == 100) {
this.Status = Enums.DownloadStatus.DONE.ToString();
}
else
{
} else {
this.Status = Enums.DownloadStatus.DOWNLOADING.ToString();
}
}
@@ -139,8 +129,7 @@ namespace NYoutubeDL.Models
/// <summary>
/// The current download's total size
/// </summary>
public string VideoSize
{
public string VideoSize {
get => this.videoSize;
set => this.SetField(ref this.videoSize, value);
}
@@ -150,31 +139,25 @@ namespace NYoutubeDL.Models
/// </summary>
public List<string> Warnings { get; } = new List<string>();
internal static DownloadInfo CreateDownloadInfo(string output)
{
try
{
internal static DownloadInfo CreateDownloadInfo(string output) {
if (string.IsNullOrEmpty(output) || output.Equals("null"))
return null;
try {
PlaylistInfo info = JsonConvert.DeserializeObject<PlaylistInfo>(output);
if (!string.IsNullOrEmpty(info._type) && info._type.Equals("playlist"))
{
if (!string.IsNullOrEmpty(info._type) && info._type.Equals("playlist")) {
return new PlaylistDownloadInfo(info);
}
}
catch (Exception ex)
{
} catch (Exception ex) {
Console.WriteLine(ex);
}
try
{
try {
VideoInfo info = JsonConvert.DeserializeObject<VideoInfo>(output);
if (!string.IsNullOrEmpty(info.title))
{
if (!string.IsNullOrEmpty(info.title)) {
return new VideoDownloadInfo(info);
}
}
catch (Exception ex)
{
} catch (Exception ex) {
Console.WriteLine(ex);
}
@@ -186,57 +169,45 @@ namespace NYoutubeDL.Models
/// </summary>
public event EventHandler<string> ErrorEvent;
internal virtual void ParseError(object sender, string error)
{
internal virtual void ParseError(object sender, string error) {
this.ErrorEvent?.Invoke(this, error);
if (error.Contains("WARNING"))
{
if (error.Contains("WARNING")) {
this.Warnings.Add(error);
this.Status = Enums.DownloadStatus.WARNING.ToString();
}
else if (error.Contains("ERROR"))
{
} else if (error.Contains("ERROR")) {
this.Errors.Add(error);
this.Status = Enums.DownloadStatus.ERROR.ToString();
}
}
internal virtual void ParseOutput(object sender, string output)
{
try
{
if (output.Contains("%"))
{
internal virtual void ParseOutput(object sender, string output) {
try {
if (output.Contains("%")) {
int progressIndex = output.LastIndexOf(' ', output.IndexOf('%')) + 1;
string progressString = output.Substring(progressIndex, output.IndexOf('%') - progressIndex);
this.VideoProgress = (int) Math.Round(double.Parse(progressString));
this.VideoProgress = (int)Math.Round(double.Parse(progressString));
int sizeIndex = output.LastIndexOf(' ', output.IndexOf(DOWNLOADSIZESTRING)) + 1;
string sizeString = output.Substring(sizeIndex, output.IndexOf(DOWNLOADSIZESTRING) - sizeIndex + 2);
this.VideoSize = sizeString;
}
if (output.Contains(DOWNLOADRATESTRING))
{
if (output.Contains(DOWNLOADRATESTRING)) {
int rateIndex = output.LastIndexOf(' ', output.LastIndexOf(DOWNLOADRATESTRING)) + 1;
string rateString =
output.Substring(rateIndex, output.LastIndexOf(DOWNLOADRATESTRING) - rateIndex + 4);
this.DownloadRate = rateString;
}
if (output.Contains(ETASTRING))
{
if (output.Contains(ETASTRING)) {
this.Eta = output.Substring(output.LastIndexOf(' ') + 1);
}
if (output.Contains(ALREADY))
{
if (output.Contains(ALREADY)) {
this.Status = Enums.DownloadStatus.DONE.ToString();
this.VideoProgress = 100;
}
}
catch (Exception)
{
} catch (Exception) {
}
}
}

View File

@@ -39,6 +39,7 @@ namespace NYoutubeDL.Models
public PlaylistDownloadInfo(PlaylistInfo info)
{
this.Id = info.id;
this.Title = info.title;
foreach (VideoInfo videoInfo in info.entries)
{

View File

@@ -180,11 +180,6 @@ namespace NYoutubeDL.Models
public int? Width { get; }
/// <summary>
/// The ID string of the video
/// </summary>
public string Id { get; }
public List<ThumbnailDownloadInfo> Thumbnails { get; } = new List<ThumbnailDownloadInfo>();
public List<string> Tags { get; }

View File

@@ -57,7 +57,7 @@ namespace PodNoms.Api.Services.Processor {
public async Task<AudioType> GetInformation(PodcastEntry entry) {
var downloader = new AudioDownloader(entry.SourceUrl, _applicationsSettings.Downloader);
var ret = await downloader.GetInfo();
var ret = downloader.GetInfo();
if (ret == AudioType.Valid) {
entry.Title = downloader.Properties?.Title;
entry.Description = downloader.Properties?.Description;

View File

@@ -46,6 +46,7 @@ using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.AspNetCore.Identity.UI.Services;
using PodNoms.Api.Utils.RemoteParsers;
namespace PodNoms.Api {
public class Startup {
@@ -102,7 +103,7 @@ namespace PodNoms.Api {
services.Configure<ImageFileStorageSettings>(Configuration.GetSection("ImageFileStorageSettings"));
services.Configure<AudioFileStorageSettings>(Configuration.GetSection("AudioFileStorageSettings"));
services.Configure<FormOptions>(options => {
options.ValueCountLimit = 10;
// options.ValueCountLimit = 10;
options.ValueLengthLimit = int.MaxValue;
options.MemoryBufferThreshold = Int32.MaxValue;
options.MultipartBodyLengthLimit = long.MaxValue;
@@ -112,6 +113,10 @@ namespace PodNoms.Api {
e.AddProfile(new MappingProvider(Configuration));
});
services.AddHttpClient("mixcloud", c => {
c.BaseAddress = new Uri("https://api.mixcloud.com/");
c.DefaultRequestHeaders.Add("Accept", "application/json");
});
services.AddHttpClient();
var jwtAppSettingOptions = Configuration.GetSection(nameof(JwtIssuerOptions));
@@ -220,6 +225,8 @@ namespace PodNoms.Api {
services.AddScoped<INotifyJobCompleteService, NotifyJobCompleteService>();
services.AddScoped<IAudioUploadProcessService, AudioUploadProcessService>();
services.AddScoped<IMailSender, MailgunSender>();
services.AddScoped<YouTubeParser>();
services.AddScoped<MixcloudParser>();
services.AddHttpClient<Services.Gravatar.GravatarHttpClient>();
//register the codepages (required for slugify)

View File

@@ -0,0 +1,36 @@
using System.Collections.Generic;
using System.Net.Http;
using System.Linq;
using System.Threading.Tasks;
using Newtonsoft.Json;
using System.Text.RegularExpressions;
namespace PodNoms.Api.Utils.RemoteParsers {
public class MixcloudParser {
const string URL_REGEX = @"^(http(s)?:\/\/)?((w){3}.)?mixcloud?(\.com)?\/.+";
private readonly IHttpClientFactory _httpClientFactory;
public MixcloudParser(IHttpClientFactory httpClientFactory) {
this._httpClientFactory = httpClientFactory;
}
public bool ValidateUrl(string url) {
var regex = new Regex(URL_REGEX);
var result = regex.Match(url);
return result.Success;
}
public async Task<List<ParsedItemResult>> GetEntries(string identifier) {
var client = _httpClientFactory.CreateClient("mixcloud");
var result = await client.GetAsync(identifier);
if (result.IsSuccessStatusCode) {
var typed = JsonConvert.DeserializeObject<MixcloudResult>(await result.Content.ReadAsStringAsync());
return typed.data[0].cloudcasts.Select(c => new ParsedItemResult {
Id = c.key,
VideoType = "mixcloud",
UploadDate = c.updated_time
}).ToList();
}
return null;
}
}
}

View File

@@ -0,0 +1,75 @@
using System;
using System.Collections.Generic;
namespace PodNoms.Api.Utils.RemoteParsers {
public class Paging {
public string previous { get; set; }
public string next { get; set; }
}
public class Pictures {
public string medium { get; set; }
public string extra_large { get; set; }
public string large { get; set; }
public string medium_mobile { get; set; }
public string small { get; set; }
public string thumbnail { get; set; }
}
public class From {
public string url { get; set; }
public string username { get; set; }
public string name { get; set; }
public string key { get; set; }
public Pictures pictures { get; set; }
}
public class Tag {
public string url { get; set; }
public string name { get; set; }
public string key { get; set; }
}
public class User {
public string url { get; set; }
public string username { get; set; }
public string name { get; set; }
public string key { get; set; }
}
public class Cloudcast {
public IList<Tag> tags { get; set; }
public int play_count { get; set; }
public User user { get; set; }
public string key { get; set; }
public DateTime created_time { get; set; }
public int audio_length { get; set; }
public string slug { get; set; }
public int favorite_count { get; set; }
public int listener_count { get; set; }
public string name { get; set; }
public string url { get; set; }
public int repost_count { get; set; }
public DateTime updated_time { get; set; }
public int comment_count { get; set; }
}
public class Datum {
public From from { get; set; }
public string title { get; set; }
public string url { get; set; }
public string key { get; set; }
public DateTime created_time { get; set; }
public IList<Cloudcast> cloudcasts { get; set; }
public string type { get; set; }
}
public class MixcloudResult {
public Paging paging { get; set; }
public IList<Datum> data { get; set; }
public string name { get; set; }
}
}

View File

@@ -0,0 +1,9 @@
using System;
namespace PodNoms.Api.Utils.RemoteParsers {
public class ParsedItemResult {
public string Id { get; set; }
public string VideoType { get; set; }
public DateTime? UploadDate { get; set; }
}
}

View File

@@ -0,0 +1,61 @@
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Google.Apis.Services;
using Google.Apis.YouTube.v3;
using Microsoft.Extensions.Options;
using PodNoms.Api.Models;
namespace PodNoms.Api.Utils.RemoteParsers {
public partial class YouTubeParser {
const string URL_REGEX = @"^(http(s)?:\/\/)?((w){3}.)?youtu(be|.be)?(\.com)?\/.+";
private readonly AppSettings _settings;
private YouTubeService youtube;
public YouTubeParser(IOptions<AppSettings> options) {
this._settings = options.Value;
this.youtube = _getYouTubeService();
}
private YouTubeService _getYouTubeService() {
return new YouTubeService(new BaseClientService.Initializer() {
ApiKey = _settings.GoogleApiKey,
ApplicationName = this.GetType().ToString()
});
}
public bool ValidateUrl(string url) {
var regex = new Regex(URL_REGEX);
var result = regex.Match(url);
return result.Success;
}
public async Task<List<ParsedItemResult>> GetPlaylistEntriesForId(string id, int nCount = 10) {
var playlistRequest = youtube.PlaylistItems.List("contentDetails");
playlistRequest.PlaylistId = id;
playlistRequest.MaxResults = nCount;
var plists = await playlistRequest.ExecuteAsync();
return plists.Items
.Select(p => new ParsedItemResult {
Id = p.ContentDetails.VideoId,
VideoType = "youtube",
UploadDate = p.ContentDetails.VideoPublishedAt
}).ToList();
}
public async Task<List<ParsedItemResult>> GetPlaylistEntriesForChannelName(string channelName, string searchType, int nCount = 10) {
var request = youtube.Channels.List("contentDetails");
if (searchType.Equals("id"))
request.Id = channelName;
else
request.ForUsername = channelName;
request.MaxResults = 1;
var resp = await request.ExecuteAsync();
if (resp.Items.Count == 1) {
var uploadListId = resp.Items[0].ContentDetails.RelatedPlaylists.Uploads;
if (!string.IsNullOrEmpty(uploadListId)) {
return await GetPlaylistEntriesForId(uploadListId, nCount);
}
}
return null;
}
}
}

View File

@@ -8,9 +8,7 @@
}
},
"App": {
"Version": "0.23.0",
"SiteUrl": "http://localhost:4200",
"RssUrl": "http://localhost:5000/rss/"
"Version": "0.23.0"
},
"ConnectionStrings": {
"DefaultConnection": "server=localhost;database=PodNoms;user id=sa;password=cTXu1nJLCpC/c",