Removed frontend and moved out of sln into solely csproj style
This commit is contained in:
57
Controllers/Admin.cs
Normal file
57
Controllers/Admin.cs
Normal file
@ -0,0 +1,57 @@
|
||||
using System.Linq;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Hangfire.Storage;
|
||||
|
||||
namespace YTManager.Controllers {
|
||||
[Produces("application/json")]
|
||||
[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";
|
||||
|
||||
// POST api/Admin/Trigger_Mass_Update
|
||||
// Trigger a mass channel update.
|
||||
[HttpPost("Trigger_Mass_Update")]
|
||||
public IActionResult Trigger_Mass_Update() {
|
||||
Hangfire.RecurringJob.Trigger(Mass_Updater_ID);
|
||||
return Ok();
|
||||
}
|
||||
|
||||
// POST api/Admin/Update/YTChannelID
|
||||
// Update the videos for a specific channel.
|
||||
[HttpPost("Update/{YoutubeID}")]
|
||||
public IActionResult Update([FromRoute] string YoutubeID) {
|
||||
Hangfire.BackgroundJob.Enqueue(() => Tasks.FetchVideos.ChannelUpdate(Startup.DBStr, YoutubeID));
|
||||
return Ok();
|
||||
}
|
||||
|
||||
// POST api/Admin/Start_Updater
|
||||
// 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);
|
||||
}
|
||||
|
||||
return Ok();
|
||||
}
|
||||
|
||||
// GET api/Admin/Updater
|
||||
// Check if the periodic update job is enqued.
|
||||
[HttpGet("Update")]
|
||||
public IActionResult Get_Update_Status() {
|
||||
return Ok(get_massupdatedaemon() == null ? "false" : "true");
|
||||
}
|
||||
}
|
||||
}
|
85
Controllers/Channels.cs
Normal file
85
Controllers/Channels.cs
Normal file
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
124
Controllers/Raw/Private_Channel.cs
Normal file
124
Controllers/Raw/Private_Channel.cs
Normal file
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
129
Controllers/Raw/Private_Video.cs
Normal file
129
Controllers/Raw/Private_Video.cs
Normal file
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
160
Controllers/Videos.cs
Normal file
160
Controllers/Videos.cs
Normal file
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user