HUGE arch changes to make searching work right
This commit is contained in:
parent
96a26ce30d
commit
9f5fb78835
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
{
|
||||
|
@ -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;
|
||||
@ -93,10 +67,26 @@ namespace YTManager.Controllers {
|
||||
public List<String> Remaining_Tokens;
|
||||
}
|
||||
|
||||
// Searches for videos using the specified tag(s).
|
||||
[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("search/{searchstr}")]
|
||||
[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);
|
||||
|
@ -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){ }
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
@ -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");
|
||||
|
@ -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")
|
19
YTManager/Migrations/20180305045634_Tag change.cs
Normal file
19
YTManager/Migrations/20180305045634_Tag change.cs
Normal 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)
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
}
|
@ -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")
|
28
YTManager/Migrations/20180305052950_Made tags required.cs
Normal file
28
YTManager/Migrations/20180305052950_Made tags required.cs
Normal 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[]));
|
||||
}
|
||||
}
|
||||
}
|
98
YTManager/Migrations/20180305055427_Fixed videos tags.Designer.cs
generated
Normal file
98
YTManager/Migrations/20180305055427_Fixed videos tags.Designer.cs
generated
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
19
YTManager/Migrations/20180305055427_Fixed videos tags.cs
Normal file
19
YTManager/Migrations/20180305055427_Fixed videos tags.cs
Normal 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)
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
}
|
@ -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")
|
||||
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
@ -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; }
|
||||
}
|
||||
}
|
@ -42,6 +42,6 @@ namespace YTManager.Models {
|
||||
|
||||
// Tag this video applies to.
|
||||
[Required]
|
||||
public List<Tag> Tags { get; set; }
|
||||
public string[] Tags { get; set; }
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
|
@ -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 = { }
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -35,8 +35,4 @@
|
||||
<DotNetCliToolReference Include="Microsoft.EntityFrameworkCore.Tools.DotNet" Version="2.0.1" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="Migrations\" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
15
YTManager/frontend/admin/index.html
Normal file
15
YTManager/frontend/admin/index.html
Normal 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>
|
6
YTManager/frontend/package-lock.json
generated
6
YTManager/frontend/package-lock.json
generated
@ -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",
|
||||
|
@ -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",
|
||||
|
126
YTManager/frontend/src/components/Channel_Add.vue
Normal file
126
YTManager/frontend/src/components/Channel_Add.vue
Normal 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>
|
44
YTManager/frontend/src/components/Channel_Table.vue
Normal file
44
YTManager/frontend/src/components/Channel_Table.vue
Normal 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>
|
44
YTManager/frontend/src/components/Search.vue
Normal file
44
YTManager/frontend/src/components/Search.vue
Normal 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>
|
@ -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);
|
||||
Dyt.search_videos(s).then(e => this.Videos = e);
|
||||
})
|
||||
v(s);
|
||||
} else {
|
||||
get_videos().then(e => this.Videos = e );
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// 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>
|
||||
|
||||
|
47
YTManager/frontend/src/components/Video_Table.vue
Normal file
47
YTManager/frontend/src/components/Video_Table.vue
Normal 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>
|
||||
|
210
YTManager/frontend/src/dumbyt.ts
Normal file
210
YTManager/frontend/src/dumbyt.ts
Normal 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);
|
||||
}
|
@ -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: {
|
||||
|
||||
}
|
||||
});
|
||||
|
Loading…
Reference in New Issue
Block a user