Compare commits
31 Commits
Author | SHA1 | Date |
---|---|---|
hak8or | 312dbdd8a5 | |
hak8or | 9b2ff546f0 | |
hak8or | a3e7b4f8f7 | |
hak8or | dc1937d2f8 | |
hak8or | a50956539a | |
hak8or | 9f5fb78835 | |
hak8or | 96a26ce30d | |
hak8or | 76ae5a6f8f | |
hak8or | 2709154cee | |
hak8or | 9b89df3d4b | |
hak8or | 1f49050d69 | |
hak8or | 2ead7115e8 | |
hak8or | ac749b1300 | |
hak8or | 5c74e2016a | |
hak8or | 152a000927 | |
hak8or | 3a11ecf893 | |
hak8or | ccef5fee0c | |
hak8or | e140cae317 | |
hak8or | 9ac93a540e | |
hak8or | 8978fb71e4 | |
hak8or | 038d363b00 | |
hak8or | a37fe30b6f | |
hak8or | 8b148b1ad9 | |
hak8or | f708503072 | |
hak8or | 01bc50f9f2 | |
hak8or | 1e42ac746f | |
hak8or | 8deff5459a | |
hak8or | dc86e704ef | |
hak8or | 7714a35d3e | |
hak8or | e11ddfa72c | |
hak8or | 66a9c1edb4 |
|
@ -260,5 +260,5 @@ paket-files/
|
|||
__pycache__/
|
||||
i*.pyc
|
||||
|
||||
# For Visual Studio Code
|
||||
*/.vscode/
|
||||
# For Visual Studio Code.
|
||||
.vscode
|
||||
|
|
|
@ -1,18 +1,20 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using System.Linq;
|
||||
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";
|
||||
|
||||
|
@ -36,9 +38,12 @@ namespace YTManager.Controllers {
|
|||
// Ensures that the background YT Channel update API is running.
|
||||
[HttpPost("Start_Updater")]
|
||||
public IActionResult Start_Updater() {
|
||||
Hangfire.RecurringJob.AddOrUpdate(
|
||||
Mass_Updater_ID,
|
||||
() => Tasks.FetchVideos.MassUpdate(Startup.DBStr), Hangfire.Cron.Hourly);
|
||||
if (get_massupdatedaemon() == null) {
|
||||
Hangfire.RecurringJob.AddOrUpdate(
|
||||
Mass_Updater_ID,
|
||||
() => Tasks.FetchVideos.MassUpdate(Startup.DBStr), Hangfire.Cron.Minutely);
|
||||
}
|
||||
|
||||
return Ok();
|
||||
}
|
||||
|
||||
|
@ -46,8 +51,7 @@ namespace YTManager.Controllers {
|
|||
// Check if the periodic update job is enqued.
|
||||
[HttpGet("Update")]
|
||||
public IActionResult Get_Update_Status() {
|
||||
bool exists = Hangfire.JobStorage.Current.GetConnection().GetRecurringJobs().Any(j => j.Id == Mass_Updater_ID);
|
||||
return Ok(exists ? "true" : "false");
|
||||
return Ok(get_massupdatedaemon() == null ? "false" : "true");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,85 @@
|
|||
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();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,124 @@
|
|||
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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,129 @@
|
|||
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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,160 @@
|
|||
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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,8 +1,4 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace YTManager
|
||||
{
|
||||
|
@ -10,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,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;
|
||||
using Microsoft.EntityFrameworkCore.Storage.Internal;
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
using YTManager;
|
||||
|
||||
namespace YTManager.Migrations
|
||||
{
|
||||
[DbContext(typeof(MediaDB))]
|
||||
[Migration("20180220032847_initiailmigration")]
|
||||
partial class initiailmigration
|
||||
[Migration("20190127210534_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.1-rtm-125");
|
||||
.HasAnnotation("ProductVersion", "2.2.1-servicing-10028")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
modelBuilder.Entity("YTManager.Models.Channel", b =>
|
||||
{
|
||||
|
@ -31,12 +31,17 @@ 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();
|
||||
|
||||
|
@ -45,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")
|
||||
|
@ -67,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<string[]>("Tags")
|
||||
.IsRequired();
|
||||
|
||||
b.Property<string>("ThumbnailURL")
|
||||
.IsRequired();
|
||||
|
||||
|
@ -90,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
|
||||
}
|
|
@ -1,11 +1,10 @@
|
|||
using Microsoft.EntityFrameworkCore.Metadata;
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
|
||||
namespace YTManager.Migrations
|
||||
{
|
||||
public partial class initiailmigration : Migration
|
||||
public partial class initial : Migration
|
||||
{
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
|
@ -15,43 +14,34 @@ namespace YTManager.Migrations
|
|||
{
|
||||
PrimaryKey = table.Column<long>(nullable: false)
|
||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.SerialColumn),
|
||||
AddedtoDB = table.Column<DateTime>(nullable: false),
|
||||
Title = table.Column<string>(nullable: false),
|
||||
Description = table.Column<string>(nullable: false),
|
||||
ThumbnailURL = table.Column<string>(nullable: false),
|
||||
Title = table.Column<string>(nullable: false),
|
||||
YoutubeID = 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)
|
||||
},
|
||||
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),
|
||||
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)
|
||||
Duration = table.Column<TimeSpan>(nullable: false),
|
||||
ChannelPrimaryKey = table.Column<long>(nullable: false),
|
||||
Tags = table.Column<string[]>(nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
|
@ -61,7 +51,7 @@ namespace YTManager.Migrations
|
|||
column: x => x.ChannelPrimaryKey,
|
||||
principalTable: "Channels",
|
||||
principalColumn: "PrimaryKey",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
|
@ -72,9 +62,6 @@ namespace YTManager.Migrations
|
|||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "Tags");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "Videos");
|
||||
|
|
@ -1,11 +1,9 @@
|
|||
// <auto-generated />
|
||||
using System;
|
||||
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 Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
using YTManager;
|
||||
|
||||
namespace YTManager.Migrations
|
||||
|
@ -18,7 +16,8 @@ namespace YTManager.Migrations
|
|||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.SerialColumn)
|
||||
.HasAnnotation("ProductVersion", "2.0.1-rtm-125");
|
||||
.HasAnnotation("ProductVersion", "2.2.1-servicing-10028")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
modelBuilder.Entity("YTManager.Models.Channel", b =>
|
||||
{
|
||||
|
@ -30,12 +29,17 @@ 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();
|
||||
|
||||
|
@ -44,19 +48,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")
|
||||
|
@ -66,11 +57,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<string[]>("Tags")
|
||||
.IsRequired();
|
||||
|
||||
b.Property<string>("ThumbnailURL")
|
||||
.IsRequired();
|
||||
|
||||
|
@ -89,9 +85,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
|
||||
}
|
|
@ -1,7 +1,5 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace YTManager.Models {
|
||||
|
@ -30,7 +28,16 @@ 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; }
|
||||
}
|
||||
}
|
|
@ -1,7 +1,5 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace YTManager.Models {
|
||||
|
@ -34,8 +32,16 @@ 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 List<Tag> Tags;
|
||||
public string[] Tags { get; set; }
|
||||
}
|
||||
}
|
|
@ -1,12 +1,6 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using System.IO;
|
||||
using Microsoft.AspNetCore;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace YTManager
|
||||
{
|
|
@ -20,7 +20,7 @@
|
|||
"commandName": "Project",
|
||||
"launchUrl": "api/values",
|
||||
"environmentVariables": {
|
||||
"POSTGRESQL_DBSTR": "Server=192.168.1.211;Port=32768;Database=postgres;User Id=postgres;",
|
||||
"POSTGRESQL_DBSTR": "Server=localhost;Port=32768;Database=DumbYT;User Id=DumbYT;Password=DumbYTPassword;",
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
},
|
||||
"applicationUrl": "http://localhost:62214/"
|
|
@ -1,13 +1,8 @@
|
|||
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;
|
||||
|
@ -23,7 +18,7 @@ namespace YTManager {
|
|||
public static string DBStr {
|
||||
get {
|
||||
string s = Environment.GetEnvironmentVariable("POSTGRESQL_DBSTR");
|
||||
s = s ?? "Server=localhost;Port=32768;Database=postgres;User Id=postgres;";
|
||||
s = s ?? "Server=localhost;Port=32768;Database=DumbYT;User Id=DumbYT;Password=DumbYTPassword;";
|
||||
return s;
|
||||
}
|
||||
}
|
||||
|
@ -43,6 +38,7 @@ namespace YTManager {
|
|||
|
||||
app.UseDefaultFiles();
|
||||
app.UseStaticFiles();
|
||||
app.UseCors(b => { b.AllowAnyOrigin(); b.AllowAnyMethod(); });
|
||||
app.UseMvc();
|
||||
app.UseHangfireServer();
|
||||
app.UseHangfireDashboard();
|
|
@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore;
|
|||
namespace YTManager.Tasks {
|
||||
public class FetchVideos {
|
||||
// Get a bunch of videos from youtube that the channel generated.
|
||||
private static async Task<List<Models.Video>> Get_YTVideos(string channelID) {
|
||||
public static async Task<List<Models.Video>> Get_YTVideos(string channelID, int max = 50) {
|
||||
// YT API access key
|
||||
var youtubeService = new YouTubeService(new Google.Apis.Services.BaseClientService.Initializer()
|
||||
{
|
||||
|
@ -16,15 +16,18 @@ 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 query = youtubeService.Search.List("snippet");
|
||||
query.ChannelId = channelID;
|
||||
query.Order = SearchResource.ListRequest.OrderEnum.Date;
|
||||
query.MaxResults = 50;
|
||||
var response = await query.ExecuteAsync();
|
||||
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();
|
||||
|
||||
// Convert the response into models.
|
||||
return response.Items?
|
||||
var videos = videos_response.Items?
|
||||
.Where(i => i.Id.Kind == "youtube#video")
|
||||
.Select(newvid => new Models.Video
|
||||
{
|
||||
|
@ -33,8 +36,29 @@ 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.
|
||||
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.
|
||||
|
@ -58,7 +82,10 @@ namespace YTManager.Tasks {
|
|||
Title = response.Items.First().Snippet.Title,
|
||||
ThumbnailURL = response.Items.First().Snippet.Thumbnails.Medium.Url,
|
||||
YoutubeID = channelID,
|
||||
AddedtoDB = DateTime.Now
|
||||
AddedtoDB = DateTime.Now,
|
||||
Refreshed = DateTime.MinValue,
|
||||
UserTags = new string[]{},
|
||||
Videos = { }
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -68,10 +95,12 @@ namespace YTManager.Tasks {
|
|||
var ops = new DbContextOptionsBuilder<MediaDB>();
|
||||
ops.UseNpgsql(dbstr);
|
||||
|
||||
// Get all the channels from the db.
|
||||
// Get all the channels from the db that expired.
|
||||
var threshold = DateTime.Now.Subtract(TimeSpan.FromMinutes(60));
|
||||
var channel_ids = await
|
||||
(new MediaDB(ops.Options))
|
||||
.Channels.Select(ch => ch.YoutubeID)
|
||||
(new MediaDB(ops.Options)).Channels
|
||||
.Where(ch => ch.Refreshed < threshold)
|
||||
.Select(ch => ch.YoutubeID)
|
||||
.ToListAsync();
|
||||
|
||||
// For each channel, do an update.
|
||||
|
@ -96,7 +125,15 @@ 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));
|
||||
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;
|
||||
|
||||
// Add all the videos to the databse.
|
||||
await db.Videos.AddRangeAsync(newvids);
|
|
@ -1,41 +1,38 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netcoreapp2.0</TargetFramework>
|
||||
<TargetFramework>netcoreapp2.2</TargetFramework>
|
||||
<TypeScriptToolsVersion>2.5</TypeScriptToolsVersion>
|
||||
</PropertyGroup>
|
||||
|
||||
<!-- Job System -->
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Hangfire" Version="1.6.17" />
|
||||
<PackageReference Include="Hangfire.AspNetCore" Version="1.6.17" />
|
||||
<PackageReference Include="Hangfire.PostgreSql" Version="1.4.8.1" />
|
||||
<PackageReference Include="Hangfire" Version="1.6.22" />
|
||||
<PackageReference Include="Hangfire.AspNetCore" Version="1.6.22" />
|
||||
<PackageReference Include="Hangfire.PostgreSql" Version="1.5.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<!-- Database -->
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="2.0.0" />
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="2.2.0" />
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL.Design" Version="1.1.1" />
|
||||
</ItemGroup>
|
||||
|
||||
<!-- Backend Website -->
|
||||
<ItemGroup>
|
||||
<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" />
|
||||
<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" />
|
||||
</ItemGroup>
|
||||
|
||||
<!-- So we can run dotnet migration ... -->
|
||||
<ItemGroup>
|
||||
<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\" />
|
||||
<DotNetCliToolReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Tools" Version="2.0.4" />
|
||||
<DotNetCliToolReference Include="Microsoft.EntityFrameworkCore.Tools.DotNet" Version="2.0.3" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
|
@ -1,25 +0,0 @@
|
|||
|
||||
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
|
|
@ -1,109 +0,0 @@
|
|||
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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,80 +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/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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,19 +0,0 @@
|
|||
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; }
|
||||
}
|
||||
}
|
|
@ -1,39 +0,0 @@
|
|||
|
||||
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?}");
|
||||
});
|
||||
}
|
|
@ -1,44 +0,0 @@
|
|||
<!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>
|
|
@ -1,271 +0,0 @@
|
|||
$(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();
|
||||
});
|
|
@ -1,68 +0,0 @@
|
|||
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);
|
||||
}
|
||||
}
|
|
@ -1,37 +0,0 @@
|
|||
<!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>
|
|
@ -1,58 +0,0 @@
|
|||
$(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();
|
||||
});
|
|
@ -0,0 +1,26 @@
|
|||
[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
|
Loading…
Reference in New Issue