HUGE arch changes to make searching work right

This commit is contained in:
hak8or 2018-03-05 01:08:55 -05:00
parent 96a26ce30d
commit 9f5fb78835
32 changed files with 800 additions and 447 deletions

View File

@ -53,14 +53,5 @@ namespace YTManager.Controllers {
public IActionResult Get_Update_Status() {
return Ok(get_massupdatedaemon() == null ? "false" : "true");
}
// Testing
[HttpGet("Test")]
public async System.Threading.Tasks.Task<IActionResult> Test() {
// await Tasks.FetchVideos.MassUpdate(Startup.DBStr);
// var vids = Tasks.FetchVideos.Get_YTVideos("UCsXVk37bltHxD1rDPwtNM8Q", 1).Result;
// return Ok(vids);
return Ok();
}
}
}

View File

@ -1,14 +1,13 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Cors;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using System;
namespace YTManager.Controllers {
[Produces("application/json")]
[Route("api/Channels")]
[EnableCors("AllowAllOrigins")]
public class ChannelsController : Controller {
// Custom return type for API accesses. Done this way to ensure we
// always return the expected data regardless of the underlying model.
@ -16,6 +15,7 @@ namespace YTManager.Controllers {
public string Title;
public string Description;
public string ID;
public List<string> User_Tags;
public List<string> Video_IDs;
public Channel_ForAPI(Models.Channel c) {
@ -23,6 +23,7 @@ namespace YTManager.Controllers {
Description = c.Description;
ID = c.YoutubeID;
Video_IDs = c.Videos.Select(v => v.YoutubeID).ToList();
User_Tags = c.UserTags == null ? new List<string>() : c.UserTags.ToList();
}
}
@ -38,6 +39,9 @@ namespace YTManager.Controllers {
// Returns the most recently added channels.
[HttpGet]
public async Task<IActionResult> Get() {
// Log this to the terminal.
Console.WriteLine($"{DateTime.Now} == Channels GET");
// Get all the relevant channels.
var chanels = await db.Channels
.Include(c => c.Videos)
@ -53,5 +57,29 @@ namespace YTManager.Controllers {
// Convert all the videos to what we will send back.
return Ok(converted);
}
[HttpPost("{channelid}")]
public async Task<IActionResult> PostChannel([FromRoute] string channelid) {
Console.WriteLine($"{DateTime.Now} == Channels POST -> {channelid}");
// Verify the channel looks resonable.
var expected_len = "UCyS4xQE6DK4_p3qXQwJQAyA".Length;
if (channelid.Length != expected_len)
return BadRequest($"Length should be {expected_len} but is {channelid.Length}");
// Only add it to the databse if it's not in there already.
if (db.Channels.Any(c => c.YoutubeID == channelid))
return BadRequest($"Channel {channelid} is already in DB!");
// Get the channel contents from youtube.
var channel = await Tasks.FetchVideos.Get_YTChannel(channelid);
// Add it to the databse.
await db.Channels.AddAsync(channel);
await db.SaveChangesAsync();
// Say all went ok.
return Ok();
}
}
}

View File

@ -82,23 +82,6 @@ namespace YTManager.Controllers.Private
return NoContent();
}
[HttpPost("{channelid}")]
public async Task<IActionResult> PostChannel([FromRoute] string channelid) {
// Only add it to the databse if it's not in there already.
if (db.Channels.Any(c => c.YoutubeID == channelid))
return BadRequest();
// Get the channel contents from youtube.
var channel = await Tasks.FetchVideos.Get_YTChannel(channelid);
// Add it to the databse.
await db.Channels.AddAsync(channel);
await db.SaveChangesAsync();
// Say all went ok.
return Ok(channel);
}
[HttpPost]
public async Task<IActionResult> PostChannel([FromBody] Channel channel)
{

View File

@ -3,14 +3,12 @@ using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using System.Xml;
using Microsoft.AspNetCore.Cors;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
namespace YTManager.Controllers {
[Produces("application/json")]
[Route("api/Videos")]
[EnableCors("AllowAllOrigins")]
public class VideosController : Controller {
// Custom return type for API accesses. Done this way to ensure we
// always return the expected data regardless of the underlying model.
@ -44,7 +42,7 @@ namespace YTManager.Controllers {
Thumbnail = video.ThumbnailURL;
Channel = video.Channel.Title;
Seconds = (int)video.Duration.TotalSeconds;
Tags = video.Tags?.Select(t => t.Name).ToList();
Tags = video.Tags == null ? new List<string>() : video.Tags.ToList();
}
}
@ -57,31 +55,7 @@ namespace YTManager.Controllers {
// Constructor to fetch the db context.
public VideosController(MediaDB context) => db = context;
// Returns the most recent videos.
[HttpGet]
public async Task<IActionResult> GetVideos() {
// Get all the relevant videos.
var vids = await db.Videos
.OrderByDescending(i => i.AddedToYT)
.Take(max_per_query)
.Include(v => v.Channel)
.Include(v => v.Tags)
.ToListAsync();
// Convert them to what we will send out.
var converted = vids
.Select(v => new Video_ForAPI(v))
.ToList();
// Convert all the videos to what we will send back.
return Ok(converted);
}
struct Search_Query
{
// Tags.
public List<Models.Tag> Tags;
struct Search_Query {
// What the duration type is.
public enum _Duration_Type { LessThan, GreaterThan, Unset };
public _Duration_Type Duration_Type;
@ -92,11 +66,27 @@ namespace YTManager.Controllers {
// Remaining tokens.
public List<String> Remaining_Tokens;
}
// Searches for videos using the specified tag(s).
[HttpGet("search/{searchstr}")]
[HttpGet]
public async Task<IActionResult> Search() {
var converted = await db.Videos
.OrderByDescending(i => i.AddedToYT)
.Take(max_per_query)
.Include(v => v.Channel)
.Select(v => new Video_ForAPI(v))
.ToListAsync();
// Convert all the videos to what we will send back.
return Ok(converted);
}
// Searches for videos using the specified tag(s).
[HttpGet("{searchstr}")]
public async Task<IActionResult> Search([FromRoute] string searchstr) {
// Log this to the terminal.
Console.WriteLine($"{DateTime.Now} == Search request for -> {searchstr}.");
// What to use for searching videos with.
var parsed_query = new Search_Query();
@ -128,12 +118,6 @@ namespace YTManager.Controllers {
}
}
// Find which tokens are explicit tags
parsed_query.Tags = await db.Tags
.Where(t => parsed_query.Remaining_Tokens.Any(token => token == t.Name.ToLower()))
.ToListAsync();
parsed_query.Tags.ForEach(tag => parsed_query.Remaining_Tokens.Remove(tag.Name.ToLower()));
// Get from the database all videos which satisfy the query via
// AND'ing all the queries.
var dbquery = db.Videos.Select(v => v);
@ -144,16 +128,14 @@ namespace YTManager.Controllers {
else if (parsed_query.Duration_Type == Search_Query._Duration_Type.LessThan)
dbquery = dbquery.Where(v => v.Duration <= parsed_query.Duration);
// Match videos where the tag matches.
parsed_query.Tags.ForEach(tag => dbquery = dbquery.Where(V => V.Tags.Any(vt => vt.Name == tag.Name)));
// Get all videos that match their title, description, or channel name
// with the remaining tokens.
parsed_query.Remaining_Tokens.ForEach(token => {
dbquery = dbquery.Where(v =>
v.Channel.Title.ToLower().Contains(token) ||
v.Title.ToLower().Contains(token) ||
v.Description.ToLower().Contains(token));
v.Description.ToLower().Contains(token) ||
v.YoutubeID.ToLower().Contains(token));
});
// Get all the relevant videos.
@ -161,7 +143,6 @@ namespace YTManager.Controllers {
.OrderByDescending(i => i.AddedToYT)
.Take(max_per_query)
.Include(v => v.Channel)
.Include(v => v.Tags)
.ToListAsync();
// Convert them to what we will send out.
@ -169,26 +150,8 @@ namespace YTManager.Controllers {
.Select(v => new Video_ForAPI(v))
.ToList();
// Convert all the videos to what we will send back.
return Ok(converted);
}
// Returns the most recent videos of a channel.
[HttpGet("fromchannel/{channelName}")]
public async Task<IActionResult> Get_Channel_Videos([FromRoute] string channelName) {
// Get all the relevant videos.
var vids = await db.Videos
.Where(v => v.Channel.Title == channelName)
.OrderByDescending(i => i.AddedToYT)
.Take(max_per_query)
.Include(v => v.Channel)
.Include(v => v.Tags)
.ToListAsync();
// Convert them to what we will send out.
var converted = vids
.Select(v => new Video_ForAPI(v))
.ToList();
// Log this to the terminal.
Console.WriteLine($"{DateTime.Now} == Search request for -> {searchstr} found {converted.Count()} videos.");
// Convert all the videos to what we will send back.
return Ok(converted);

View File

@ -6,7 +6,6 @@ namespace YTManager
{
public DbSet<Models.Channel> Channels { get; set; }
public DbSet<Models.Video> Videos { get; set; }
public DbSet<Models.Tag> Tags { get; set; }
public MediaDB(DbContextOptions<MediaDB> options) : base(options){ }

View File

@ -1,82 +0,0 @@
using Microsoft.EntityFrameworkCore.Migrations;
using System;
using System.Collections.Generic;
namespace YTManager.Migrations
{
public partial class fix_channel_relationship : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_Videos_Channels_ChannelPrimaryKey",
table: "Videos");
migrationBuilder.AlterColumn<long>(
name: "ChannelPrimaryKey",
table: "Videos",
nullable: false,
oldClrType: typeof(long),
oldNullable: true);
migrationBuilder.AddColumn<long>(
name: "VideoPrimaryKey",
table: "Tags",
nullable: true);
migrationBuilder.CreateIndex(
name: "IX_Tags_VideoPrimaryKey",
table: "Tags",
column: "VideoPrimaryKey");
migrationBuilder.AddForeignKey(
name: "FK_Tags_Videos_VideoPrimaryKey",
table: "Tags",
column: "VideoPrimaryKey",
principalTable: "Videos",
principalColumn: "PrimaryKey",
onDelete: ReferentialAction.Restrict);
migrationBuilder.AddForeignKey(
name: "FK_Videos_Channels_ChannelPrimaryKey",
table: "Videos",
column: "ChannelPrimaryKey",
principalTable: "Channels",
principalColumn: "PrimaryKey",
onDelete: ReferentialAction.Cascade);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_Tags_Videos_VideoPrimaryKey",
table: "Tags");
migrationBuilder.DropForeignKey(
name: "FK_Videos_Channels_ChannelPrimaryKey",
table: "Videos");
migrationBuilder.DropIndex(
name: "IX_Tags_VideoPrimaryKey",
table: "Tags");
migrationBuilder.DropColumn(
name: "VideoPrimaryKey",
table: "Tags");
migrationBuilder.AlterColumn<long>(
name: "ChannelPrimaryKey",
table: "Videos",
nullable: true,
oldClrType: typeof(long));
migrationBuilder.AddForeignKey(
name: "FK_Videos_Channels_ChannelPrimaryKey",
table: "Videos",
column: "ChannelPrimaryKey",
principalTable: "Channels",
principalColumn: "PrimaryKey",
onDelete: ReferentialAction.Restrict);
}
}
}

View File

@ -1,25 +0,0 @@
using Microsoft.EntityFrameworkCore.Migrations;
using System;
using System.Collections.Generic;
namespace YTManager.Migrations
{
public partial class added_vid_duration : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<TimeSpan>(
name: "Duration",
table: "Videos",
nullable: false,
defaultValue: "00:00:00");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "Duration",
table: "Videos");
}
}
}

View File

@ -6,12 +6,13 @@ using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage;
using Microsoft.EntityFrameworkCore.Storage.Internal;
using System;
using System.Collections.Generic;
using YTManager;
namespace YTManager.Migrations
{
[DbContext(typeof(MediaDB))]
[Migration("20180224051602_initial")]
[Migration("20180304034317_initial")]
partial class initial
{
protected override void BuildTargetModel(ModelBuilder modelBuilder)
@ -39,6 +40,8 @@ namespace YTManager.Migrations
b.Property<string>("Title")
.IsRequired();
b.Property<List<string>>("UserTags");
b.Property<string>("YoutubeID")
.IsRequired();
@ -47,19 +50,6 @@ namespace YTManager.Migrations
b.ToTable("Channels");
});
modelBuilder.Entity("YTManager.Models.Tag", b =>
{
b.Property<long>("PrimaryKey")
.ValueGeneratedOnAdd();
b.Property<string>("Name")
.IsRequired();
b.HasKey("PrimaryKey");
b.ToTable("Tags");
});
modelBuilder.Entity("YTManager.Models.Video", b =>
{
b.Property<long>("PrimaryKey")
@ -69,11 +59,16 @@ namespace YTManager.Migrations
b.Property<DateTime>("AddedtoDB");
b.Property<long?>("ChannelPrimaryKey");
b.Property<long>("ChannelPrimaryKey");
b.Property<string>("Description")
.IsRequired();
b.Property<TimeSpan>("Duration");
b.Property<List<string>>("Tags")
.IsRequired();
b.Property<string>("ThumbnailURL")
.IsRequired();
@ -92,9 +87,10 @@ namespace YTManager.Migrations
modelBuilder.Entity("YTManager.Models.Video", b =>
{
b.HasOne("YTManager.Models.Channel")
b.HasOne("YTManager.Models.Channel", "Channel")
.WithMany("Videos")
.HasForeignKey("ChannelPrimaryKey");
.HasForeignKey("ChannelPrimaryKey")
.OnDelete(DeleteBehavior.Cascade);
});
#pragma warning restore 612, 618
}

View File

@ -20,6 +20,7 @@ namespace YTManager.Migrations
Refreshed = table.Column<DateTime>(nullable: false),
ThumbnailURL = table.Column<string>(nullable: false),
Title = table.Column<string>(nullable: false),
UserTags = table.Column<List<string>>(nullable: true),
YoutubeID = table.Column<string>(nullable: false)
},
constraints: table =>
@ -27,19 +28,6 @@ namespace YTManager.Migrations
table.PrimaryKey("PK_Channels", x => x.PrimaryKey);
});
migrationBuilder.CreateTable(
name: "Tags",
columns: table => new
{
PrimaryKey = table.Column<long>(nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.SerialColumn),
Name = table.Column<string>(nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Tags", x => x.PrimaryKey);
});
migrationBuilder.CreateTable(
name: "Videos",
columns: table => new
@ -48,8 +36,10 @@ namespace YTManager.Migrations
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.SerialColumn),
AddedToYT = table.Column<DateTime>(nullable: false),
AddedtoDB = table.Column<DateTime>(nullable: false),
ChannelPrimaryKey = table.Column<long>(nullable: true),
ChannelPrimaryKey = table.Column<long>(nullable: false),
Description = table.Column<string>(nullable: false),
Duration = table.Column<TimeSpan>(nullable: false),
Tags = table.Column<List<string>>(nullable: false),
ThumbnailURL = table.Column<string>(nullable: false),
Title = table.Column<string>(nullable: false),
YoutubeID = table.Column<string>(nullable: false)
@ -62,7 +52,7 @@ namespace YTManager.Migrations
column: x => x.ChannelPrimaryKey,
principalTable: "Channels",
principalColumn: "PrimaryKey",
onDelete: ReferentialAction.Restrict);
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
@ -73,9 +63,6 @@ namespace YTManager.Migrations
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "Tags");
migrationBuilder.DropTable(
name: "Videos");

View File

@ -6,13 +6,14 @@ using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage;
using Microsoft.EntityFrameworkCore.Storage.Internal;
using System;
using System.Collections.Generic;
using YTManager;
namespace YTManager.Migrations
{
[DbContext(typeof(MediaDB))]
[Migration("20180228202611_added_vid_duration")]
partial class added_vid_duration
[Migration("20180305045634_Tag change")]
partial class Tagchange
{
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
@ -39,6 +40,8 @@ namespace YTManager.Migrations
b.Property<string>("Title")
.IsRequired();
b.Property<List<string>>("UserTags");
b.Property<string>("YoutubeID")
.IsRequired();
@ -47,23 +50,6 @@ namespace YTManager.Migrations
b.ToTable("Channels");
});
modelBuilder.Entity("YTManager.Models.Tag", b =>
{
b.Property<long>("PrimaryKey")
.ValueGeneratedOnAdd();
b.Property<string>("Name")
.IsRequired();
b.Property<long?>("VideoPrimaryKey");
b.HasKey("PrimaryKey");
b.HasIndex("VideoPrimaryKey");
b.ToTable("Tags");
});
modelBuilder.Entity("YTManager.Models.Video", b =>
{
b.Property<long>("PrimaryKey")
@ -80,6 +66,9 @@ namespace YTManager.Migrations
b.Property<TimeSpan>("Duration");
b.Property<List<string>>("Tags")
.IsRequired();
b.Property<string>("ThumbnailURL")
.IsRequired();
@ -96,13 +85,6 @@ namespace YTManager.Migrations
b.ToTable("Videos");
});
modelBuilder.Entity("YTManager.Models.Tag", b =>
{
b.HasOne("YTManager.Models.Video")
.WithMany("Tags")
.HasForeignKey("VideoPrimaryKey");
});
modelBuilder.Entity("YTManager.Models.Video", b =>
{
b.HasOne("YTManager.Models.Channel", "Channel")

View File

@ -0,0 +1,19 @@
using Microsoft.EntityFrameworkCore.Migrations;
using System;
using System.Collections.Generic;
namespace YTManager.Migrations
{
public partial class Tagchange : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
}
protected override void Down(MigrationBuilder migrationBuilder)
{
}
}
}

View File

@ -6,13 +6,14 @@ using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage;
using Microsoft.EntityFrameworkCore.Storage.Internal;
using System;
using System.Collections.Generic;
using YTManager;
namespace YTManager.Migrations
{
[DbContext(typeof(MediaDB))]
[Migration("20180224055707_fix_channel_relationship")]
partial class fix_channel_relationship
[Migration("20180305052950_Made tags required")]
partial class Madetagsrequired
{
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
@ -39,6 +40,9 @@ namespace YTManager.Migrations
b.Property<string>("Title")
.IsRequired();
b.Property<string[]>("UserTags")
.IsRequired();
b.Property<string>("YoutubeID")
.IsRequired();
@ -47,23 +51,6 @@ namespace YTManager.Migrations
b.ToTable("Channels");
});
modelBuilder.Entity("YTManager.Models.Tag", b =>
{
b.Property<long>("PrimaryKey")
.ValueGeneratedOnAdd();
b.Property<string>("Name")
.IsRequired();
b.Property<long?>("VideoPrimaryKey");
b.HasKey("PrimaryKey");
b.HasIndex("VideoPrimaryKey");
b.ToTable("Tags");
});
modelBuilder.Entity("YTManager.Models.Video", b =>
{
b.Property<long>("PrimaryKey")
@ -78,6 +65,11 @@ namespace YTManager.Migrations
b.Property<string>("Description")
.IsRequired();
b.Property<TimeSpan>("Duration");
b.Property<List<string>>("Tags")
.IsRequired();
b.Property<string>("ThumbnailURL")
.IsRequired();
@ -94,13 +86,6 @@ namespace YTManager.Migrations
b.ToTable("Videos");
});
modelBuilder.Entity("YTManager.Models.Tag", b =>
{
b.HasOne("YTManager.Models.Video")
.WithMany("Tags")
.HasForeignKey("VideoPrimaryKey");
});
modelBuilder.Entity("YTManager.Models.Video", b =>
{
b.HasOne("YTManager.Models.Channel", "Channel")

View File

@ -0,0 +1,28 @@
using Microsoft.EntityFrameworkCore.Migrations;
using System;
using System.Collections.Generic;
namespace YTManager.Migrations
{
public partial class Madetagsrequired : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AlterColumn<string[]>(
name: "UserTags",
table: "Channels",
nullable: false,
oldClrType: typeof(List<string>),
oldNullable: true);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.AlterColumn<List<string>>(
name: "UserTags",
table: "Channels",
nullable: true,
oldClrType: typeof(string[]));
}
}
}

View File

@ -0,0 +1,98 @@
// <auto-generated />
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage;
using Microsoft.EntityFrameworkCore.Storage.Internal;
using System;
using YTManager;
namespace YTManager.Migrations
{
[DbContext(typeof(MediaDB))]
[Migration("20180305055427_Fixed videos tags")]
partial class Fixedvideostags
{
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.SerialColumn)
.HasAnnotation("ProductVersion", "2.0.1-rtm-125");
modelBuilder.Entity("YTManager.Models.Channel", b =>
{
b.Property<long>("PrimaryKey")
.ValueGeneratedOnAdd();
b.Property<DateTime>("AddedtoDB");
b.Property<string>("Description")
.IsRequired();
b.Property<DateTime>("Refreshed");
b.Property<string>("ThumbnailURL")
.IsRequired();
b.Property<string>("Title")
.IsRequired();
b.Property<string[]>("UserTags")
.IsRequired();
b.Property<string>("YoutubeID")
.IsRequired();
b.HasKey("PrimaryKey");
b.ToTable("Channels");
});
modelBuilder.Entity("YTManager.Models.Video", b =>
{
b.Property<long>("PrimaryKey")
.ValueGeneratedOnAdd();
b.Property<DateTime>("AddedToYT");
b.Property<DateTime>("AddedtoDB");
b.Property<long>("ChannelPrimaryKey");
b.Property<string>("Description")
.IsRequired();
b.Property<TimeSpan>("Duration");
b.Property<string[]>("Tags")
.IsRequired();
b.Property<string>("ThumbnailURL")
.IsRequired();
b.Property<string>("Title")
.IsRequired();
b.Property<string>("YoutubeID")
.IsRequired();
b.HasKey("PrimaryKey");
b.HasIndex("ChannelPrimaryKey");
b.ToTable("Videos");
});
modelBuilder.Entity("YTManager.Models.Video", b =>
{
b.HasOne("YTManager.Models.Channel", "Channel")
.WithMany("Videos")
.HasForeignKey("ChannelPrimaryKey")
.OnDelete(DeleteBehavior.Cascade);
});
#pragma warning restore 612, 618
}
}
}

View File

@ -0,0 +1,19 @@
using Microsoft.EntityFrameworkCore.Migrations;
using System;
using System.Collections.Generic;
namespace YTManager.Migrations
{
public partial class Fixedvideostags : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
}
protected override void Down(MigrationBuilder migrationBuilder)
{
}
}
}

View File

@ -38,6 +38,9 @@ namespace YTManager.Migrations
b.Property<string>("Title")
.IsRequired();
b.Property<string[]>("UserTags")
.IsRequired();
b.Property<string>("YoutubeID")
.IsRequired();
@ -46,23 +49,6 @@ namespace YTManager.Migrations
b.ToTable("Channels");
});
modelBuilder.Entity("YTManager.Models.Tag", b =>
{
b.Property<long>("PrimaryKey")
.ValueGeneratedOnAdd();
b.Property<string>("Name")
.IsRequired();
b.Property<long?>("VideoPrimaryKey");
b.HasKey("PrimaryKey");
b.HasIndex("VideoPrimaryKey");
b.ToTable("Tags");
});
modelBuilder.Entity("YTManager.Models.Video", b =>
{
b.Property<long>("PrimaryKey")
@ -79,6 +65,9 @@ namespace YTManager.Migrations
b.Property<TimeSpan>("Duration");
b.Property<string[]>("Tags")
.IsRequired();
b.Property<string>("ThumbnailURL")
.IsRequired();
@ -95,13 +84,6 @@ namespace YTManager.Migrations
b.ToTable("Videos");
});
modelBuilder.Entity("YTManager.Models.Tag", b =>
{
b.HasOne("YTManager.Models.Video")
.WithMany("Tags")
.HasForeignKey("VideoPrimaryKey");
});
modelBuilder.Entity("YTManager.Models.Video", b =>
{
b.HasOne("YTManager.Models.Channel", "Channel")

View File

@ -33,6 +33,11 @@ namespace YTManager.Models {
public DateTime Refreshed { get; set; }
// Videos this channel has.
[Required]
public List<Video> Videos { get; set; }
// Tags attached to this channel by user.
[Required]
public string[] UserTags { get; set; }
}
}

View File

@ -1,15 +0,0 @@
using System.ComponentModel.DataAnnotations;
namespace YTManager.Models
{
public class Tag
{
// Uniquie ID for this media type
[Key]
public long PrimaryKey { get; set; }
// Tag Name.
[Required]
public string Name { get; set; }
}
}

View File

@ -42,6 +42,6 @@ namespace YTManager.Models {
// Tag this video applies to.
[Required]
public List<Tag> Tags { get; set; }
public string[] Tags { get; set; }
}
}

View File

@ -29,7 +29,6 @@ namespace YTManager {
services.AddMvc();
services.AddDbContext<MediaDB>(x => x.UseNpgsql(DBStr));
services.AddHangfire(x => x.UsePostgreSqlStorage(DBStr));
services.AddCors(op => op.AddPolicy("AllowAllOrigins", builder => builder.AllowAnyOrigin()));
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
@ -39,6 +38,7 @@ namespace YTManager {
app.UseDefaultFiles();
app.UseStaticFiles();
app.UseCors(b => { b.AllowAnyOrigin(); b.AllowAnyMethod(); });
app.UseMvc();
app.UseHangfireServer();
app.UseHangfireDashboard();

View File

@ -36,7 +36,8 @@ namespace YTManager.Tasks {
YoutubeID = newvid.Id.VideoId,
AddedToYT = newvid.Snippet.PublishedAt.GetValueOrDefault(),
AddedtoDB = DateTime.Now,
ThumbnailURL = newvid.Snippet.Thumbnails.Medium.Url
ThumbnailURL = newvid.Snippet.Thumbnails.Medium.Url,
Tags = new string[] {}
}).ToList();
// Search youtube to get the length and tags for each video.
@ -52,10 +53,8 @@ namespace YTManager.Tasks {
vid.Duration = System.Xml.XmlConvert.ToTimeSpan(contentdetail.ContentDetails.Duration);
// Copy over all the created tags if any tags were provided.
if (contentdetail.Snippet.Tags == null)
vid.Tags = new List<Models.Tag>();
else
vid.Tags = contentdetail.Snippet.Tags.Select(t => new Models.Tag { Name = t.ToLower() }).ToList();
if (contentdetail.Snippet.Tags != null)
vid.Tags = contentdetail.Snippet.Tags.Select(t => t.ToLower()).ToArray();
}
// Send back the parsed vids.
@ -85,7 +84,8 @@ namespace YTManager.Tasks {
YoutubeID = channelID,
AddedtoDB = DateTime.Now,
Refreshed = DateTime.MinValue,
Videos = null
UserTags = new string[]{},
Videos = { }
};
}

View File

@ -35,8 +35,4 @@
<DotNetCliToolReference Include="Microsoft.EntityFrameworkCore.Tools.DotNet" Version="2.0.1" />
</ItemGroup>
<ItemGroup>
<Folder Include="Migrations\" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,15 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" name="viewport" content="width=device-width, initial-scale=1"/>
<title>YT Manager Admin</title>
<!-- Compressed CSS -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/foundation/6.4.3/css/foundation.min.css" />
</head>
<body>
<h2>Admin Page</h2>
<div id="admin_app"></div>
<script src="./../dist/build.js"></script>
</body>
</html>

View File

@ -9320,6 +9320,12 @@
"whet.extend": "0.9.9"
}
},
"sweetalert2": {
"version": "7.13.3",
"resolved": "https://registry.npmjs.org/sweetalert2/-/sweetalert2-7.13.3.tgz",
"integrity": "sha512-cClC9uxXg2oTygUbRhVG0ELVouDbKxzZmjM/pjmPsxbdvDWPmijOq350kvcOIClKezqaG7GsyUSxUbZQWQGZDg==",
"dev": true
},
"symbol-observable": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.0.1.tgz",

View File

@ -11,6 +11,7 @@
"axios": "^0.18.0",
"css-loader": "^0.28.10",
"style-loader": "^0.20.2",
"sweetalert2": "^7.13.3",
"ts-loader": "^4.0.0",
"typescript": "^2.7.2",
"uglifyjs-webpack-plugin": "^1.2.2",

View File

@ -0,0 +1,126 @@
<template>
<div>
<div class="grid-x">
<div class="medium-11 cell"></div>
<div class="medium-1 cell">
<button type="button" class="button input-group" v-on:click='Expanded = !Expanded'>
<i class="fa fa-plus" aria-hidden="true"></i> Add
</button>
</div>
</div>
<transition name="fade">
<form v-cloak v-if="Expanded">
<h2>Add new Channel</h2>
<div class="grid-x">
<div class="input-group medium-6 cell">
<span class="input-group-label">Title</span>
<input class="input-group-field" type="text" placeholder="Title" v-model="Title">
</div>
<div class="input-group medium-1 cell"></div>
<div class="input-group medium-5 cell">
<span class="input-group-label">Channel</span>
<input class="input-group-field" v-bind:class="{ error: !Valid }" type="text"
placeholder="URL" v-model="Channel_Identification_Box">
</div>
</div>
<div class="input-group">
<span class="input-group-label">Description</span>
<input class="input-group-field" type="text" placeholder="Description" v-model="Description">
</div>
<div class="grid-x">
<img class="small-4 cell" :src="Thumbnail" />
<div class="small-7"></div>
<button type="button" style="max-height:50px" class="button input-group small-1 cell" @click="Submit">Submit</button>
</div>
</form>
</transition>
</div>
</template>
<script lang="ts">
import Vue from "vue";
import * as Dyt from "../dumbyt";
import Axios from "axios";
import SA2 from "sweetalert2";
// Vue class for keeping state of the videos.
export default Vue.extend({
data() {
return {
Title: "",
Description: "",
ID: "",
Thumbnail: "",
Valid: false,
Expanded: false,
Channel_Identification_Box : ""
}
},
watch: {
Channel_Identification_Box(newcontents: string) {
this.GetChannelFromYT(newcontents);
}
},
methods: {
Submit() : void {
Dyt.channel_modify(this.ID, Dyt.Modification.Add).then((resp) => {
if (resp == null)
SA2(
"Channel Add Success!",
"The channel has been added succesfully, video contents should be updated shortly.",
"success"
);
else
SA2(
"Channel Add Fail!",
"The channel has not been added due to the following: \n" + resp,
"error"
);
});
// Hide the bars and erase internal state.
this.Expanded = false;
this.Title = "";
this.Description = "";
this.ID = "";
this.Thumbnail = "";
this.Valid = false;
this.Channel_Identification_Box = "";
},
GetChannelFromYT(Channel: string) : void {
// Say it failed first so if we exit early then correctly marked fail.
this.Valid = false;
// Copy over to internal ID box.
this.ID = Channel;
// Remove any potential youtube URL from the field.
const ytchurl = "https://www.youtube.com/channel/";
if (this.ID.startsWith(ytchurl))
this.ID = this.ID.replace(ytchurl, "");
// Check if what remains looks like a youtube channel ID.
if (this.ID.length != "UCyS4xQE6DK4_p3qXQwJQAyA".length)
return;
// Get channel metadata.
const API = 'https://www.googleapis.com/youtube/v3/channels?';
const API_Parts = 'part=snippet%2CcontentDetails%2Cstatistics';
const API_Key = '&key=AIzaSyCuIYkMc5SktlnXRXNaDf2ObX-fQvtWCnQ'
const API_Search_ID = '&id=' + this.ID;
Axios.get(API + API_Parts + API_Search_ID + API_Key).then((resp) => {
this.Description = resp.data.items[0].snippet.description;
this.Title = resp.data.items[0].snippet.title;
this.Thumbnail = resp.data.items[0].snippet.thumbnails.medium.url;
})
.catch(function (error) {
console.log(error);
});
}
}
});
</script>

View File

@ -0,0 +1,44 @@
<template>
<table>
<thead>
<tr>
<th>ID</th>
<th>Title</th>
<th>Description</th>
<th>User Tags</th>
</tr>
</thead>
<tbody>
<tr v-cloak v-for="channel in Channels" :key=channel.ID class="Grid_Small_Text">
<td>{{channel.ID}}</td>
<td>{{channel.Title}}</td>
<td>{{channel.Description}}</td>
<td>{{channel.User_Tags}}</td>
</tr>
</tbody>
</table>
</template>
<script lang="ts">
import Vue from "vue";
import * as Dyt from "../dumbyt";
// Vue class for keeping state of the videos.
export default Vue.extend({
data() {
return {
Channels: Array<Dyt.Channel>()
}
},
mounted(){
Dyt.search_channels("").then(v => this.Channels = v);
},
});
</script>
<style>
.Grid_Small_Text {
font-size: 0.7em;
}
</style>

View File

@ -0,0 +1,44 @@
<template>
<div>
<input id="searchbox0" class="searchbox" type="text" v-model="searchbox_str"
placeholder="Search String goes here, for example: >5m,c++"
/>
</div>
</template>
<script lang="ts">
import Vue from "vue";
import * as _ from "lodash";
import * as Dyt from "../dumbyt";
// Vue class for keeping state of the videos.
export default Vue.extend({
data() {
return {
searchbox_str: "",
Videos: Array<Dyt.Video>()
}
},
// Manually attaching functions to watchers of data.
watch: {
// Searchbox updater.
searchbox_str (s: string) {
var v = _.debounce((s) => {
Dyt.search_videos(s).then(e => this.Videos = e);
}, 400)
v(s);
}
}
});
</script>
<style scoped>
.searchbox {
font-size: 1.2em;
max-width: 70em;
margin-left: auto;
margin-right: auto;
margin-top: 2em;
}
</style>

View File

@ -17,114 +17,15 @@
<script lang="ts">
import Vue from "vue";
import Axios from "axios";
import * as _ from "lodash";
// Wrapper for videos returned from our server.
class Video {
// Title of the video according to youtube.
public Title: string;
// Description of video according to youtube.
public Description: string;
// Youtube ID
public ID: string;
// Thumbnail
public Thumbnail: string;
// What channel made this video.
public Channel: string;
// Duration of the video in seconds.
public Seconds: number;
// Tags relevant to the video.
public Tags: Array<string>;
// Youtube URL of the video.
public URL: string;
// Popuplate this using data from our server.
constructor(v: any){
this.Title = v.title;
this.Description = v.description;
this.ID = v.id;
this.Thumbnail = v.thumbnail;
this.Channel = v.channel;
this.Seconds = v.seconds;
this.Tags = v.tags;
this.URL = "https://www.youtube.com/watch?v=" + v.id;
}
}
async function search_videos(searchstr : string): Promise<Array<Video>> {
// How many chars at most for the description.
const maxcharsdescriptionfield = 100;
// Temporary holder for videos.
let Videos : Array<Video> = [];
// Ask server for videos.
let resp = await Axios.get('http://localhost:62214/api/Videos/search/' + searchstr).catch(e => console.log(e));
// Handle all our nulls.
if (resp == null || resp.data == null)
console.log("server /api/videos/search return is null");
else {
// Parse our videos.
resp.data.forEach((v: any) => {
// Trim description if needed.
if (v.description.length > maxcharsdescriptionfield) {
v.description = v.description.substring(0, maxcharsdescriptionfield) + " ...";
}
// Add it to our array
Videos.push(new Video(v));
});
}
// Send back the resulting videos.
return Promise.resolve(Videos);
}
async function get_videos(): Promise<Array<Video>> {
// How many chars at most for the description.
const maxcharsdescriptionfield = 100;
// Temporary holder for videos.
let Videos : Array<Video> = [];
// Ask server for videos.
let resp = await Axios.get('http://localhost:62214/api/Videos').catch(e => console.log(e));
// Handle all our nulls.
if (resp == null || resp.data == null)
console.log("server /api/videos return is null");
else {
// Parse our videos.
resp.data.forEach((v: any) => {
// Trim description if needed.
if (v.description.length > maxcharsdescriptionfield) {
v.description = v.description.substring(0, maxcharsdescriptionfield) + " ...";
}
// Add it to our array
Videos.push(new Video(v));
});
}
// Send back the resulting videos.
return Promise.resolve(Videos);
}
import * as Dyt from "../dumbyt";
// Vue class for keeping state of the videos.
export default Vue.extend({
data() {
return {
searchbox_str: "",
Videos: Array<Video>()
Videos: Array<Dyt.Video>()
}
},
@ -132,19 +33,15 @@ export default Vue.extend({
watch: {
// Searchbox updater.
searchbox_str (s: string) {
if (s.trim().length > 0){
var v = _.debounce((s) => {
search_videos(s).then(e => this.Videos = e);
})
v(s);
} else {
get_videos().then(e => this.Videos = e );
}
var v = _.debounce((s) => {
Dyt.search_videos(s).then(e => this.Videos = e);
})
v(s);
}
},
// Ugh, vue doesn't have async support for computed, wow ...
mounted() { get_videos().then(e => this.Videos = e ); }
mounted() { Dyt.search_videos("").then(e => this.Videos = e ); }
});
</script>

View File

@ -0,0 +1,47 @@
<template>
<table>
<thead>
<tr>
<th>ID</th>
<th>Title</th>
<th>Channel</th>
<th>Seconds</th>
<th>Tags</th>
</tr>
</thead>
<tbody>
<tr v-cloak v-for="video in Videos" :key=video.ID class="Grid_Small_Text">
<td>{{video.ID}}</td>
<td>{{video.Title}}</td>
<td>{{video.Channel}}</td>
<td>{{video.Seconds}}</td>
<td>{{video.Tags}}</td>
</tr>
</tbody>
</table>
</template>
<script lang="ts">
import Vue from "vue";
import * as Dyt from "../dumbyt";
// Vue class for keeping state of the videos.
export default Vue.extend({
data() {
return {
Videos: Array<Dyt.Video>()
}
},
mounted (){
Dyt.search_videos("").then(v => this.Videos = v);
},
});
</script>
<style>
.Grid_Small_Text {
font-size: 0.7em;
}
</style>

View File

@ -0,0 +1,210 @@
import Axios from "axios";
// Base URL for API queries.
const API_BASE_URL = "http://localhost:62214/api";
// How many chars at most for the description.
const max_description_length = 100;
// Wrapper for channels returned from our server.
export class Channel {
// Title of the channel according to youtube.
public Title: string;
// Description of channel according to youtube.
public Description: string;
// Youtube ID
public ID: string;
// Tags given to the video.
public User_Tags: Array<string>;
// ID's belonging to the channel.
public Video_IDs: Array<string>;
// Popuplate this using data from our server.
constructor(c : any){
if (c == null) {
this.Title = "NULL"
this.Description = "NULL"
this.ID = "NULL"
this.User_Tags = ["NULL", "NULL", "NULL"]
this.Video_IDs = ["NULL", "NULL"]
} else {
this.Title = c.title;
this.Description = c.description;
this.ID = c.id;
this.User_Tags = c.user_Tags;
this.Video_IDs = c.video_IDs;
}
}
}
// Wrapper for videos returned from our server.
export class Video {
// Title of the video according to youtube.
public Title: string;
// Description of video according to youtube.
public Description: string;
// Youtube ID
public ID: string;
// Thumbnail
public Thumbnail: string;
// What channel made this video.
public Channel: string;
// Duration of the video in seconds.
public Seconds: number;
// Tags relevant to the video.
public Tags: Array<string>;
// Youtube URL of the video.
public URL: string;
// Popuplate this using data from our server.
constructor(v: any){
if (v == null){
this.Title = "NULL";
this.Description = "NULL";
this.ID = "NULL";
this.Thumbnail = "NULL";
this.Channel = "NULL";
this.Seconds = 9999999;
this.Tags = ["NULL", "NULL"];
this.URL = "NULL";
} else {
this.Title = v.title;
this.Description = v.description;
this.ID = v.id;
this.Thumbnail = v.thumbnail;
this.Channel = v.channel;
this.Seconds = v.seconds;
this.Tags = v.tags;
this.URL = "https://www.youtube.com/watch?v=" + v.id;
}
}
}
// Types of modifications which can be applied to various models.
export enum Modification {
Add, Delete, Refresh
}
// Change a channel state.
export async function channel_modify(youtubeID : string, modify : Modification): Promise<void | string> {
switch (modify){
case Modification.Add: {
let URL = API_BASE_URL + '/Channels/' + youtubeID;
let resp = await Axios.post(URL).catch((error) => {
if (error.response)
return error.response.data;
else if (error.request)
return error.request;
else
return "Axios request failed for unkown reason.";
});
if (typeof resp == "string")
return resp;
break;
}
case Modification.Delete: {
let URL = API_BASE_URL + '/Channels/' + youtubeID;
let resp = await Axios.delete(URL).catch(e => console.log(e));
if (resp != null){
if (resp.status != 200)
return resp.data();
else
return;
} else {
return "Response is null";
}
}
case Modification.Refresh: {
let URL = API_BASE_URL + '/Channels/Update/' + youtubeID;
let resp = await Axios.post(URL).catch(e => console.log(e));
if (resp != null){
if (resp.status != 200)
return resp.data();
else
return;
} else {
return "Response is null";
}
}
default: {
console.log("Unknown request type, error ...");
break;
}
}
}
// Delete a video.
export async function video_delete(youtubeID : string) : Promise<void> {
let URL = API_BASE_URL + '/Videos/' + youtubeID;
let resp = await Axios.delete(URL).catch(e => console.log(e));
if ((resp == null) || (resp.data == null)){
console.log("Video delete via " + URL + " FAIL");
};
}
// Search for channels based on a search string.
export async function search_channels(searchstr : string): Promise<Array<Channel>> {
// Temporary holder for data.
let Channels : Array<Channel> = [];
// Ask server for data.
let resp = await Axios.get(API_BASE_URL + '/Channels/' + searchstr).catch(e => console.log(e));
// Handle all our nulls.
if (resp == null || resp.data == null)
console.log("server /api/Channels/" + searchstr + " return is null");
else {
// Parse our videos.
resp.data.forEach((c: any) => {
// Trim description if needed.
if (c.description.length > max_description_length) {
c.description = c.description.substring(0, max_description_length) + " ...";
}
// Add it to our array
Channels.push(new Channel(c));
});
}
// Send back the resulting videos.
return Promise.resolve(Channels);
}
// Search for videos based on a search string.
export async function search_videos(searchstr : string): Promise<Array<Video>> {
// Temporary holder for videos.
let Videos : Array<Video> = [];
// Ask server for videos.
let resp = await Axios.get(API_BASE_URL + '/Videos/' + searchstr).catch(e => console.log(e));
// Handle all our nulls.
if (resp == null || resp.data == null)
console.log("server /videos/ return is null");
else {
// Parse our videos.
resp.data.forEach((v: any) => {
// Trim description if needed.
if (v.description.length > max_description_length) {
v.description = v.description.substring(0, max_description_length) + " ...";
}
// Add it to our array
Videos.push(new Video(v));
});
}
// Send back the resulting videos.
return Promise.resolve(Videos);
}

View File

@ -1,16 +1,40 @@
import Vue from "vue";
import VGC from "./components/Video_Grid.vue";
import SCH from "./components/Search.vue";
import VTBL from "./components/Video_Table.vue";
import CTBL from "./components/Channel_Table.vue";
import CADD from "./components/Channel_Add.vue";
import './index.css';
let v = new Vue({
let MainApp = new Vue({
el: "#app",
template: `
<div>
<VGC/>
</div>
`,
data: { name: "something" },
components: {
VGC
}
});
let AdminApp = new Vue({
el: "#admin_app",
template: `
<div>
<SCH style="max-width:800px; padding-left:100px;" />
<CADD/>
<CTBL/>
<VTBL/>
</div>
`,
components: {
SCH,
CADD,
CTBL,
VTBL
},
methods: {
}
});