Add project files.

This commit is contained in:
hak8or 2017-09-01 04:55:02 -04:00
parent 70d055282c
commit ae782025f6
22 changed files with 1244 additions and 0 deletions

25
YTManager.sln Normal file
View File

@ -0,0 +1,25 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 15
VisualStudioVersion = 15.0.26730.12
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "YTManager", "YTManager\YTManager.csproj", "{14F56CC1-67A1-477F-817C-A0A7B55AE954}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{14F56CC1-67A1-477F-817C-A0A7B55AE954}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{14F56CC1-67A1-477F-817C-A0A7B55AE954}.Debug|Any CPU.Build.0 = Debug|Any CPU
{14F56CC1-67A1-477F-817C-A0A7B55AE954}.Release|Any CPU.ActiveCfg = Release|Any CPU
{14F56CC1-67A1-477F-817C-A0A7B55AE954}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {DB293C09-A2E4-4179-ADA9-BAE2093F425A}
EndGlobalSection
EndGlobal

View File

@ -0,0 +1,95 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Newtonsoft.Json.Linq;
using Microsoft.EntityFrameworkCore;
namespace YTManager.Controllers {
[Produces("application/json")]
[Route("api/Admin")]
public class AdminController : Controller {
// POST api/Admin/refreshytvids
[HttpPost("refreshytvids")]
public void Post() {
Hangfire.BackgroundJob.Enqueue(() => YTManager.Tasks.FetchVideos.run());
}
}
[Produces("application/json")]
[Route("api/Channels")]
public class ChannelsController : Controller {
private readonly MediaDB _context;
public ChannelsController(MediaDB context)
{
_context = context;
}
// GET api/Channels
[HttpGet]
public IEnumerable<Models.Channel> Get() {
return _context.Channels.ToList();
}
// GET api/Channels/5
[HttpGet("{id}")]
public Models.Channel Get(int id) {
return _context.Channels.Single(m => m.ChannelId == id);
}
// GET: api/Channels/sdfs6DFS65f/Videos (using YouTube channel ID)
[HttpGet("{id}/Videos")]
public async Task<IActionResult> GetVideos([FromRoute] string YTchannelID) {
if (!ModelState.IsValid)
return BadRequest(ModelState);
// Verify the channel exists.
var Chan = await _context.Channels.SingleOrDefaultAsync(c => c.YTChannelID == YTchannelID);
if (Chan == null)
return NotFound();
// Send back the found stuff.
return Ok(_context.Entry(Chan).Collection(c => c.Videos).LoadAsync());
}
// POST api/values
[HttpPost]
public IActionResult Post([FromBody]JObject value) {
// Get the channel out of our json body.
Models.Channel posted = value.ToObject<Models.Channel>();
// Verify items aren't empty.
if ((posted.YTChannelID.Length == 0) || (posted.Description.Length == 0) || (posted.Title.Length == 0))
return BadRequest();
// Verify the channel doesn't already exist.
if (_context.Channels.Any(c => c.YTChannelID == posted.YTChannelID))
return BadRequest();
// Seems good, so add it.
_context.Channels.Add(posted);
_context.SaveChanges();
return Ok();
}
// PUT api/values/5
[HttpPut("{id}")]
public void Put(int id, [FromBody]JObject value)
{
Models.Channel posted = value.ToObject<Models.Channel>();
posted.ChannelId = id; // Ensure an id is attached
_context.Channels.Update(posted);
_context.SaveChanges();
}
// DELETE api/values/5
[HttpDelete("{id}")]
public void Delete(int id)
{
_context.Remove(_context.Channels.Single(m => m.ChannelId == id));
_context.SaveChanges();
}
}
}

View File

@ -0,0 +1,126 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using YTManager;
using YTManager.Models;
namespace YTManager.Controllers
{
[Produces("application/json")]
[Route("api/Videos")]
public class VideosController : Controller
{
private readonly MediaDB _context;
public VideosController(MediaDB context)
{
_context = context;
}
// GET: api/Videos
[HttpGet]
public IEnumerable<Video> GetVideos()
{
return _context.Videos;
}
// GET: api/Videos/5
[HttpGet("{id}")]
public async Task<IActionResult> GetVideo([FromRoute] long id)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
var video = await _context.Videos.SingleOrDefaultAsync(m => m.VideoId == id);
if (video == null)
{
return NotFound();
}
return Ok(video);
}
// PUT: api/Videos/5
[HttpPut("{id}")]
public async Task<IActionResult> PutVideo([FromRoute] long id, [FromBody] Video video)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
if (id != video.VideoId)
{
return BadRequest();
}
_context.Entry(video).State = EntityState.Modified;
try
{
await _context.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
if (!VideoExists(id))
{
return NotFound();
}
else
{
throw;
}
}
return NoContent();
}
// POST: api/Videos
[HttpPost]
public async Task<IActionResult> PostVideo([FromBody] Video video)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
_context.Videos.Add(video);
await _context.SaveChangesAsync();
return CreatedAtAction("GetVideo", new { id = video.VideoId }, video);
}
// DELETE: api/Videos/5
[HttpDelete("{id}")]
public async Task<IActionResult> DeleteVideo([FromRoute] long id)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
var video = await _context.Videos.SingleOrDefaultAsync(m => m.VideoId == id);
if (video == null)
{
return NotFound();
}
_context.Videos.Remove(video);
await _context.SaveChangesAsync();
return Ok(video);
}
private bool VideoExists(long id)
{
return _context.Videos.Any(e => e.VideoId == id);
}
}
}

21
YTManager/MediaDB.cs Normal file
View File

@ -0,0 +1,21 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
namespace YTManager
{
public class MediaDB : DbContext
{
public DbSet<Models.Channel> Channels { get; set; }
public DbSet<Models.Video> Videos { get; set; }
public MediaDB(DbContextOptions<MediaDB> options) :base(options){ }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
base.OnConfiguring(optionsBuilder);
}
}
}

View File

@ -0,0 +1,86 @@
// <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("20170901083156_initial")]
partial class initial
{
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.SerialColumn)
.HasAnnotation("ProductVersion", "2.0.0-rtm-26452");
modelBuilder.Entity("YTManager.Models.Channel", b =>
{
b.Property<long>("ChannelId")
.ValueGeneratedOnAdd();
b.Property<string>("Description")
.IsRequired();
b.Property<string>("ThumbnailURL")
.IsRequired();
b.Property<string>("Title")
.IsRequired();
b.Property<string>("YTChannelID")
.IsRequired();
b.HasKey("ChannelId");
b.ToTable("Channels");
});
modelBuilder.Entity("YTManager.Models.Video", b =>
{
b.Property<long>("VideoId")
.ValueGeneratedOnAdd();
b.Property<DateTime>("AddedtoDB");
b.Property<long>("ChannelId");
b.Property<string>("Description")
.IsRequired();
b.Property<string>("ThumbnailURL")
.IsRequired();
b.Property<string>("Title")
.IsRequired();
b.Property<DateTime>("Uploaded");
b.Property<string>("YTVideoID")
.IsRequired();
b.HasKey("VideoId");
b.HasIndex("ChannelId");
b.ToTable("Videos");
});
modelBuilder.Entity("YTManager.Models.Video", b =>
{
b.HasOne("YTManager.Models.Channel", "Channel")
.WithMany("Videos")
.HasForeignKey("ChannelId")
.OnDelete(DeleteBehavior.Cascade);
});
#pragma warning restore 612, 618
}
}
}

View File

@ -0,0 +1,68 @@
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;
using System;
using System.Collections.Generic;
namespace YTManager.Migrations
{
public partial class initial : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "Channels",
columns: table => new
{
ChannelId = table.Column<long>(type: "int8", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.SerialColumn),
Description = table.Column<string>(type: "text", nullable: false),
ThumbnailURL = table.Column<string>(type: "text", nullable: false),
Title = table.Column<string>(type: "text", nullable: false),
YTChannelID = table.Column<string>(type: "text", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Channels", x => x.ChannelId);
});
migrationBuilder.CreateTable(
name: "Videos",
columns: table => new
{
VideoId = table.Column<long>(type: "int8", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.SerialColumn),
AddedtoDB = table.Column<DateTime>(type: "timestamp", nullable: false),
ChannelId = table.Column<long>(type: "int8", nullable: false),
Description = table.Column<string>(type: "text", nullable: false),
ThumbnailURL = table.Column<string>(type: "text", nullable: false),
Title = table.Column<string>(type: "text", nullable: false),
Uploaded = table.Column<DateTime>(type: "timestamp", nullable: false),
YTVideoID = table.Column<string>(type: "text", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Videos", x => x.VideoId);
table.ForeignKey(
name: "FK_Videos_Channels_ChannelId",
column: x => x.ChannelId,
principalTable: "Channels",
principalColumn: "ChannelId",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_Videos_ChannelId",
table: "Videos",
column: "ChannelId");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "Videos");
migrationBuilder.DropTable(
name: "Channels");
}
}
}

View File

@ -0,0 +1,85 @@
// <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))]
partial class MediaDBModelSnapshot : ModelSnapshot
{
protected override void BuildModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.SerialColumn)
.HasAnnotation("ProductVersion", "2.0.0-rtm-26452");
modelBuilder.Entity("YTManager.Models.Channel", b =>
{
b.Property<long>("ChannelId")
.ValueGeneratedOnAdd();
b.Property<string>("Description")
.IsRequired();
b.Property<string>("ThumbnailURL")
.IsRequired();
b.Property<string>("Title")
.IsRequired();
b.Property<string>("YTChannelID")
.IsRequired();
b.HasKey("ChannelId");
b.ToTable("Channels");
});
modelBuilder.Entity("YTManager.Models.Video", b =>
{
b.Property<long>("VideoId")
.ValueGeneratedOnAdd();
b.Property<DateTime>("AddedtoDB");
b.Property<long>("ChannelId");
b.Property<string>("Description")
.IsRequired();
b.Property<string>("ThumbnailURL")
.IsRequired();
b.Property<string>("Title")
.IsRequired();
b.Property<DateTime>("Uploaded");
b.Property<string>("YTVideoID")
.IsRequired();
b.HasKey("VideoId");
b.HasIndex("ChannelId");
b.ToTable("Videos");
});
modelBuilder.Entity("YTManager.Models.Video", b =>
{
b.HasOne("YTManager.Models.Channel", "Channel")
.WithMany("Videos")
.HasForeignKey("ChannelId")
.OnDelete(DeleteBehavior.Cascade);
});
#pragma warning restore 612, 618
}
}
}

View File

@ -0,0 +1,32 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using System.ComponentModel.DataAnnotations;
namespace YTManager.Models {
public class Channel {
// Uniquie ID for this media type
[Key]
public long ChannelId { get; set; }
// Title of the media
[Required]
public string Title { get; set; }
// Short description of the media
[Required]
public string Description { get; set; }
// Thumbnail link
[Required]
public string ThumbnailURL { get; set; }
// Youtube Channel ID
[Required]
public string YTChannelID { get; set; }
// Videos this channel has.
public List<Video> Videos { get; set; }
}
}

41
YTManager/Models/Video.cs Normal file
View File

@ -0,0 +1,41 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using System.ComponentModel.DataAnnotations;
namespace YTManager.Models {
public class Video {
// Uniquie ID for this media type
[Key]
public long VideoId { get; set; }
// Title of the media
[Required]
public string Title { get; set; }
// Short description of the media
[Required]
public string Description { get; set; }
// Youtube Video ID
[Required]
public string YTVideoID { get; set; }
// Thumbnail link
[Required]
public string ThumbnailURL { get; set; }
// Date video was uploaded to YT
[Required]
public DateTime Uploaded { get; set; }
// Date added to database
[Required]
public DateTime AddedtoDB { get; set; }
// Refer back to what channel this video belongs to.
public long ChannelId { get; set; }
public Channel Channel { get; set; }
}
}

26
YTManager/Program.cs Normal file
View File

@ -0,0 +1,26 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
namespace YTManager
{
public class Program
{
public static void Main(string[] args)
{
BuildWebHost(args).Run();
}
public static IWebHost BuildWebHost(string[] args) =>
WebHost.CreateDefaultBuilder(args)
.UseContentRoot(Directory.GetCurrentDirectory())
.UseStartup<Startup>()
.Build();
}
}

View File

@ -0,0 +1,28 @@
{
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:62213/",
"sslPort": 0
}
},
"profiles": {
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"launchUrl": "api/values",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"YTManager": {
"commandName": "Project",
"launchUrl": "api/values",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"applicationUrl": "http://localhost:62214/"
}
}
}

View File

@ -0,0 +1,39 @@

ASP.NET MVC core dependencies have been added to the project.
(These dependencies include packages required to enable scaffolding)
However you may still need to do make changes to your project.
1. Suggested changes to Startup class:
1.1 Add a constructor:
public IConfiguration Configuration { get; }
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
1.2 Add MVC services:
public void ConfigureServices(IServiceCollection services)
{
// Add framework services.
services.AddMvc();
}
1.3 Configure web app to use use Configuration and use MVC routing:
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseStaticFiles();
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
}

49
YTManager/Startup.cs Normal file
View File

@ -0,0 +1,49 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Hangfire;
using Hangfire.PostgreSql;
using Microsoft.EntityFrameworkCore;
namespace YTManager {
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();
string constr = "Host=home.hak8or.com;Database=postgres;Username=postgres;Password=mysecretpassword";
services.AddHangfire(x => x.UsePostgreSqlStorage(constr));
services.AddDbContext<MediaDB>(options => options.UseNpgsql(constr));
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseDefaultFiles();
app.UseStaticFiles();
app.UseMvc();
app.UseHangfireServer();
app.UseHangfireDashboard();
}
}
}

View File

@ -0,0 +1,60 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Google.Apis.YouTube.v3;
using Microsoft.EntityFrameworkCore;
namespace YTManager.Tasks
{
public class FetchVideos
{
public static void run()
{
Console.WriteLine("Startnig job ...");
// YT API access key
var youtubeService = new YouTubeService(new Google.Apis.Services.BaseClientService.Initializer()
{
ApiKey = "AIzaSyCuIYkMc5SktlnXRXNaDf2ObX-fQvtWCnQ",
ApplicationName = "testingapppp"
});
// Get all the channels to update.
var ops = new DbContextOptionsBuilder<MediaDB>();
string constr = "Host=home.hak8or.com;Database=postgres;Username=postgres;Password=mysecretpassword";
ops.UseNpgsql(constr);
using (var dbcontext = new MediaDB(ops.Options)) {
var channels = dbcontext.Channels.ToList();
// Get all the most recent videos for each channel.
channels.ForEach(ch => {
// Get channel videos from youtube.
var query = youtubeService.Activities.List("snippet");
query.ChannelId = ch.YTChannelID;
var response = query.Execute();
// Get all videos which aren't already in the DB.
var notindb = response.Items
.Where(i => !dbcontext.Videos.Any(dbvid => dbvid.YTVideoID != i.Id))
.Select(newvid =>
new Models.Video {
Title = newvid.Snippet.Title,
Description = newvid.Snippet.Description,
YTVideoID = newvid.Id,
Uploaded = newvid.Snippet.PublishedAt.GetValueOrDefault(),
AddedtoDB = DateTime.Now,
Channel = ch,
ThumbnailURL = newvid.Snippet.Thumbnails.Medium.Url
}).ToList();
// Add all videos not already in the database over.
notindb.ForEach(newvid => dbcontext.Videos.Add(newvid));
// And save since we are done.
dbcontext.SaveChanges();
});
}
}
}
}

View File

@ -0,0 +1,31 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>netcoreapp2.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Google.Apis.YouTube.v3" Version="1.29.0.760" />
<PackageReference Include="Hangfire" Version="1.6.15" />
<PackageReference Include="Hangfire.AspNetCore" Version="1.6.15" />
<PackageReference Include="Hangfire.PostgreSql" Version="1.4.8.1" />
<PackageReference Include="Microsoft.AspNetCore.All" Version="2.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="2.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="2.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="2.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="2.0.0" />
<PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="2.0.0" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="2.0.0" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL.Design" Version="1.1.1" />
</ItemGroup>
<ItemGroup>
<DotNetCliToolReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Tools" Version="2.0.0" />
<DotNetCliToolReference Include="Microsoft.EntityFrameworkCore.Tools.DotNet" Version="2.0.0" />
</ItemGroup>
<ItemGroup>
<Folder Include="Migrations\" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,10 @@
{
"Logging": {
"IncludeScopes": false,
"LogLevel": {
"Default": "Debug",
"System": "Information",
"Microsoft": "Information"
}
}
}

View File

@ -0,0 +1,15 @@
{
"Logging": {
"IncludeScopes": false,
"Debug": {
"LogLevel": {
"Default": "Warning"
}
},
"Console": {
"LogLevel": {
"Default": "Warning"
}
}
}
}

View File

@ -0,0 +1,109 @@
<!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" />
<link rel="stylesheet" type="text/css" href="index.css">
</head>
<body>
<h1>Dumb YT Manager</h1>
<p>Youtube banned my account and refuses to say why, taking all my subscribed channels with it. This is a simple scrubscribed channel manager, showing recent releases from each channel and finally proper sub-catagory functionality!</p>
<hr />
<h2>Subscribed Channels</h2>
<table id="tbody-0">
<thead>
<tr>
<th>YT Channel ID</th>
<th>Title</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr v-cloak v-for="entry in entries">
<td class="tinytext12px">{{entry.ytChannelID}}</td>
<td class="tinytext12px">{{entry.title}}</td>
<td class="tinytext12px">{{entry.description}}</td>
</tr>
</tbody>
</table>
<div id="addchanel-0">
<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 of Entry" v-model="entry.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: fail }" type="text"
@blur="newurl" placeholder="URL of Entry" v-model="entry.yTChannelID">
</div>
</div>
<div class="input-group">
<span class="input-group-label">Description</span>
<input class="input-group-field" type="text" placeholder="Description of Entry" v-model="entry.description">
</div>
<div class="grid-x">
<img class="small-4 cell" :src="entry.thumbnailURL" />
<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>
<hr />
<h2>Videos in DB</h2>
<table id="videostb-0">
<thead>
<tr>
<th>Title</th>
<th>Description</th>
<th>Youtube Video ID</th>
<th>ID</th>
</tr>
</thead>
<tbody>
<tr v-cloak v-for="video in Videos">
<td class="tinytext12px">{{video.title}}</td>
<td class="tinytext12px">{{video.description}}</td>
<td class="tinytext12px">{{video.ytVideoID}}</td>
<td class="tinytext12px">{{video.videoId}}</td>
</tr>
</tbody>
</table>
<!-- Compressed JavaScript -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.1.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/foundation/6.4.3/js/foundation.min.js"></script>
<!-- For doing REST based API stuff. -->
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
<!-- Some icons -->
<script src="https://use.fontawesome.com/91af8ab4ba.js"></script>
<!-- Good ole Vue :D -->
<script src="https://unpkg.com/vue"></script>
<!-- All of my custom JS. Put here so body loads before Vue based stuff loads/attempts to bind. -->
<script src="admin.js"></script>
</body>
</html>

139
YTManager/wwwroot/admin.js Normal file
View File

@ -0,0 +1,139 @@
$(document).foundation();
// How many chars at most for the description.
var maxcharstablefield = 200;
// Bind to the form for submitting a new channel
var addchanel0 = new Vue({
el: '#addchanel-0',
data: {
entry: {
title: "",
description: "",
yTChannelID: "",
thumbnailURL: "",
},
fail: false,
expanded: false
},
methods: {
submit: function (event) {
// Send the channel to our API and request tables be updated.
var d = this.entry;
axios.post('/api/Channels', d)
.then(function (response) {
this.entry.title = "";
this.entry.description = "";
this.entry.yTChannelID = "";
this.entry.thumbnailURL = "";
this.expanded = false;
tbody0.retrieve();
axios.post('/api/admin/refreshytvids');
setTimeout(() => videostb.retrieve(), 1000);
}.bind(this))
.catch(function (error) {
console.log(error);
});
},
// Handle when user put in a new URL (verification and thumbnail fetch)
newurl: function (event) {
// Remove any potential youtube URL from the field.
var ytchurl = "https://www.youtube.com/channel/";
if (this.entry.yTChannelID.startsWith(ytchurl)) {
this.entry.yTChannelID = this.entry.yTChannelID.replace(ytchurl, "");
}
// Check if what remains looks like a youtube channel ID.
if (this.entry.yTChannelID.length != "UCyS4xQE6DK4_p3qXQwJQAyA".length) {
this.fail = true;
return;
}
this.fail = false;
// Get the Channel URL
var basestr = 'https://www.googleapis.com/youtube/v3/channels?part=snippet%2CcontentDetails%2Cstatistics';
var apikey = '&key=AIzaSyCuIYkMc5SktlnXRXNaDf2ObX-fQvtWCnQ '
var channel = '&id=' + addchanel0.entry.yTChannelID;
axios.get(basestr + channel + apikey)
.then(function (response) {
// Only attempt to fill the UI text boxes if they are empty.
if (this.entry.description.length == 0) {
this.entry.description = response.data.items[0].snippet.description;
}
if (this.entry.title.length == 0) {
this.entry.title = response.data.items[0].snippet.title;
}
// Get client to load the thumbnail
this.entry.thumbnailURL = response.data.items[0].snippet.thumbnails.medium.url;
}.bind(this))
.catch(function (error) {
console.log(error);
});
}
}
});
// Bind to our table of entries.
var tbody0 = new Vue({
el: '#tbody-0',
data: {
entries: [""]
},
methods: {
retrieve: function (event) {
axios.get('/api/Channels')
.then(function (response) {
// Wipe out all old entries.
this.entries = [];
// And fill it with all the retrieved entries.
response.data.forEach(function (x) {
if (x.description.length > maxcharstablefield) {
x.description = x.description.substring(0, maxcharstablefield) + " ...";
}
this.entries.push(x);
}.bind(this));
}.bind(this))
.catch(function (error) {
console.log(error);
});
}
}
});
// Grid if images.
var videostb = new Vue({
el: '#videostb-0',
data: {
Videos: []
},
methods: {
// Get new videos from the web api.
retrieve: function (event) {
axios.get('/api/Videos')
.then(function (response) {
// And fill it with all the retrieved entries.
response.data.forEach(function (x) {
// Trim description if needed.
if (x.description.length > maxcharstablefield) {
x.description = x.description.substring(0, maxcharstablefield) + " ...";
}
// Add it to our array
this.Videos.push(x);
}.bind(this));
}.bind(this))
.catch(function (error) {
console.log(error);
});
}
}
});
window.addEventListener('load', function () {
tbody0.retrieve();
videostb.retrieve();
});

View File

@ -0,0 +1,39 @@
body {
max-width: 1280px;
margin: auto;
line-height: 1.6;
font-size: 18px;
color: #444;
background-color: #F8F8F8;
}
.error {
background-color: red;
}
#addnewchannelform {
width: inherit;
margin: auto;
}
#vidholder-0 {
}
.tinytext10px{ font-size: 10px; }
.tinytext12px{ font-size: 12px; }
.tinytext14px{ font-size: 14px; }
.tinytext16px{ font-size: 16px; }
.tinytext18px{ font-size: 18px; }
[v-cloak] {
display: none;
}
.fade-enter-active, .fade-leave-active {
transition: opacity 0.75s;
}
.fade-enter, .fade-leave-to /* .fade-leave-active below version 2.1.8 */ {
opacity: 0;
}

View File

@ -0,0 +1,47 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" name="viewport" content="width=device-width, initial-scale=1"/>
<title>YT Manager</title>
<!-- Compressed CSS -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/foundation/6.4.3/css/foundation.min.css" />
<link rel="stylesheet" type="text/css" href="index.css">
</head>
<body>
<h1>Dumb YT Manager</h1>
<p>Youtube banned my account and refuses to say why, taking all my subscribed channels with it. This is a simple scrubscribed channel manager, showing recent releases from each channel and finally proper sub-catagory functionality!</p>
<hr />
<h2>Most Recent Videos</h2>
<div v-cloak id="vidholder-0">
<div class="grid-x grid-margin-x imgrow" v-for="row in Videos.Rows">
<div class="card medium-2 large-1 cell" v-for="video in row.Columns">
<div class="card-divider">
<h6>{{video.title}}</h6>
</div>
<img :src="video.thumbnailURL">
<div class="card-section">
<p class="tinytext14px">{{video.description}}</p>
</div>
</div>
</div>
</div>
<!-- Compressed JavaScript -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.1.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/foundation/6.4.3/js/foundation.min.js"></script>
<!-- For doing REST based API stuff. -->
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
<!-- Some icons -->
<script src="https://use.fontawesome.com/91af8ab4ba.js"></script>
<!-- Good ole Vue :D -->
<script src="https://unpkg.com/vue"></script>
<!-- All of my custom JS. Put here so body loads before Vue based stuff loads/attempts to bind. -->
<script src="index.js"></script>
</body>
</html>

View File

@ -0,0 +1,73 @@
$(document).foundation();
// How many chars at most for the description.
var maxcharsdescriptionfield = 100;
// Grid if images.
var vidholder = new Vue({
el: '#vidholder-0',
data: {
Videos: {
Rows: [
/*{
Columns: [
{
"title": "0",
"description": "",
"yTVideoID": "",
"thumbnailURL": "",
"uploaded": "",
"yTChannelID": ""
}
]
}*/
]
}
},
methods: {
// Get new videos from the web api.
retrieve: function (event) {
// Wipe out all old entries.
this.Videos.Rows = [];
axios.get('/api/Videos')
.then(function (response) {
// Dimension counters and limits.
var maxcols = 4;
var colcounter = 0;
var rowcounter = 0;
// And fill it with all the retrieved entries.
response.data.forEach(function (x) {
// Trim description if needed.
if (x.description.length > maxcharsdescriptionfield) {
x.description = x.description.substring(0, maxcharsdescriptionfield) + " ...";
}
// Handle creation of a new row.
if (colcounter == 0) {
Vue.set(this.Videos.Rows, rowcounter, []);
Vue.set(this.Videos.Rows[rowcounter], 'Columns', []);
}
// Add it to our array
this.Videos.Rows[rowcounter].Columns.push(x);
// So we get it all in groups of maxcols.
colcounter = colcounter + 1;
if (colcounter == maxcols) {
colcounter = 0;
rowcounter = rowcounter + 1;
}
}.bind(this));
}.bind(this))
.catch(function (error) {
console.log(error);
});
}
}
});
window.addEventListener('load', function () {
vidholder.retrieve();
});