Compare commits

..

No commits in common. "develop" and "master" have entirely different histories.

30 changed files with 896 additions and 688 deletions

4
.gitignore vendored
View File

@ -260,5 +260,5 @@ paket-files/
__pycache__/
i*.pyc
# For Visual Studio Code.
.vscode
# For Visual Studio Code
*/.vscode/

View File

@ -1,85 +0,0 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using System;
namespace YTManager.Controllers {
[Produces("application/json")]
[Route("api/Channels")]
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.
struct Channel_ForAPI {
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) {
Title = c.Title;
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();
}
}
// DB context used for all these calls.
private readonly MediaDB db;
// Maximum number of channels to return per query.
private readonly int max_per_query = 10;
// Constructor to fetch the db context.
public ChannelsController(MediaDB context) => db = context;
// 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)
.OrderByDescending(i => i.AddedtoDB)
.Take(max_per_query)
.ToListAsync();
// Convert them to what we will send out.
var converted = chanels
.Select(ch => new Channel_ForAPI(ch))
.ToList();
// 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

@ -1,124 +0,0 @@
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.Private
{
[Produces("application/json")]
[Route("api_raw/Channels")]
public class Private_Channel : Controller
{
private readonly MediaDB db;
public Private_Channel(MediaDB context)
{
db = context;
}
[HttpGet]
public IEnumerable<Channel> GetChannels()
{
return db.Channels;
}
[HttpGet("{id}")]
public async Task<IActionResult> GetChannel([FromRoute] long id)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
var channel = await db
.Channels
.Include(c => c.Videos)
.SingleOrDefaultAsync(m => m.PrimaryKey == id);
if (channel == null)
{
return NotFound();
}
return Ok(channel);
}
[HttpPut("{id}")]
public async Task<IActionResult> PutChannel([FromRoute] long id, [FromBody] Channel channel)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
if (id != channel.PrimaryKey)
{
return BadRequest();
}
db.Entry(channel).State = EntityState.Modified;
try
{
await db.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
if (!ChannelExists(id))
{
return NotFound();
}
else
{
throw;
}
}
return NoContent();
}
[HttpPost]
public async Task<IActionResult> PostChannel([FromBody] Channel channel)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
db.Channels.Add(channel);
await db.SaveChangesAsync();
return CreatedAtAction("GetChannel", new { id = channel.PrimaryKey }, channel);
}
[HttpDelete("{id}")]
public async Task<IActionResult> DeleteChannel([FromRoute] long id)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
var channel = await db.Channels.SingleOrDefaultAsync(m => m.PrimaryKey == id);
if (channel == null)
{
return NotFound();
}
db.Channels.Remove(channel);
await db.SaveChangesAsync();
return Ok(channel);
}
private bool ChannelExists(long id)
{
return db.Channels.Any(e => e.PrimaryKey == id);
}
}
}

View File

@ -1,129 +0,0 @@
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_raw/Videos")]
public class Private_Videos : Controller
{
private readonly MediaDB _context;
public Private_Videos(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
.Include(v => v.Channel)
.SingleOrDefaultAsync(m => m.PrimaryKey == 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.PrimaryKey)
{
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.PrimaryKey }, 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.PrimaryKey == 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.PrimaryKey == id);
}
}
}

View File

@ -1,160 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using System.Xml;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
namespace YTManager.Controllers {
[Produces("application/json")]
[Route("api/Videos")]
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.
struct Video_ForAPI {
// Title of the video.
public string Title;
// Description of the video.
public string Description;
// Youtube ID of the video.
public string ID;
// Thumbnail URL.
public string Thumbnail;
// Channel on youtube that owns this video
public string Channel;
// Duration of the video in seconds.
public int Seconds;
// What tags are relevant with this video.
public List<string> Tags;
// Populate this struct using a model video.
public Video_ForAPI(Models.Video video) {
Title = video.Title;
Description = video.Description;
ID = video.YoutubeID;
Thumbnail = video.ThumbnailURL;
Channel = video.Channel.Title;
Seconds = (int)video.Duration.TotalSeconds;
Tags = video.Tags == null ? new List<string>() : video.Tags.ToList();
}
}
// Maximum number of entries to return per query.
private readonly int max_per_query = 18;
// DB context used for all these calls.
private readonly MediaDB db;
// Constructor to fetch the db context.
public VideosController(MediaDB context) => db = context;
struct Search_Query {
// What the duration type is.
public enum _Duration_Type { LessThan, GreaterThan, Unset };
public _Duration_Type Duration_Type;
// What the expected duration of the videos are.
public TimeSpan Duration;
// Remaining tokens.
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("{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();
// Assume the search string is comma seperated.
parsed_query.Remaining_Tokens = searchstr.Split(',').Select(t => t.Trim().ToLower()).ToList();
// Find a potential single video duration.
List<string> greaterthan = parsed_query.Remaining_Tokens.Where(token => token.StartsWith('>')).ToList();
List<string> lessthan = parsed_query.Remaining_Tokens.Where(token => token.StartsWith('<')).ToList();
if (greaterthan.Count() + lessthan.Count() > 1)
return BadRequest();
if (greaterthan.Count() == 1) parsed_query.Duration_Type = Search_Query._Duration_Type.GreaterThan;
else if (lessthan.Count() == 1) parsed_query.Duration_Type = Search_Query._Duration_Type.LessThan;
else parsed_query.Duration_Type = Search_Query._Duration_Type.Unset;
greaterthan.ForEach(g => parsed_query.Remaining_Tokens.Remove(g));
lessthan.ForEach(l => parsed_query.Remaining_Tokens.Remove(l));
// Attempt to parse it using ISO8601 duration.
if (parsed_query.Duration_Type != Search_Query._Duration_Type.Unset) {
string timingstr = "PT" + greaterthan
.Concat(lessthan)
.Select(s => s.TrimStart(new char[] { '<', '>' }))
.First();
try {
parsed_query.Duration = XmlConvert.ToTimeSpan(timingstr.ToUpper());
} catch (Exception) {
return BadRequest("Failed to parse duration.");
}
}
// Get from the database all videos which satisfy the query via
// AND'ing all the queries.
var dbquery = db.Videos.Select(v => v);
// Get all videos that satisfy the time duration.
if (parsed_query.Duration_Type == Search_Query._Duration_Type.GreaterThan)
dbquery = dbquery.Where(v => v.Duration >= parsed_query.Duration);
else if (parsed_query.Duration_Type == Search_Query._Duration_Type.LessThan)
dbquery = dbquery.Where(v => v.Duration <= parsed_query.Duration);
// 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.YoutubeID.ToLower().Contains(token));
});
// Get all the relevant videos.
var vids = await dbquery
.OrderByDescending(i => i.AddedToYT)
.Take(max_per_query)
.Include(v => v.Channel)
.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);
}
}
}

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

@ -1,20 +1,18 @@
using System.Linq;
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;
using Hangfire.Storage;
namespace YTManager.Controllers {
[Produces("application/json")]
[Route("api/admin")]
[Route("api/Admin")]
public class AdminController : Controller {
// Get the mass update daemon job.
private static RecurringJobDto get_massupdatedaemon() {
return Hangfire.JobStorage
.Current
.GetConnection()
.GetRecurringJobs()
.SingleOrDefault(j => j.Id == Mass_Updater_ID);
}
// ID for mass update job, used to tell if the job is running or not.
public static string Mass_Updater_ID { get; } = "2013066213";
@ -38,12 +36,9 @@ namespace YTManager.Controllers {
// Ensures that the background YT Channel update API is running.
[HttpPost("Start_Updater")]
public IActionResult Start_Updater() {
if (get_massupdatedaemon() == null) {
Hangfire.RecurringJob.AddOrUpdate(
Mass_Updater_ID,
() => Tasks.FetchVideos.MassUpdate(Startup.DBStr), Hangfire.Cron.Minutely);
}
Hangfire.RecurringJob.AddOrUpdate(
Mass_Updater_ID,
() => Tasks.FetchVideos.MassUpdate(Startup.DBStr), Hangfire.Cron.Hourly);
return Ok();
}
@ -51,7 +46,8 @@ namespace YTManager.Controllers {
// Check if the periodic update job is enqued.
[HttpGet("Update")]
public IActionResult Get_Update_Status() {
return Ok(get_massupdatedaemon() == null ? "false" : "true");
bool exists = Hangfire.JobStorage.Current.GetConnection().GetRecurringJobs().Any(j => j.Id == Mass_Updater_ID);
return Ok(exists ? "true" : "false");
}
}
}

View File

@ -0,0 +1,109 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Newtonsoft.Json.Linq;
using Microsoft.EntityFrameworkCore;
using YTManager;
using YTManager.Models;
namespace YTManager.Controllers {
[Produces("application/json")]
[Route("api/Channels")]
public class ChannelsController : Controller {
// DB context used for all these calls.
private readonly MediaDB db;
// Constructor to fetch the db context.
public ChannelsController(MediaDB context) => db = context;
// GET api/Channels
[HttpGet]
public async Task<IEnumerable<Models.Channel>> Get() {
return await db.Channels.OrderByDescending(c => c.AddedtoDB).ToListAsync();
}
// GET api/Channels/5
[HttpGet("{YTchannelID}")]
public Task<Models.Channel> Get([FromQuery] string YTchannelID) {
return db.Channels.SingleOrDefaultAsync(c => c.YoutubeID == YTchannelID);
}
// GET: api/Channels/sdfs6DFS65f/Videos (using YouTube channel ID)
[HttpGet("{YTChannelID}/Videos")]
public async Task<IActionResult> GetVideos([FromRoute] string YTchannelID) {
if (!ModelState.IsValid)
return BadRequest(ModelState);
// Attempt to get the video from the database.
var channel = await db.Channels.SingleOrDefaultAsync(m => m.YoutubeID == YTchannelID);
// If the channel wasn't found then send back not found.
if (channel == null)
return NotFound();
// Send back the videos from the channel.
return Ok(db.Entry(channel).Collection(c => c.Videos).LoadAsync());
}
// POST api/Channels/sdfs6DFS65f
[HttpPost("{YTchannelID}")]
public async Task<IActionResult> Post([FromRoute] string YTchannelID) {
// Check if we were able to parse.
if (!ModelState.IsValid) return BadRequest(ModelState);
// Verify the channel doesn't already exist.
if (db.Channels.Any(c => c.YoutubeID == YTchannelID))
return BadRequest();
// Get the channel info.
var ch = await Tasks.FetchVideos.Get_YTChannel(YTchannelID);
// Seems good, so add it.
db.Channels.Add(ch);
await db.SaveChangesAsync();
// And all is well.
return Ok();
}
// POST api/Channels
[HttpPost]
public async Task<IActionResult> Post([FromBody] Channel channel) {
// Check if we were able to parse.
if (!ModelState.IsValid) return BadRequest(ModelState);
// Verify the channel doesn't already exist.
if (db.Channels.Any(c => c.YoutubeID == channel.YoutubeID))
return BadRequest();
// Seems good, so add it.
db.Channels.Add(channel);
await db.SaveChangesAsync();
// And all is well.
return Ok();
}
// DELETE api/Channels/5
[HttpDelete("{YTChannelID}")]
public async Task<IActionResult> Delete([FromRoute] string YTChannelID) {
// Check if we were able to parse.
if (!ModelState.IsValid) return BadRequest(ModelState);
// Attempt to find the channel.
var channel = await db.Channels.SingleOrDefaultAsync(c => c.YoutubeID == YTChannelID);
// Check if such an entry exists already.
if (channel == null)
return NotFound();
// Remove.
db.Channels.Remove(channel);
await db.SaveChangesAsync();
return Ok(channel);
}
}
}

View File

@ -0,0 +1,80 @@
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 {
// DB context used for all these calls.
private readonly MediaDB db;
// Constructor to fetch the db context.
public VideosController(MediaDB context) => db = context;
// GET: api/Videos
[HttpGet]
public async Task<List<Video>> GetVideos() {
return await db.Videos.OrderByDescending(i => i.AddedtoDB).ToListAsync();
}
// GET: api/Videos/5
[HttpGet("{id}")]
public async Task<IActionResult> GetVideo([FromRoute] long id){
// Check if we were able to parse.
if (!ModelState.IsValid) return BadRequest(ModelState);
// Attempt to get the video from the database.
var video = await db.Videos.SingleOrDefaultAsync(m => m.PrimaryKey == id);
// If the video wasn't found then send back not foud.
if (video == null)
return NotFound();
else
return Ok(video);
}
// POST: api/Videos
[HttpPost]
public async Task<IActionResult> PostVideo([FromBody] Video video){
// Check if we were able to parse.
if (!ModelState.IsValid) return BadRequest(ModelState);
// Check if such a database exists already.
if (await db.Videos.AnyAsync(d => d.YoutubeID == video.YoutubeID))
return BadRequest();
// Add our video to the database and tell db to update.
db.Videos.Add(video);
await db.SaveChangesAsync();
// Say that the creation was succesfull.
return Ok();
}
// DELETE: api/Videos/alfkeo4f5
[HttpDelete("{ytid}")]
public async Task<IActionResult> DeleteVideo([FromRoute] string YTVideoID){
// Check if we were able to parse.
if (!ModelState.IsValid) return BadRequest(ModelState);
// Attempt to find the video.
var video = await db.Videos.SingleOrDefaultAsync(m => m.YoutubeID == YTVideoID);
// Check if such a database exists already.
if (video == null)
return NotFound();
// Remove.
db.Videos.Remove(video);
await db.SaveChangesAsync();
return Ok(video);
}
}
}

View File

@ -1,4 +1,8 @@
using Microsoft.EntityFrameworkCore;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
namespace YTManager
{
@ -6,6 +10,7 @@ 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,25 +1,25 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
using Microsoft.EntityFrameworkCore.Storage;
using Microsoft.EntityFrameworkCore.Storage.Internal;
using System;
using YTManager;
namespace YTManager.Migrations
{
[DbContext(typeof(MediaDB))]
[Migration("20190127210534_initial")]
partial class initial
[Migration("20180220032847_initiailmigration")]
partial class initiailmigration
{
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.SerialColumn)
.HasAnnotation("ProductVersion", "2.2.1-servicing-10028")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
.HasAnnotation("ProductVersion", "2.0.1-rtm-125");
modelBuilder.Entity("YTManager.Models.Channel", b =>
{
@ -31,17 +31,12 @@ namespace YTManager.Migrations
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();
@ -50,6 +45,19 @@ 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")
@ -59,16 +67,11 @@ 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<string[]>("Tags")
.IsRequired();
b.Property<string>("ThumbnailURL")
.IsRequired();
@ -87,10 +90,9 @@ namespace YTManager.Migrations
modelBuilder.Entity("YTManager.Models.Video", b =>
{
b.HasOne("YTManager.Models.Channel", "Channel")
b.HasOne("YTManager.Models.Channel")
.WithMany("Videos")
.HasForeignKey("ChannelPrimaryKey")
.OnDelete(DeleteBehavior.Cascade);
.HasForeignKey("ChannelPrimaryKey");
});
#pragma warning restore 612, 618
}

View File

@ -1,10 +1,11 @@
using System;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
using System;
using System.Collections.Generic;
namespace YTManager.Migrations
{
public partial class initial : Migration
public partial class initiailmigration : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
@ -14,34 +15,43 @@ namespace YTManager.Migrations
{
PrimaryKey = table.Column<long>(nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.SerialColumn),
Title = table.Column<string>(nullable: false),
AddedtoDB = table.Column<DateTime>(nullable: false),
Description = table.Column<string>(nullable: false),
ThumbnailURL = table.Column<string>(nullable: false),
YoutubeID = table.Column<string>(nullable: false),
AddedtoDB = table.Column<DateTime>(nullable: false),
Refreshed = table.Column<DateTime>(nullable: false),
UserTags = table.Column<string[]>(nullable: false)
Title = table.Column<string>(nullable: false),
YoutubeID = table.Column<string>(nullable: false)
},
constraints: table =>
{
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
{
PrimaryKey = table.Column<long>(nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.SerialColumn),
Title = table.Column<string>(nullable: false),
Description = table.Column<string>(nullable: false),
YoutubeID = table.Column<string>(nullable: false),
ThumbnailURL = table.Column<string>(nullable: false),
AddedToYT = table.Column<DateTime>(nullable: false),
AddedtoDB = table.Column<DateTime>(nullable: false),
Duration = table.Column<TimeSpan>(nullable: false),
ChannelPrimaryKey = table.Column<long>(nullable: false),
Tags = table.Column<string[]>(nullable: false)
ChannelPrimaryKey = table.Column<long>(nullable: true),
Description = table.Column<string>(nullable: false),
ThumbnailURL = table.Column<string>(nullable: false),
Title = table.Column<string>(nullable: false),
YoutubeID = table.Column<string>(nullable: false)
},
constraints: table =>
{
@ -51,7 +61,7 @@ namespace YTManager.Migrations
column: x => x.ChannelPrimaryKey,
principalTable: "Channels",
principalColumn: "PrimaryKey",
onDelete: ReferentialAction.Cascade);
onDelete: ReferentialAction.Restrict);
});
migrationBuilder.CreateIndex(
@ -62,6 +72,9 @@ namespace YTManager.Migrations
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "Tags");
migrationBuilder.DropTable(
name: "Videos");

View File

@ -1,9 +1,11 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage;
using Microsoft.EntityFrameworkCore.Storage.Internal;
using System;
using YTManager;
namespace YTManager.Migrations
@ -16,8 +18,7 @@ namespace YTManager.Migrations
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.SerialColumn)
.HasAnnotation("ProductVersion", "2.2.1-servicing-10028")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
.HasAnnotation("ProductVersion", "2.0.1-rtm-125");
modelBuilder.Entity("YTManager.Models.Channel", b =>
{
@ -29,17 +30,12 @@ namespace YTManager.Migrations
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();
@ -48,6 +44,19 @@ 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")
@ -57,16 +66,11 @@ 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<string[]>("Tags")
.IsRequired();
b.Property<string>("ThumbnailURL")
.IsRequired();
@ -85,10 +89,9 @@ namespace YTManager.Migrations
modelBuilder.Entity("YTManager.Models.Video", b =>
{
b.HasOne("YTManager.Models.Channel", "Channel")
b.HasOne("YTManager.Models.Channel")
.WithMany("Videos")
.HasForeignKey("ChannelPrimaryKey")
.OnDelete(DeleteBehavior.Cascade);
.HasForeignKey("ChannelPrimaryKey");
});
#pragma warning restore 612, 618
}

View File

@ -1,5 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using System.ComponentModel.DataAnnotations;
namespace YTManager.Models {
@ -28,16 +30,7 @@ namespace YTManager.Models {
[Required]
public DateTime AddedtoDB { get; set; }
//! Last time this channel was updated.
[Required]
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; }
}
}

19
YTManager/Models/Tag.cs Normal file
View File

@ -0,0 +1,19 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
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

@ -1,5 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using System.ComponentModel.DataAnnotations;
namespace YTManager.Models {
@ -32,16 +34,8 @@ namespace YTManager.Models {
[Required]
public DateTime AddedtoDB { get; set; }
// How long the video is
[Required]
public TimeSpan Duration { get; set; }
// What channel this video comes from.
[Required]
public Channel Channel { get; set; }
// Tag this video applies to.
[Required]
public string[] Tags { get; set; }
public List<Tag> Tags;
}
}

View File

@ -1,6 +1,12 @@
using System.IO;
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
{

View File

@ -20,7 +20,7 @@
"commandName": "Project",
"launchUrl": "api/values",
"environmentVariables": {
"POSTGRESQL_DBSTR": "Server=localhost;Port=32768;Database=DumbYT;User Id=DumbYT;Password=DumbYTPassword;",
"POSTGRESQL_DBSTR": "Server=192.168.1.211;Port=32768;Database=postgres;User Id=postgres;",
"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?}");
});
}

View File

@ -1,8 +1,13 @@
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;
@ -18,7 +23,7 @@ namespace YTManager {
public static string DBStr {
get {
string s = Environment.GetEnvironmentVariable("POSTGRESQL_DBSTR");
s = s ?? "Server=localhost;Port=32768;Database=DumbYT;User Id=DumbYT;Password=DumbYTPassword;";
s = s ?? "Server=localhost;Port=32768;Database=postgres;User Id=postgres;";
return s;
}
}
@ -38,7 +43,6 @@ namespace YTManager {
app.UseDefaultFiles();
app.UseStaticFiles();
app.UseCors(b => { b.AllowAnyOrigin(); b.AllowAnyMethod(); });
app.UseMvc();
app.UseHangfireServer();
app.UseHangfireDashboard();

View File

@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore;
namespace YTManager.Tasks {
public class FetchVideos {
// Get a bunch of videos from youtube that the channel generated.
public static async Task<List<Models.Video>> Get_YTVideos(string channelID, int max = 50) {
private static async Task<List<Models.Video>> Get_YTVideos(string channelID) {
// YT API access key
var youtubeService = new YouTubeService(new Google.Apis.Services.BaseClientService.Initializer()
{
@ -16,18 +16,15 @@ namespace YTManager.Tasks {
ApplicationName = "testingapppp"
});
// Max cannot be larger than 50, so clamp it.
max = max > 50 ? 50 : max;
// Search youtube for all the relevant data of the channel.
var videos_query = youtubeService.Search.List("snippet");
videos_query.ChannelId = channelID;
videos_query.Order = SearchResource.ListRequest.OrderEnum.Date;
videos_query.MaxResults = max;
var videos_response = await videos_query.ExecuteAsync();
var query = youtubeService.Search.List("snippet");
query.ChannelId = channelID;
query.Order = SearchResource.ListRequest.OrderEnum.Date;
query.MaxResults = 50;
var response = await query.ExecuteAsync();
// Convert the response into models.
var videos = videos_response.Items?
return response.Items?
.Where(i => i.Id.Kind == "youtube#video")
.Select(newvid => new Models.Video
{
@ -36,29 +33,8 @@ namespace YTManager.Tasks {
YoutubeID = newvid.Id.VideoId,
AddedToYT = newvid.Snippet.PublishedAt.GetValueOrDefault(),
AddedtoDB = DateTime.Now,
ThumbnailURL = newvid.Snippet.Thumbnails.Medium.Url,
Tags = new string[] {}
ThumbnailURL = newvid.Snippet.Thumbnails.Medium.Url
}).ToList();
// Search youtube to get the length and tags for each video.
var duration_query = youtubeService.Videos.List("contentDetails,snippet");
duration_query.Id = string.Join(',', videos.Select(v => v.YoutubeID));
var duration_response = await duration_query.ExecuteAsync();
// Pair each video with the result.
foreach(var contentdetail in duration_response.Items.Where(i => i.Kind == "youtube#video")){
var vid = videos.Single(v => v.YoutubeID == contentdetail.Id);
// Yes, really, ISO8601 time span => c#'s TimeSpan is not in TimeSpan, it's in xmlconvert! Wtf?!
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 = contentdetail.Snippet.Tags.Select(t => t.ToLower()).ToArray();
}
// Send back the parsed vids.
return videos;
}
// Gets some info about a youtube channel.
@ -82,10 +58,7 @@ namespace YTManager.Tasks {
Title = response.Items.First().Snippet.Title,
ThumbnailURL = response.Items.First().Snippet.Thumbnails.Medium.Url,
YoutubeID = channelID,
AddedtoDB = DateTime.Now,
Refreshed = DateTime.MinValue,
UserTags = new string[]{},
Videos = { }
AddedtoDB = DateTime.Now
};
}
@ -95,12 +68,10 @@ namespace YTManager.Tasks {
var ops = new DbContextOptionsBuilder<MediaDB>();
ops.UseNpgsql(dbstr);
// Get all the channels from the db that expired.
var threshold = DateTime.Now.Subtract(TimeSpan.FromMinutes(60));
// Get all the channels from the db.
var channel_ids = await
(new MediaDB(ops.Options)).Channels
.Where(ch => ch.Refreshed < threshold)
.Select(ch => ch.YoutubeID)
(new MediaDB(ops.Options))
.Channels.Select(ch => ch.YoutubeID)
.ToListAsync();
// For each channel, do an update.
@ -125,15 +96,7 @@ namespace YTManager.Tasks {
var Videos = await Get_YTVideos(channel.YoutubeID);
// Get all the videos which haven't been put into this channels videos.
var newvids = Videos
.Where(nv => !channel.Videos.Any(cv => cv.YoutubeID == nv.YoutubeID));
// Say what channel all the videos came from.
foreach (var v in newvids)
v.Channel = channel;
// Say the channel has been refreshed.
channel.Refreshed = DateTime.Now;
var newvids = Videos.Where(nv => !channel.Videos.Any(cv => cv.YoutubeID == nv.YoutubeID));
// Add all the videos to the databse.
await db.Videos.AddRangeAsync(newvids);

View File

@ -1,38 +1,41 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>netcoreapp2.2</TargetFramework>
<TypeScriptToolsVersion>2.5</TypeScriptToolsVersion>
<TargetFramework>netcoreapp2.0</TargetFramework>
</PropertyGroup>
<!-- Job System -->
<ItemGroup>
<PackageReference Include="Hangfire" Version="1.6.22" />
<PackageReference Include="Hangfire.AspNetCore" Version="1.6.22" />
<PackageReference Include="Hangfire.PostgreSql" Version="1.5.0" />
<PackageReference Include="Hangfire" Version="1.6.17" />
<PackageReference Include="Hangfire.AspNetCore" Version="1.6.17" />
<PackageReference Include="Hangfire.PostgreSql" Version="1.4.8.1" />
</ItemGroup>
<!-- Database -->
<ItemGroup>
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="2.2.0" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="2.0.0" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL.Design" Version="1.1.1" />
</ItemGroup>
<!-- Backend Website -->
<ItemGroup>
<PackageReference Include="Google.Apis.YouTube.v3" Version="1.37.0.1469" />
<PackageReference Include="Microsoft.AspNetCore.All" />
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="2.2.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="2.2.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="2.2.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="2.2.1" />
<PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="2.2.1" />
<PackageReference Include="Google.Apis.YouTube.v3" Version="1.32.2.1131" />
<PackageReference Include="Microsoft.AspNetCore.All" Version="2.0.5" />
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="2.0.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="2.0.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="2.0.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="2.0.1" />
<PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="2.0.2" />
</ItemGroup>
<!-- So we can run dotnet migration ... -->
<ItemGroup>
<DotNetCliToolReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Tools" Version="2.0.4" />
<DotNetCliToolReference Include="Microsoft.EntityFrameworkCore.Tools.DotNet" Version="2.0.3" />
<DotNetCliToolReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Tools" Version="2.0.2" />
<DotNetCliToolReference Include="Microsoft.EntityFrameworkCore.Tools.DotNet" Version="2.0.1" />
</ItemGroup>
<ItemGroup>
<Folder Include="Migrations\" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,44 @@
<!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>
<div class="pageheader">
<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>
<div id="apistatus-0"></div>
</div>
<hr />
<h2>Subscribed Channels</h2>
<div id="subbedchannelstable-0"></div>
<div id="addchanel-0"></div>
<hr />
<h2>Videos in DB</h2>
<div id="videosindbtable-0"></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="admin.js"></script>
</body>
</html>

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

@ -0,0 +1,271 @@
$(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
},
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 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>
`,
methods: {
submit: function (event) {
// Send the channel to our API and request tables be updated.
var d = this.entry;
axios.post('/api/Channels/' + this.entry.yTChannelID)
.then(function (response) {
this.entry.title = "";
this.entry.description = "";
this.entry.yTChannelID = "";
this.entry.thumbnailURL = "";
this.expanded = false;
tbody0.retrieve();
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: '#subbedchannelstable-0',
data: {
entries: [""]
},
template: `
<table>
<thead>
<tr>
<th>YT Channel ID</th>
<th>Title</th>
<th>Description</th>
<th>ID</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<tr v-cloak v-for="entry in entries">
<td class="tinytext12px">{{entry.youtubeID}}</td>
<td class="tinytext12px">{{entry.title}}</td>
<td class="tinytext12px">{{entry.description}}</td>
<td class="tinytext12px">{{entry.primaryKey}}</td>
<td>
<a href="#"><i class="fa fa-refresh" aria-hidden="true" :id="entry.youtubeID" @click="forcerefresh"/></a>
<a href="#"><i class="fa fa-trash-o" aria-hidden="true" :id="entry.youtubeID" @click="deletechannel"/></a>
</td>
</tr>
</tbody>
</table>
`,
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);
});
},
forcerefresh: function (event) {
axios
.post('/api/Admin/Update/' + event.target.id)
.catch(function (error) { console.log(error); });
},
deletechannel: function (event) {
axios
.delete('/api/Channels/' + event.target.id)
.catch(function (error) { console.log(error); });
}
}
});
// Bind to our table of entries.
var apistatus = new Vue({
el: '#apistatus-0',
data: {
apiaccessible: false,
updatingjobactive: false
},
template: `
<div class="grid-x">
<div class="medium-1 cell">API Status</div>
<div class="medium-1 cell">
<i class="fa fa-thumbs-o-up apistatusicon fa-2x" aria-hidden="true" v-if="apiaccessible"></i>
<i class="fa fa-thumbs-o-down apistatusicon fa-2x" aria-hidden="true" v-else></i>
</div>
<div class="medium-7 cell"></div>
<div class="medium-2 cell">Video Fetching Status</div>
<div class="medium-1 cell">
<i class="fa fa-thumbs-o-up apistatusicon fa-2x" aria-hidden="true" v-if="updatingjobactive"></i>
<i class="fa fa-thumbs-o-down apistatusicon fa-2x" aria-hidden="true" v-else></i>
</div>
</div>
`,
methods: {
update: function (event) {
axios.get('/api/Admin/Update')
.then(function (response) {
this.apiaccessible = (response.status == 200);
this.updatingjobactive = (response.data != "false")
}.bind(this)
)
.catch(function (error) {
console.log(error);
});
}
}
});
// Grid of images.
var videostb = new Vue({
el: '#videosindbtable-0',
data: {
Videos: []
},
template: `
<table>
<thead>
<tr>
<th>Title</th>
<th>Description</th>
<th>Youtube Video ID</th>
<th>Uploaded</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.youtubeID}}</td>
<td class="tinytext12px">{{video.uploaded}}</td>
<td class="tinytext12px">{{video.primaryKey}}</td>
</tr>
</tbody>
</table>
`,
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 () {
apistatus.update();
tbody0.retrieve();
videostb.retrieve();
});

View File

@ -0,0 +1,68 @@
body {
margin-left: 5%;
margin-right: 5%;
line-height: 1.6;
font-size: 18px;
color: #003636;
background-color: #F8F8F8;
max-width: 1024px;
margin: auto;
}
.pageheader {
max-width: 960px;
margin: auto;
}
.error {
background-color: red;
}
.success {
background-color: rgb(149, 167, 82);
}
.curvedbottom {
border-bottom-left-radius: 10px;
border-bottom-right-radius: 10px;
}
#addnewchannelform {
width: inherit;
margin: auto;
}
.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 {
opacity: 0;
}
.apistatusicon {
animation-duration: 1s;
animation-name: spinny;
animation-iteration-count: infinite;
animation-direction: alternate;
}
@keyframes spinny {
from {
transform: rotate(-15deg);
}
to {
transform: rotate(15deg);
}
}

View File

@ -0,0 +1,37 @@
<!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>
<div class="pageheader">
<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>
</div>
<hr />
<div v-cloak id="vidholder-0"></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,58 @@
$(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: []
},
// Template has wrapping div because v-for can't be in root it seems.
template: `
<div>
<div class="grid-x grid-margin-x large-up-6">
<div v-for="video in Videos" class="cell">
<div class="success card curvedbottom">
<a :href="video.url"><img :src="video.thumbnailURL"></a>
<div class="card-section" style="padding-top: 7px; padding-bottom: 7px;">
<p style="padding-top: 0px; padding-bottom: 0px; font-size: 12px;">{{ video.title }}</p>
</div>
</div>
</div>
</div >
</div>
`,
methods: {
// Get new videos from the web api.
retrieve: function (event) {
// Wipe out all old entries.
this.Videos = [];
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 > maxcharsdescriptionfield) {
x.description = x.description.substring(0, maxcharsdescriptionfield) + " ...";
}
// Generate a new URL by adding the YT ID.
x.url = "https://www.youtube.com/watch?v=" + x.youtubeID;
// Add it to our array
this.Videos.push(x);
}.bind(this));
}.bind(this))
.catch(function (error) {
console.log(error);
});
}
}
});
window.addEventListener('load', function () {
vidholder.retrieve();
});

View File

@ -1,26 +0,0 @@
[Unit]
Description=Dotnet interface for DumbYT
After=network-online.target
Wants=network-online.target systemd-networkd-wait-online.service
[Service]
WorkingDirectory=/var/www/BackEnd/bin/Release/netcoreapp2.0/
ExecStart=/usr/bin/dotnet /var/www/BackEnd/bin/Release/netcoreapp2.0/YTManager.dll
; Restart configuration
Restart=always
; How this shows up in logs
SyslogIdentifier=dumbyt_dotnet
; User and Cgroup the process will run as.
User=hak8or
Group=hak8or
Environment=ASPNETCORE_ENVIRONMENT=Production
Environment=DOTNET_PRINT_TELEMETRY_MESSAGE=false
Environment=ASPNETCORE_URLS="http://*:62214"
Environment=POSTGRESQL_DBSTR="Server=10.10.10.200;Port=32768;Database=DumbYT;User Id=DumbYT;Password=DumbYT;"
[Install]
WantedBy=multi-user.target