mirror of
https://codeberg.org/ashley/poke.git
synced 2025-06-21 15:07:01 -04:00
owo
This commit is contained in:
parent
f431111611
commit
72143fede3
100 changed files with 12438 additions and 0 deletions
351
core/LightTube/Controllers/AccountController.cs
Normal file
351
core/LightTube/Controllers/AccountController.cs
Normal file
|
@ -0,0 +1,351 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Data;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
using System.Web;
|
||||
using InnerTube;
|
||||
using InnerTube.Models;
|
||||
using LightTube.Contexts;
|
||||
using LightTube.Database;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Primitives;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace LightTube.Controllers
|
||||
{
|
||||
public class AccountController : Controller
|
||||
{
|
||||
private readonly Youtube _youtube;
|
||||
|
||||
public AccountController(Youtube youtube)
|
||||
{
|
||||
_youtube = youtube;
|
||||
}
|
||||
|
||||
[Route("/Account")]
|
||||
public IActionResult Account()
|
||||
{
|
||||
return View(new BaseContext
|
||||
{
|
||||
MobileLayout = Utils.IsClientMobile(Request)
|
||||
});
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public IActionResult Login(string err = null)
|
||||
{
|
||||
if (HttpContext.TryGetUser(out LTUser _, "web"))
|
||||
return Redirect("/");
|
||||
|
||||
return View(new MessageContext
|
||||
{
|
||||
Message = err,
|
||||
MobileLayout = Utils.IsClientMobile(Request)
|
||||
});
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
public async Task<IActionResult> Login(string userid, string password)
|
||||
{
|
||||
if (HttpContext.TryGetUser(out LTUser _, "web"))
|
||||
return Redirect("/");
|
||||
|
||||
try
|
||||
{
|
||||
LTLogin login = await DatabaseManager.Logins.CreateToken(userid, password, Request.Headers["user-agent"], new []{"web"});
|
||||
Response.Cookies.Append("token", login.Token, new CookieOptions
|
||||
{
|
||||
Expires = DateTimeOffset.MaxValue
|
||||
});
|
||||
|
||||
return Redirect("/");
|
||||
}
|
||||
catch (KeyNotFoundException e)
|
||||
{
|
||||
return Redirect("/Account/Login?err=" + HttpUtility.UrlEncode(e.Message));
|
||||
}
|
||||
catch (UnauthorizedAccessException e)
|
||||
{
|
||||
return Redirect("/Account/Login?err=" + HttpUtility.UrlEncode(e.Message));
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<IActionResult> Logout()
|
||||
{
|
||||
if (HttpContext.Request.Cookies.TryGetValue("token", out string token))
|
||||
{
|
||||
await DatabaseManager.Logins.RemoveToken(token);
|
||||
}
|
||||
|
||||
HttpContext.Response.Cookies.Delete("token");
|
||||
HttpContext.Response.Cookies.Delete("account_data");
|
||||
return Redirect("/");
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public IActionResult Register(string err = null)
|
||||
{
|
||||
if (HttpContext.TryGetUser(out LTUser _, "web"))
|
||||
return Redirect("/");
|
||||
|
||||
return View(new MessageContext
|
||||
{
|
||||
Message = err,
|
||||
MobileLayout = Utils.IsClientMobile(Request)
|
||||
});
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
public async Task<IActionResult> Register(string userid, string password)
|
||||
{
|
||||
if (HttpContext.TryGetUser(out LTUser _, "web"))
|
||||
return Redirect("/");
|
||||
|
||||
try
|
||||
{
|
||||
await DatabaseManager.Logins.CreateUser(userid, password);
|
||||
LTLogin login = await DatabaseManager.Logins.CreateToken(userid, password, Request.Headers["user-agent"], new []{"web"});
|
||||
Response.Cookies.Append("token", login.Token, new CookieOptions
|
||||
{
|
||||
Expires = DateTimeOffset.MaxValue
|
||||
});
|
||||
|
||||
return Redirect("/");
|
||||
}
|
||||
catch (DuplicateNameException e)
|
||||
{
|
||||
return Redirect("/Account/Register?err=" + HttpUtility.UrlEncode(e.Message));
|
||||
}
|
||||
}
|
||||
|
||||
public IActionResult RegisterLocal()
|
||||
{
|
||||
if (!HttpContext.TryGetUser(out LTUser _, "web"))
|
||||
HttpContext.CreateLocalAccount();
|
||||
|
||||
return Redirect("/");
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public IActionResult Delete(string err = null)
|
||||
{
|
||||
if (!HttpContext.TryGetUser(out LTUser _, "web"))
|
||||
return Redirect("/");
|
||||
|
||||
return View(new MessageContext
|
||||
{
|
||||
Message = err,
|
||||
MobileLayout = Utils.IsClientMobile(Request)
|
||||
});
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
public async Task<IActionResult> Delete(string userid, string password)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (userid == "Local Account" && password == "local_account")
|
||||
Response.Cookies.Delete("account_data");
|
||||
else
|
||||
await DatabaseManager.Logins.DeleteUser(userid, password);
|
||||
return Redirect("/Account/Register?err=Account+deleted");
|
||||
}
|
||||
catch (KeyNotFoundException e)
|
||||
{
|
||||
return Redirect("/Account/Delete?err=" + HttpUtility.UrlEncode(e.Message));
|
||||
}
|
||||
catch (UnauthorizedAccessException e)
|
||||
{
|
||||
return Redirect("/Account/Delete?err=" + HttpUtility.UrlEncode(e.Message));
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<IActionResult> Logins()
|
||||
{
|
||||
if (!HttpContext.TryGetUser(out LTUser _, "web") || !HttpContext.Request.Cookies.TryGetValue("token", out string token))
|
||||
return Redirect("/Account/Login");
|
||||
|
||||
return View(new LoginsContext
|
||||
{
|
||||
CurrentLogin = await DatabaseManager.Logins.GetCurrentLoginId(token),
|
||||
Logins = await DatabaseManager.Logins.GetAllUserTokens(token),
|
||||
MobileLayout = Utils.IsClientMobile(Request)
|
||||
});
|
||||
}
|
||||
|
||||
public async Task<IActionResult> DisableLogin(string id)
|
||||
{
|
||||
if (!HttpContext.Request.Cookies.TryGetValue("token", out string token))
|
||||
return Redirect("/Account/Login");
|
||||
|
||||
try
|
||||
{
|
||||
await DatabaseManager.Logins.RemoveTokenFromId(token, id);
|
||||
} catch { }
|
||||
return Redirect("/Account/Logins");
|
||||
}
|
||||
|
||||
public async Task<IActionResult> Subscribe(string channel)
|
||||
{
|
||||
if (!HttpContext.TryGetUser(out LTUser user, "web"))
|
||||
return Unauthorized();
|
||||
|
||||
try
|
||||
{
|
||||
YoutubeChannel youtubeChannel = await _youtube.GetChannelAsync(channel, ChannelTabs.About);
|
||||
|
||||
(LTChannel channel, bool subscribed) result;
|
||||
result.channel = await DatabaseManager.Channels.UpdateChannel(youtubeChannel.Id, youtubeChannel.Name, youtubeChannel.Subscribers,
|
||||
youtubeChannel.Avatars.First().Url);
|
||||
|
||||
if (user.PasswordHash == "local_account")
|
||||
{
|
||||
LTChannel ltChannel = await DatabaseManager.Channels.UpdateChannel(youtubeChannel.Id, youtubeChannel.Name, youtubeChannel.Subscribers,
|
||||
youtubeChannel.Avatars.First().Url);
|
||||
if (user.SubscribedChannels.Contains(ltChannel.ChannelId))
|
||||
user.SubscribedChannels.Remove(ltChannel.ChannelId);
|
||||
else
|
||||
user.SubscribedChannels.Add(ltChannel.ChannelId);
|
||||
|
||||
HttpContext.Response.Cookies.Append("account_data", JsonConvert.SerializeObject(user),
|
||||
new CookieOptions
|
||||
{
|
||||
Expires = DateTimeOffset.MaxValue
|
||||
});
|
||||
|
||||
result.subscribed = user.SubscribedChannels.Contains(ltChannel.ChannelId);
|
||||
}
|
||||
else
|
||||
{
|
||||
result =
|
||||
await DatabaseManager.Logins.SubscribeToChannel(user, youtubeChannel);
|
||||
}
|
||||
|
||||
return Ok(result.subscribed ? "true" : "false");
|
||||
}
|
||||
catch
|
||||
{
|
||||
return Unauthorized();
|
||||
}
|
||||
}
|
||||
|
||||
public IActionResult SubscriptionsJson()
|
||||
{
|
||||
if (!HttpContext.TryGetUser(out LTUser user, "web"))
|
||||
return Json(Array.Empty<string>());
|
||||
try
|
||||
{
|
||||
return Json(user.SubscribedChannels);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return Json(Array.Empty<string>());
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<IActionResult> Settings()
|
||||
{
|
||||
if (!HttpContext.TryGetUser(out LTUser user, "web"))
|
||||
Redirect("/Account/Login");
|
||||
|
||||
if (Request.Method == "POST")
|
||||
{
|
||||
CookieOptions opts = new()
|
||||
{
|
||||
Expires = DateTimeOffset.MaxValue
|
||||
};
|
||||
foreach ((string key, StringValues value) in Request.Form)
|
||||
{
|
||||
switch (key)
|
||||
{
|
||||
case "theme":
|
||||
Response.Cookies.Append("theme", value, opts);
|
||||
break;
|
||||
case "hl":
|
||||
Response.Cookies.Append("hl", value, opts);
|
||||
break;
|
||||
case "gl":
|
||||
Response.Cookies.Append("gl", value, opts);
|
||||
break;
|
||||
case "compatibility":
|
||||
Response.Cookies.Append("compatibility", value, opts);
|
||||
break;
|
||||
case "api-access":
|
||||
await DatabaseManager.Logins.SetApiAccess(user, bool.Parse(value));
|
||||
break;
|
||||
}
|
||||
}
|
||||
return Redirect("/Account");
|
||||
}
|
||||
|
||||
YoutubeLocals locals = await _youtube.GetLocalsAsync();
|
||||
|
||||
Request.Cookies.TryGetValue("theme", out string theme);
|
||||
|
||||
bool compatibility = false;
|
||||
if (Request.Cookies.TryGetValue("compatibility", out string compatibilityString))
|
||||
bool.TryParse(compatibilityString, out compatibility);
|
||||
|
||||
return View(new SettingsContext
|
||||
{
|
||||
Languages = locals.Languages,
|
||||
Regions = locals.Regions,
|
||||
CurrentLanguage = HttpContext.GetLanguage(),
|
||||
CurrentRegion = HttpContext.GetRegion(),
|
||||
MobileLayout = Utils.IsClientMobile(Request),
|
||||
Theme = theme ?? "light",
|
||||
CompatibilityMode = compatibility,
|
||||
ApiAccess = user.ApiAccess
|
||||
});
|
||||
}
|
||||
|
||||
public async Task<IActionResult> AddVideoToPlaylist(string v)
|
||||
{
|
||||
if (!HttpContext.TryGetUser(out LTUser user, "web"))
|
||||
Redirect("/Account/Login");
|
||||
|
||||
JObject ytPlayer = await InnerTube.Utils.GetAuthorizedPlayer(v, new HttpClient());
|
||||
return View(new AddToPlaylistContext
|
||||
{
|
||||
Id = v,
|
||||
Video = await _youtube.GetVideoAsync(v, HttpContext.GetLanguage(), HttpContext.GetRegion()),
|
||||
Playlists = await DatabaseManager.Playlists.GetUserPlaylists(user.UserID),
|
||||
Thumbnail = ytPlayer?["videoDetails"]?["thumbnail"]?["thumbnails"]?[0]?["url"]?.ToString() ?? $"https://i.ytimg.com/vi_webp/{v}/maxresdefault.webp",
|
||||
MobileLayout = Utils.IsClientMobile(Request),
|
||||
});
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public IActionResult CreatePlaylist(string returnUrl = null)
|
||||
{
|
||||
if (!HttpContext.TryGetUser(out LTUser user, "web"))
|
||||
Redirect("/Account/Login");
|
||||
|
||||
return View(new BaseContext
|
||||
{
|
||||
MobileLayout = Utils.IsClientMobile(Request),
|
||||
});
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
public async Task<IActionResult> CreatePlaylist()
|
||||
{
|
||||
if (!HttpContext.TryGetUser(out LTUser user, "web"))
|
||||
Redirect("/Account/Login");
|
||||
|
||||
if (!Request.Form.ContainsKey("name") || string.IsNullOrWhiteSpace(Request.Form["name"])) return BadRequest();
|
||||
|
||||
LTPlaylist pl = await DatabaseManager.Playlists.CreatePlaylist(
|
||||
user,
|
||||
Request.Form["name"],
|
||||
string.IsNullOrWhiteSpace(Request.Form["description"]) ? "" : Request.Form["description"],
|
||||
Enum.Parse<PlaylistVisibility>(string.IsNullOrWhiteSpace(Request.Form["visibility"]) ? "UNLISTED" : Request.Form["visibility"]));
|
||||
|
||||
return Redirect($"/playlist?list={pl.Id}");
|
||||
}
|
||||
}
|
||||
}
|
187
core/LightTube/Controllers/ApiController.cs
Normal file
187
core/LightTube/Controllers/ApiController.cs
Normal file
|
@ -0,0 +1,187 @@
|
|||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using System.Xml;
|
||||
using InnerTube;
|
||||
using InnerTube.Models;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace LightTube.Controllers
|
||||
{
|
||||
[Route("/api")]
|
||||
public class ApiController : Controller
|
||||
{
|
||||
private const string VideoIdRegex = @"[a-zA-Z0-9_-]{11}";
|
||||
private const string ChannelIdRegex = @"[a-zA-Z0-9_-]{24}";
|
||||
private const string PlaylistIdRegex = @"[a-zA-Z0-9_-]{34}";
|
||||
private readonly Youtube _youtube;
|
||||
|
||||
public ApiController(Youtube youtube)
|
||||
{
|
||||
_youtube = youtube;
|
||||
}
|
||||
|
||||
private IActionResult Xml(XmlNode xmlDocument)
|
||||
{
|
||||
MemoryStream ms = new();
|
||||
ms.Write(Encoding.UTF8.GetBytes(xmlDocument.OuterXml));
|
||||
ms.Position = 0;
|
||||
HttpContext.Response.Headers.Add("Access-Control-Allow-Origin", "*");
|
||||
return File(ms, "application/xml");
|
||||
}
|
||||
|
||||
[Route("player")]
|
||||
public async Task<IActionResult> GetPlayerInfo(string v)
|
||||
{
|
||||
if (v is null)
|
||||
return GetErrorVideoPlayer("", "Missing YouTube ID (query parameter `v`)");
|
||||
|
||||
Regex regex = new(VideoIdRegex);
|
||||
if (!regex.IsMatch(v) || v.Length != 11)
|
||||
return GetErrorVideoPlayer(v, "Invalid YouTube ID " + v);
|
||||
|
||||
try
|
||||
{
|
||||
YoutubePlayer player =
|
||||
await _youtube.GetPlayerAsync(v, HttpContext.GetLanguage(), HttpContext.GetRegion());
|
||||
XmlDocument xml = player.GetXmlDocument();
|
||||
return Xml(xml);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
return GetErrorVideoPlayer(v, e.Message);
|
||||
}
|
||||
}
|
||||
|
||||
private IActionResult GetErrorVideoPlayer(string videoId, string message)
|
||||
{
|
||||
YoutubePlayer player = new()
|
||||
{
|
||||
Id = videoId,
|
||||
Title = "",
|
||||
Description = "",
|
||||
Tags = Array.Empty<string>(),
|
||||
Channel = new Channel
|
||||
{
|
||||
Name = "",
|
||||
Id = "",
|
||||
Avatars = Array.Empty<Thumbnail>()
|
||||
},
|
||||
Duration = 0,
|
||||
Chapters = Array.Empty<Chapter>(),
|
||||
Thumbnails = Array.Empty<Thumbnail>(),
|
||||
Formats = Array.Empty<Format>(),
|
||||
AdaptiveFormats = Array.Empty<Format>(),
|
||||
Subtitles = Array.Empty<Subtitle>(),
|
||||
Storyboards = Array.Empty<string>(),
|
||||
ExpiresInSeconds = "0",
|
||||
ErrorMessage = message
|
||||
};
|
||||
return Xml(player.GetXmlDocument());
|
||||
}
|
||||
|
||||
[Route("video")]
|
||||
public async Task<IActionResult> GetVideoInfo(string v)
|
||||
{
|
||||
if (v is null)
|
||||
return GetErrorVideoPlayer("", "Missing YouTube ID (query parameter `v`)");
|
||||
|
||||
Regex regex = new(VideoIdRegex);
|
||||
if (!regex.IsMatch(v) || v.Length != 11)
|
||||
{
|
||||
XmlDocument doc = new();
|
||||
XmlElement item = doc.CreateElement("Error");
|
||||
|
||||
item.InnerText = "Invalid YouTube ID " + v;
|
||||
|
||||
doc.AppendChild(item);
|
||||
return Xml(doc);
|
||||
}
|
||||
|
||||
YoutubeVideo player = await _youtube.GetVideoAsync(v, HttpContext.GetLanguage(), HttpContext.GetRegion());
|
||||
XmlDocument xml = player.GetXmlDocument();
|
||||
return Xml(xml);
|
||||
}
|
||||
|
||||
[Route("search")]
|
||||
public async Task<IActionResult> Search(string query, string continuation = null)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(query) && string.IsNullOrWhiteSpace(continuation))
|
||||
{
|
||||
XmlDocument doc = new();
|
||||
XmlElement item = doc.CreateElement("Error");
|
||||
|
||||
item.InnerText = "Invalid query " + query;
|
||||
|
||||
doc.AppendChild(item);
|
||||
return Xml(doc);
|
||||
}
|
||||
|
||||
YoutubeSearchResults player = await _youtube.SearchAsync(query, continuation, HttpContext.GetLanguage(),
|
||||
HttpContext.GetRegion());
|
||||
XmlDocument xml = player.GetXmlDocument();
|
||||
return Xml(xml);
|
||||
}
|
||||
|
||||
[Route("playlist")]
|
||||
public async Task<IActionResult> Playlist(string id, string continuation = null)
|
||||
{
|
||||
Regex regex = new(PlaylistIdRegex);
|
||||
if (!regex.IsMatch(id) || id.Length != 34) return GetErrorVideoPlayer(id, "Invalid playlist ID " + id);
|
||||
|
||||
|
||||
if (string.IsNullOrWhiteSpace(id) && string.IsNullOrWhiteSpace(continuation))
|
||||
{
|
||||
XmlDocument doc = new();
|
||||
XmlElement item = doc.CreateElement("Error");
|
||||
|
||||
item.InnerText = "Invalid ID " + id;
|
||||
|
||||
doc.AppendChild(item);
|
||||
return Xml(doc);
|
||||
}
|
||||
|
||||
YoutubePlaylist player = await _youtube.GetPlaylistAsync(id, continuation, HttpContext.GetLanguage(),
|
||||
HttpContext.GetRegion());
|
||||
XmlDocument xml = player.GetXmlDocument();
|
||||
return Xml(xml);
|
||||
}
|
||||
|
||||
[Route("channel")]
|
||||
public async Task<IActionResult> Channel(string id, ChannelTabs tab = ChannelTabs.Home,
|
||||
string continuation = null)
|
||||
{
|
||||
Regex regex = new(ChannelIdRegex);
|
||||
if (!regex.IsMatch(id) || id.Length != 24) return GetErrorVideoPlayer(id, "Invalid channel ID " + id);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(id) && string.IsNullOrWhiteSpace(continuation))
|
||||
{
|
||||
XmlDocument doc = new();
|
||||
XmlElement item = doc.CreateElement("Error");
|
||||
|
||||
item.InnerText = "Invalid ID " + id;
|
||||
|
||||
doc.AppendChild(item);
|
||||
return Xml(doc);
|
||||
}
|
||||
|
||||
YoutubeChannel player = await _youtube.GetChannelAsync(id, tab, continuation, HttpContext.GetLanguage(),
|
||||
HttpContext.GetRegion());
|
||||
XmlDocument xml = player.GetXmlDocument();
|
||||
return Xml(xml);
|
||||
}
|
||||
|
||||
[Route("trending")]
|
||||
public async Task<IActionResult> Trending(string id, string continuation = null)
|
||||
{
|
||||
YoutubeTrends player = await _youtube.GetExploreAsync(id, continuation,
|
||||
HttpContext.GetLanguage(),
|
||||
HttpContext.GetRegion());
|
||||
XmlDocument xml = player.GetXmlDocument();
|
||||
return Xml(xml);
|
||||
}
|
||||
}
|
||||
}
|
189
core/LightTube/Controllers/AuthorizedApiController.cs
Normal file
189
core/LightTube/Controllers/AuthorizedApiController.cs
Normal file
|
@ -0,0 +1,189 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using System.Xml;
|
||||
using InnerTube;
|
||||
using InnerTube.Models;
|
||||
using LightTube.Database;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Primitives;
|
||||
|
||||
namespace LightTube.Controllers
|
||||
{
|
||||
[Route("/api/auth")]
|
||||
public class AuthorizedApiController : Controller
|
||||
{
|
||||
private readonly Youtube _youtube;
|
||||
|
||||
private IReadOnlyList<string> _scopes = new[]
|
||||
{
|
||||
"api.subscriptions.read",
|
||||
"api.subscriptions.write"
|
||||
};
|
||||
|
||||
public AuthorizedApiController(Youtube youtube)
|
||||
{
|
||||
_youtube = youtube;
|
||||
}
|
||||
|
||||
private IActionResult Xml(XmlNode xmlDocument, HttpStatusCode statusCode)
|
||||
{
|
||||
MemoryStream ms = new();
|
||||
ms.Write(Encoding.UTF8.GetBytes(xmlDocument.OuterXml));
|
||||
ms.Position = 0;
|
||||
HttpContext.Response.Headers.Add("Access-Control-Allow-Origin", "*");
|
||||
Response.StatusCode = (int)statusCode;
|
||||
return File(ms, "application/xml");
|
||||
}
|
||||
|
||||
private XmlNode BuildErrorXml(string message)
|
||||
{
|
||||
XmlDocument doc = new();
|
||||
XmlElement error = doc.CreateElement("Error");
|
||||
error.InnerText = message;
|
||||
doc.AppendChild(error);
|
||||
return doc;
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[Route("getToken")]
|
||||
public async Task<IActionResult> GetToken()
|
||||
{
|
||||
if (!Request.Headers.TryGetValue("User-Agent", out StringValues userAgent))
|
||||
return Xml(BuildErrorXml("Missing User-Agent header"), HttpStatusCode.BadRequest);
|
||||
|
||||
Match match = Regex.Match(userAgent.ToString(), DatabaseManager.ApiUaRegex);
|
||||
if (!match.Success)
|
||||
return Xml(BuildErrorXml("Bad User-Agent header. Please see 'Documentation/API requests'"), HttpStatusCode.BadRequest);
|
||||
if (match.Groups[1].ToString() != "1.0")
|
||||
return Xml(BuildErrorXml($"Unknown API version {match.Groups[1]}"), HttpStatusCode.BadRequest);
|
||||
|
||||
if (!Request.Form.TryGetValue("user", out StringValues user))
|
||||
return Xml(BuildErrorXml("Missing request value: 'user'"), HttpStatusCode.BadRequest);
|
||||
if (!Request.Form.TryGetValue("password", out StringValues password))
|
||||
return Xml(BuildErrorXml("Missing request value: 'password'"), HttpStatusCode.BadRequest);
|
||||
if (!Request.Form.TryGetValue("scopes", out StringValues scopes))
|
||||
return Xml(BuildErrorXml("Missing request value: 'scopes'"), HttpStatusCode.BadRequest);
|
||||
|
||||
string[] newScopes = scopes.First().Split(",");
|
||||
foreach (string s in newScopes)
|
||||
if (!_scopes.Contains(s))
|
||||
return Xml(BuildErrorXml($"Unknown scope '{s}'"), HttpStatusCode.BadRequest);
|
||||
|
||||
try
|
||||
{
|
||||
LTLogin ltLogin =
|
||||
await DatabaseManager.Logins.CreateToken(user, password, userAgent.ToString(),
|
||||
scopes.First().Split(","));
|
||||
return Xml(ltLogin.GetXmlElement(), HttpStatusCode.Created);
|
||||
}
|
||||
catch (UnauthorizedAccessException)
|
||||
{
|
||||
return Xml(BuildErrorXml("Invalid credentials"), HttpStatusCode.Unauthorized);
|
||||
}
|
||||
catch (InvalidOperationException)
|
||||
{
|
||||
return Xml(BuildErrorXml("User has API access disabled"), HttpStatusCode.Forbidden);
|
||||
}
|
||||
}
|
||||
|
||||
[Route("subscriptions/feed")]
|
||||
public async Task<IActionResult> SubscriptionsFeed()
|
||||
{
|
||||
if (!HttpContext.TryGetUser(out LTUser user, "api.subscriptions.read"))
|
||||
return Xml(BuildErrorXml("Unauthorized"), HttpStatusCode.Unauthorized);
|
||||
|
||||
SubscriptionFeed feed = new()
|
||||
{
|
||||
videos = await YoutubeRSS.GetMultipleFeeds(user.SubscribedChannels)
|
||||
};
|
||||
|
||||
return Xml(feed.GetXmlDocument(), HttpStatusCode.OK);
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("subscriptions/channels")]
|
||||
public IActionResult SubscriptionsChannels()
|
||||
{
|
||||
if (!HttpContext.TryGetUser(out LTUser user, "api.subscriptions.read"))
|
||||
return Xml(BuildErrorXml("Unauthorized"), HttpStatusCode.Unauthorized);
|
||||
|
||||
SubscriptionChannels feed = new()
|
||||
{
|
||||
Channels = user.SubscribedChannels.Select(DatabaseManager.Channels.GetChannel).ToArray()
|
||||
};
|
||||
Array.Sort(feed.Channels, (p, q) => string.Compare(p.Name, q.Name, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
return Xml(feed.GetXmlDocument(), HttpStatusCode.OK);
|
||||
}
|
||||
|
||||
[HttpPut]
|
||||
[Route("subscriptions/channels")]
|
||||
public async Task<IActionResult> Subscribe()
|
||||
{
|
||||
if (!HttpContext.TryGetUser(out LTUser user, "api.subscriptions.write"))
|
||||
return Xml(BuildErrorXml("Unauthorized"), HttpStatusCode.Unauthorized);
|
||||
|
||||
Request.Form.TryGetValue("id", out StringValues ids);
|
||||
string id = ids.ToString();
|
||||
|
||||
if (user.SubscribedChannels.Contains(id))
|
||||
return StatusCode((int)HttpStatusCode.NotModified);
|
||||
|
||||
try
|
||||
{
|
||||
YoutubeChannel channel = await _youtube.GetChannelAsync(id);
|
||||
|
||||
if (channel.Id is null)
|
||||
return StatusCode((int)HttpStatusCode.NotFound);
|
||||
|
||||
(LTChannel ltChannel, bool _) = await DatabaseManager.Logins.SubscribeToChannel(user, channel);
|
||||
|
||||
XmlDocument doc = new();
|
||||
doc.AppendChild(ltChannel.GetXmlElement(doc));
|
||||
return Xml(doc, HttpStatusCode.OK);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
return Xml(BuildErrorXml(e.Message), HttpStatusCode.InternalServerError);
|
||||
}
|
||||
}
|
||||
|
||||
[HttpDelete]
|
||||
[Route("subscriptions/channels")]
|
||||
public async Task<IActionResult> Unsubscribe()
|
||||
{
|
||||
if (!HttpContext.TryGetUser(out LTUser user, "api.subscriptions.write"))
|
||||
return Xml(BuildErrorXml("Unauthorized"), HttpStatusCode.Unauthorized);
|
||||
|
||||
Request.Form.TryGetValue("id", out StringValues ids);
|
||||
string id = ids.ToString();
|
||||
|
||||
if (!user.SubscribedChannels.Contains(id))
|
||||
return StatusCode((int)HttpStatusCode.NotModified);
|
||||
|
||||
try
|
||||
{
|
||||
YoutubeChannel channel = await _youtube.GetChannelAsync(id);
|
||||
|
||||
if (channel.Id is null)
|
||||
return StatusCode((int)HttpStatusCode.NotFound);
|
||||
|
||||
(LTChannel ltChannel, bool _) = await DatabaseManager.Logins.SubscribeToChannel(user, channel);
|
||||
|
||||
XmlDocument doc = new();
|
||||
doc.AppendChild(ltChannel.GetXmlElement(doc));
|
||||
return Xml(doc, HttpStatusCode.OK);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
return Xml(BuildErrorXml(e.Message), HttpStatusCode.InternalServerError);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
104
core/LightTube/Controllers/FeedController.cs
Normal file
104
core/LightTube/Controllers/FeedController.cs
Normal file
|
@ -0,0 +1,104 @@
|
|||
using System;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using LightTube.Contexts;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using InnerTube;
|
||||
using LightTube.Database;
|
||||
|
||||
namespace LightTube.Controllers
|
||||
{
|
||||
[Route("/feed")]
|
||||
public class FeedController : Controller
|
||||
{
|
||||
private readonly ILogger<FeedController> _logger;
|
||||
private readonly Youtube _youtube;
|
||||
|
||||
public FeedController(ILogger<FeedController> logger, Youtube youtube)
|
||||
{
|
||||
_logger = logger;
|
||||
_youtube = youtube;
|
||||
}
|
||||
|
||||
[Route("subscriptions")]
|
||||
public async Task<IActionResult> Subscriptions()
|
||||
{
|
||||
if (!HttpContext.TryGetUser(out LTUser user, "web"))
|
||||
return Redirect("/Account/Login");
|
||||
|
||||
try
|
||||
{
|
||||
FeedContext context = new()
|
||||
{
|
||||
Channels = user.SubscribedChannels.Select(DatabaseManager.Channels.GetChannel).ToArray(),
|
||||
Videos = await YoutubeRSS.GetMultipleFeeds(user.SubscribedChannels),
|
||||
RssToken = user.RssToken,
|
||||
MobileLayout = Utils.IsClientMobile(Request)
|
||||
};
|
||||
Array.Sort(context.Channels, (p, q) => string.Compare(p.Name, q.Name, StringComparison.OrdinalIgnoreCase));
|
||||
return View(context);
|
||||
}
|
||||
catch
|
||||
{
|
||||
HttpContext.Response.Cookies.Delete("token");
|
||||
return Redirect("/Account/Login");
|
||||
}
|
||||
}
|
||||
|
||||
[Route("channels")]
|
||||
public IActionResult Channels()
|
||||
{
|
||||
if (!HttpContext.TryGetUser(out LTUser user, "web"))
|
||||
return Redirect("/Account/Login");
|
||||
|
||||
try
|
||||
{
|
||||
FeedContext context = new()
|
||||
{
|
||||
Channels = user.SubscribedChannels.Select(DatabaseManager.Channels.GetChannel).ToArray(),
|
||||
Videos = null,
|
||||
MobileLayout = Utils.IsClientMobile(Request)
|
||||
};
|
||||
Array.Sort(context.Channels, (p, q) => string.Compare(p.Name, q.Name, StringComparison.OrdinalIgnoreCase));
|
||||
return View(context);
|
||||
}
|
||||
catch
|
||||
{
|
||||
HttpContext.Response.Cookies.Delete("token");
|
||||
return Redirect("/Account/Login");
|
||||
}
|
||||
}
|
||||
|
||||
[Route("explore")]
|
||||
public IActionResult Explore()
|
||||
{
|
||||
return View(new BaseContext
|
||||
{
|
||||
MobileLayout = Utils.IsClientMobile(Request)
|
||||
});
|
||||
}
|
||||
|
||||
[Route("/feed/library")]
|
||||
public async Task<IActionResult> Playlists()
|
||||
{
|
||||
if (!HttpContext.TryGetUser(out LTUser user, "web"))
|
||||
Redirect("/Account/Login");
|
||||
|
||||
return View(new PlaylistsContext
|
||||
{
|
||||
MobileLayout = Utils.IsClientMobile(Request),
|
||||
Playlists = await DatabaseManager.Playlists.GetUserPlaylists(user.UserID)
|
||||
});
|
||||
}
|
||||
|
||||
[Route("/rss")]
|
||||
public async Task<IActionResult> Playlists(string token, int limit = 15)
|
||||
{
|
||||
if (!DatabaseManager.TryGetRssUser(token, out LTUser user))
|
||||
return Unauthorized();
|
||||
return File(Encoding.UTF8.GetBytes(await user.GenerateRssFeed(Request.Host.ToString(), Math.Clamp(limit, 0, 50))), "application/xml");
|
||||
}
|
||||
}
|
||||
}
|
50
core/LightTube/Controllers/HomeController.cs
Normal file
50
core/LightTube/Controllers/HomeController.cs
Normal file
|
@ -0,0 +1,50 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using LightTube.Contexts;
|
||||
using LightTube.Models;
|
||||
using Microsoft.AspNetCore.Diagnostics;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Primitives;
|
||||
using InnerTube;
|
||||
using InnerTube.Models;
|
||||
using ErrorContext = LightTube.Contexts.ErrorContext;
|
||||
|
||||
namespace LightTube.Controllers
|
||||
{
|
||||
public class HomeController : Controller
|
||||
{
|
||||
private readonly ILogger<HomeController> _logger;
|
||||
private readonly Youtube _youtube;
|
||||
|
||||
public HomeController(ILogger<HomeController> logger, Youtube youtube)
|
||||
{
|
||||
_logger = logger;
|
||||
_youtube = youtube;
|
||||
}
|
||||
|
||||
public IActionResult Index()
|
||||
{
|
||||
return View(new BaseContext
|
||||
{
|
||||
MobileLayout = Utils.IsClientMobile(Request)
|
||||
});
|
||||
}
|
||||
|
||||
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
|
||||
public IActionResult Error()
|
||||
{
|
||||
return View(new ErrorContext
|
||||
{
|
||||
Path = HttpContext.Features.Get<IExceptionHandlerPathFeature>().Path,
|
||||
MobileLayout = Utils.IsClientMobile(Request)
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
62
core/LightTube/Controllers/ManifestController.cs
Normal file
62
core/LightTube/Controllers/ManifestController.cs
Normal file
|
@ -0,0 +1,62 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using InnerTube;
|
||||
using InnerTube.Models;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace LightTube.Controllers
|
||||
{
|
||||
[Route("/manifest")]
|
||||
public class ManifestController : Controller
|
||||
{
|
||||
private readonly Youtube _youtube;
|
||||
private readonly HttpClient _client = new();
|
||||
|
||||
public ManifestController(Youtube youtube)
|
||||
{
|
||||
_youtube = youtube;
|
||||
}
|
||||
|
||||
[Route("{v}")]
|
||||
public async Task<IActionResult> DefaultManifest(string v)
|
||||
{
|
||||
YoutubePlayer player = await _youtube.GetPlayerAsync(v, HttpContext.GetLanguage(), HttpContext.GetRegion());
|
||||
if (!string.IsNullOrWhiteSpace(player.ErrorMessage))
|
||||
return StatusCode(500, player.ErrorMessage);
|
||||
return Redirect(player.IsLive ? $"/manifest/{v}.m3u8" : $"/manifest/{v}.mpd" + Request.QueryString);
|
||||
}
|
||||
|
||||
[Route("{v}.mpd")]
|
||||
public async Task<IActionResult> DashManifest(string v, string videoCodec = null, string audioCodec = null, bool useProxy = true)
|
||||
{
|
||||
YoutubePlayer player = await _youtube.GetPlayerAsync(v, HttpContext.GetLanguage(), HttpContext.GetRegion());
|
||||
string manifest = player.GetMpdManifest(useProxy ? $"https://{Request.Host}/proxy/" : null, videoCodec, audioCodec);
|
||||
return File(Encoding.UTF8.GetBytes(manifest), "application/dash+xml");
|
||||
}
|
||||
|
||||
[Route("{v}.m3u8")]
|
||||
public async Task<IActionResult> HlsManifest(string v, bool useProxy = true)
|
||||
{
|
||||
YoutubePlayer player = await _youtube.GetPlayerAsync(v, HttpContext.GetLanguage(), HttpContext.GetRegion(), true);
|
||||
if (!string.IsNullOrWhiteSpace(player.ErrorMessage))
|
||||
return StatusCode(403, player.ErrorMessage);
|
||||
|
||||
if (player.IsLive)
|
||||
{
|
||||
string manifest = await player.GetHlsManifest(useProxy ? $"https://{Request.Host}/proxy" : null);
|
||||
return File(Encoding.UTF8.GetBytes(manifest), "application/vnd.apple.mpegurl");
|
||||
}
|
||||
|
||||
if (useProxy)
|
||||
return StatusCode(400, "HLS proxy for non-live videos are not supported at the moment.");
|
||||
return Redirect(player.HlsManifestUrl);
|
||||
}
|
||||
}
|
||||
}
|
517
core/LightTube/Controllers/ProxyController.cs
Normal file
517
core/LightTube/Controllers/ProxyController.cs
Normal file
|
@ -0,0 +1,517 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using System.Web;
|
||||
using InnerTube;
|
||||
using InnerTube.Models;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Primitives;
|
||||
|
||||
namespace LightTube.Controllers
|
||||
{
|
||||
[Route("/proxy")]
|
||||
public class ProxyController : Controller
|
||||
{
|
||||
private readonly ILogger<YoutubeController> _logger;
|
||||
private readonly Youtube _youtube;
|
||||
private string[] BlockedHeaders =
|
||||
{
|
||||
"host",
|
||||
"cookies"
|
||||
};
|
||||
|
||||
public ProxyController(ILogger<YoutubeController> logger, Youtube youtube)
|
||||
{
|
||||
_logger = logger;
|
||||
_youtube = youtube;
|
||||
}
|
||||
|
||||
[Route("media/{videoId}/{formatId}")]
|
||||
public async Task Media(string videoId, string formatId)
|
||||
{
|
||||
try
|
||||
{
|
||||
YoutubePlayer player = await _youtube.GetPlayerAsync(videoId);
|
||||
if (!string.IsNullOrWhiteSpace(player.ErrorMessage))
|
||||
{
|
||||
Response.StatusCode = (int) HttpStatusCode.InternalServerError;
|
||||
await Response.Body.WriteAsync(Encoding.UTF8.GetBytes(player.ErrorMessage));
|
||||
await Response.StartAsync();
|
||||
return;
|
||||
}
|
||||
|
||||
List<Format> formats = new();
|
||||
|
||||
formats.AddRange(player.Formats);
|
||||
formats.AddRange(player.AdaptiveFormats);
|
||||
|
||||
if (!formats.Any(x => x.FormatId == formatId))
|
||||
{
|
||||
Response.StatusCode = (int) HttpStatusCode.NotFound;
|
||||
await Response.Body.WriteAsync(Encoding.UTF8.GetBytes(
|
||||
$"Format with ID {formatId} not found.\nAvailable IDs are: {string.Join(", ", formats.Select(x => x.FormatId.ToString()))}"));
|
||||
await Response.StartAsync();
|
||||
return;
|
||||
}
|
||||
|
||||
string url = formats.First(x => x.FormatId == formatId).Url;
|
||||
|
||||
if (!url.StartsWith("http://") && !url.StartsWith("https://"))
|
||||
url = "https://" + url;
|
||||
|
||||
HttpWebRequest request = (HttpWebRequest) WebRequest.Create(url);
|
||||
request.AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate;
|
||||
request.Method = Request.Method;
|
||||
|
||||
foreach ((string header, StringValues values) in HttpContext.Request.Headers.Where(header =>
|
||||
!header.Key.StartsWith(":") && !BlockedHeaders.Contains(header.Key.ToLower())))
|
||||
foreach (string value in values)
|
||||
request.Headers.Add(header, value);
|
||||
|
||||
HttpWebResponse response;
|
||||
|
||||
try
|
||||
{
|
||||
response = (HttpWebResponse) request.GetResponse();
|
||||
}
|
||||
catch (WebException e)
|
||||
{
|
||||
response = e.Response as HttpWebResponse;
|
||||
}
|
||||
|
||||
if (response == null)
|
||||
await Response.StartAsync();
|
||||
|
||||
foreach (string header in response.Headers.AllKeys)
|
||||
if (Response.Headers.ContainsKey(header))
|
||||
Response.Headers[header] = response.Headers.Get(header);
|
||||
else
|
||||
Response.Headers.Add(header, response.Headers.Get(header));
|
||||
Response.StatusCode = (int) response.StatusCode;
|
||||
|
||||
await using Stream stream = response.GetResponseStream();
|
||||
try
|
||||
{
|
||||
await stream.CopyToAsync(Response.Body, HttpContext.RequestAborted);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// an exception is thrown if the client suddenly stops streaming
|
||||
}
|
||||
|
||||
await Response.StartAsync();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Response.StatusCode = (int) HttpStatusCode.InternalServerError;
|
||||
await Response.Body.WriteAsync(Encoding.UTF8.GetBytes(e.ToString()));
|
||||
await Response.StartAsync();
|
||||
}
|
||||
}
|
||||
|
||||
[Route("download/{videoId}/{formatId}/{filename}")]
|
||||
public async Task Download(string videoId, string formatId, string filename)
|
||||
{
|
||||
try
|
||||
{
|
||||
YoutubePlayer player = await _youtube.GetPlayerAsync(videoId);
|
||||
if (!string.IsNullOrWhiteSpace(player.ErrorMessage))
|
||||
{
|
||||
Response.StatusCode = (int) HttpStatusCode.InternalServerError;
|
||||
await Response.Body.WriteAsync(Encoding.UTF8.GetBytes(player.ErrorMessage));
|
||||
await Response.StartAsync();
|
||||
return;
|
||||
}
|
||||
|
||||
List<Format> formats = new();
|
||||
|
||||
formats.AddRange(player.Formats);
|
||||
formats.AddRange(player.AdaptiveFormats);
|
||||
|
||||
if (!formats.Any(x => x.FormatId == formatId))
|
||||
{
|
||||
Response.StatusCode = (int) HttpStatusCode.NotFound;
|
||||
await Response.Body.WriteAsync(Encoding.UTF8.GetBytes(
|
||||
$"Format with ID {formatId} not found.\nAvailable IDs are: {string.Join(", ", formats.Select(x => x.FormatId.ToString()))}"));
|
||||
await Response.StartAsync();
|
||||
return;
|
||||
}
|
||||
|
||||
string url = formats.First(x => x.FormatId == formatId).Url;
|
||||
|
||||
if (!url.StartsWith("http://") && !url.StartsWith("https://"))
|
||||
url = "https://" + url;
|
||||
|
||||
HttpWebRequest request = (HttpWebRequest) WebRequest.Create(url);
|
||||
request.AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate;
|
||||
request.Method = Request.Method;
|
||||
|
||||
foreach ((string header, StringValues values) in HttpContext.Request.Headers.Where(header =>
|
||||
!header.Key.StartsWith(":") && !BlockedHeaders.Contains(header.Key.ToLower())))
|
||||
foreach (string value in values)
|
||||
request.Headers.Add(header, value);
|
||||
|
||||
HttpWebResponse response;
|
||||
|
||||
try
|
||||
{
|
||||
response = (HttpWebResponse) request.GetResponse();
|
||||
}
|
||||
catch (WebException e)
|
||||
{
|
||||
response = e.Response as HttpWebResponse;
|
||||
}
|
||||
|
||||
if (response == null)
|
||||
await Response.StartAsync();
|
||||
|
||||
foreach (string header in response.Headers.AllKeys)
|
||||
if (Response.Headers.ContainsKey(header))
|
||||
Response.Headers[header] = response.Headers.Get(header);
|
||||
else
|
||||
Response.Headers.Add(header, response.Headers.Get(header));
|
||||
Response.Headers.Add("Content-Disposition", $"attachment; filename=\"{Regex.Replace(filename, @"[^\u0000-\u007F]+", string.Empty)}\"");
|
||||
Response.StatusCode = (int) response.StatusCode;
|
||||
|
||||
await using Stream stream = response.GetResponseStream();
|
||||
try
|
||||
{
|
||||
await stream.CopyToAsync(Response.Body, HttpContext.RequestAborted);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// an exception is thrown if the client suddenly stops streaming
|
||||
}
|
||||
|
||||
await Response.StartAsync();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Response.StatusCode = (int) HttpStatusCode.InternalServerError;
|
||||
await Response.Body.WriteAsync(Encoding.UTF8.GetBytes(e.ToString()));
|
||||
await Response.StartAsync();
|
||||
}
|
||||
}
|
||||
|
||||
[Route("caption/{videoId}/{language}")]
|
||||
public async Task<FileStreamResult> SubtitleProxy(string videoId, string language)
|
||||
{
|
||||
YoutubePlayer player = await _youtube.GetPlayerAsync(videoId);
|
||||
if (!string.IsNullOrWhiteSpace(player.ErrorMessage))
|
||||
{
|
||||
Response.StatusCode = (int) HttpStatusCode.InternalServerError;
|
||||
return File(new MemoryStream(Encoding.UTF8.GetBytes(player.ErrorMessage)),
|
||||
"text/plain");
|
||||
}
|
||||
|
||||
string url = null;
|
||||
Subtitle? subtitle = player.Subtitles.FirstOrDefault(x => string.Equals(x.Language, language, StringComparison.InvariantCultureIgnoreCase));
|
||||
if (subtitle is null)
|
||||
{
|
||||
Response.StatusCode = (int) HttpStatusCode.NotFound;
|
||||
return File(
|
||||
new MemoryStream(Encoding.UTF8.GetBytes(
|
||||
$"There are no available subtitles for {language}. Available language codes are: {string.Join(", ", player.Subtitles.Select(x => $"\"{x.Language}\""))}")),
|
||||
"text/plain");
|
||||
}
|
||||
url = subtitle.Url.Replace("fmt=srv3", "fmt=vtt");
|
||||
|
||||
if (!url.StartsWith("http://") && !url.StartsWith("https://"))
|
||||
url = "https://" + url;
|
||||
|
||||
HttpWebRequest request = (HttpWebRequest)WebRequest.Create(url);
|
||||
request.AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate;
|
||||
|
||||
foreach ((string header, StringValues values) in HttpContext.Request.Headers.Where(header =>
|
||||
!header.Key.StartsWith(":") && !BlockedHeaders.Contains(header.Key.ToLower())))
|
||||
foreach (string value in values)
|
||||
request.Headers.Add(header, value);
|
||||
|
||||
using HttpWebResponse response = (HttpWebResponse)request.GetResponse();
|
||||
|
||||
await using Stream stream = response.GetResponseStream();
|
||||
using StreamReader reader = new(stream);
|
||||
|
||||
return File(new MemoryStream(Encoding.UTF8.GetBytes(await reader.ReadToEndAsync())),
|
||||
"text/vtt");
|
||||
}
|
||||
|
||||
[Route("image")]
|
||||
[Obsolete("Use /proxy/thumbnail instead")]
|
||||
public async Task ImageProxy(string url)
|
||||
{
|
||||
if (!url.StartsWith("http://") && !url.StartsWith("https://"))
|
||||
url = "https://" + url;
|
||||
|
||||
HttpWebRequest request = (HttpWebRequest)WebRequest.Create(url);
|
||||
request.AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate;
|
||||
|
||||
foreach ((string header, StringValues values) in HttpContext.Request.Headers.Where(header =>
|
||||
!header.Key.StartsWith(":") && !BlockedHeaders.Contains(header.Key.ToLower())))
|
||||
foreach (string value in values)
|
||||
request.Headers.Add(header, value);
|
||||
|
||||
using HttpWebResponse response = (HttpWebResponse)request.GetResponse();
|
||||
|
||||
foreach (string header in response.Headers.AllKeys)
|
||||
if (Response.Headers.ContainsKey(header))
|
||||
Response.Headers[header] = response.Headers.Get(header);
|
||||
else
|
||||
Response.Headers.Add(header, response.Headers.Get(header));
|
||||
Response.StatusCode = (int)response.StatusCode;
|
||||
|
||||
await using Stream stream = response.GetResponseStream();
|
||||
await stream.CopyToAsync(Response.Body);
|
||||
await Response.StartAsync();
|
||||
}
|
||||
|
||||
[Route("thumbnail/{videoId}/{index:int}")]
|
||||
public async Task ThumbnailProxy(string videoId, int index = 0)
|
||||
{
|
||||
YoutubePlayer player = await _youtube.GetPlayerAsync(videoId);
|
||||
if (index == -1) index = player.Thumbnails.Length - 1;
|
||||
if (index >= player.Thumbnails.Length)
|
||||
{
|
||||
Response.StatusCode = 404;
|
||||
await Response.Body.WriteAsync(Encoding.UTF8.GetBytes(
|
||||
$"Cannot find thumbnail #{index} for {videoId}. The maximum quality is {player.Thumbnails.Length - 1}"));
|
||||
await Response.StartAsync();
|
||||
return;
|
||||
}
|
||||
|
||||
string url = player.Thumbnails.FirstOrDefault()?.Url;
|
||||
|
||||
if (!url.StartsWith("http://") && !url.StartsWith("https://"))
|
||||
url = "https://" + url;
|
||||
|
||||
HttpWebRequest request = (HttpWebRequest)WebRequest.Create(url);
|
||||
request.AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate;
|
||||
|
||||
foreach ((string header, StringValues values) in HttpContext.Request.Headers.Where(header =>
|
||||
!header.Key.StartsWith(":") && !BlockedHeaders.Contains(header.Key.ToLower())))
|
||||
foreach (string value in values)
|
||||
request.Headers.Add(header, value);
|
||||
|
||||
using HttpWebResponse response = (HttpWebResponse)request.GetResponse();
|
||||
|
||||
foreach (string header in response.Headers.AllKeys)
|
||||
if (Response.Headers.ContainsKey(header))
|
||||
Response.Headers[header] = response.Headers.Get(header);
|
||||
else
|
||||
Response.Headers.Add(header, response.Headers.Get(header));
|
||||
Response.StatusCode = (int)response.StatusCode;
|
||||
|
||||
await using Stream stream = response.GetResponseStream();
|
||||
await stream.CopyToAsync(Response.Body);
|
||||
await Response.StartAsync();
|
||||
}
|
||||
|
||||
[Route("storyboard/{videoId}")]
|
||||
public async Task StoryboardProxy(string videoId)
|
||||
{
|
||||
try
|
||||
{
|
||||
YoutubePlayer player = await _youtube.GetPlayerAsync(videoId);
|
||||
if (!string.IsNullOrWhiteSpace(player.ErrorMessage))
|
||||
{
|
||||
Response.StatusCode = (int) HttpStatusCode.InternalServerError;
|
||||
await Response.Body.WriteAsync(Encoding.UTF8.GetBytes(player.ErrorMessage));
|
||||
await Response.StartAsync();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!player.Storyboards.Any())
|
||||
{
|
||||
Response.StatusCode = (int) HttpStatusCode.NotFound;
|
||||
await Response.Body.WriteAsync(Encoding.UTF8.GetBytes("No usable storyboard found."));
|
||||
await Response.StartAsync();
|
||||
return;
|
||||
}
|
||||
|
||||
string url = player.Storyboards.First();
|
||||
|
||||
HttpWebRequest request = (HttpWebRequest)WebRequest.Create(url);
|
||||
request.AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate;
|
||||
|
||||
foreach ((string header, StringValues values) in HttpContext.Request.Headers.Where(header =>
|
||||
!header.Key.StartsWith(":") && !BlockedHeaders.Contains(header.Key.ToLower())))
|
||||
foreach (string value in values)
|
||||
request.Headers.Add(header, value);
|
||||
|
||||
using HttpWebResponse response = (HttpWebResponse)request.GetResponse();
|
||||
|
||||
foreach (string header in response.Headers.AllKeys)
|
||||
if (Response.Headers.ContainsKey(header))
|
||||
Response.Headers[header] = response.Headers.Get(header);
|
||||
else
|
||||
Response.Headers.Add(header, response.Headers.Get(header));
|
||||
Response.StatusCode = (int)response.StatusCode;
|
||||
|
||||
await using Stream stream = response.GetResponseStream();
|
||||
await stream.CopyToAsync(Response.Body);
|
||||
await Response.StartAsync();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Response.StatusCode = (int) HttpStatusCode.InternalServerError;
|
||||
await Response.Body.WriteAsync(Encoding.UTF8.GetBytes(e.ToString()));
|
||||
await Response.StartAsync();
|
||||
}
|
||||
}
|
||||
|
||||
[Route("hls")]
|
||||
public async Task<IActionResult> HlsProxy(string url)
|
||||
{
|
||||
if (!url.StartsWith("http://") && !url.StartsWith("https://"))
|
||||
url = "https://" + url;
|
||||
|
||||
HttpWebRequest request = (HttpWebRequest)WebRequest.Create(url);
|
||||
request.AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate;
|
||||
|
||||
foreach ((string header, StringValues values) in HttpContext.Request.Headers.Where(header =>
|
||||
!header.Key.StartsWith(":") && !BlockedHeaders.Contains(header.Key.ToLower())))
|
||||
foreach (string value in values)
|
||||
request.Headers.Add(header, value);
|
||||
|
||||
using HttpWebResponse response = (HttpWebResponse)request.GetResponse();
|
||||
|
||||
await using Stream stream = response.GetResponseStream();
|
||||
using StreamReader reader = new(stream);
|
||||
string manifest = await reader.ReadToEndAsync();
|
||||
StringBuilder proxyManifest = new ();
|
||||
|
||||
foreach (string s in manifest.Split("\n"))
|
||||
{
|
||||
// also check if proxy enabled
|
||||
proxyManifest.AppendLine(!s.StartsWith("http")
|
||||
? s
|
||||
: $"https://{Request.Host}/proxy/video?url={HttpUtility.UrlEncode(s)}");
|
||||
}
|
||||
|
||||
return File(new MemoryStream(Encoding.UTF8.GetBytes(proxyManifest.ToString())),
|
||||
"application/vnd.apple.mpegurl");
|
||||
}
|
||||
|
||||
[Route("manifest/{videoId}")]
|
||||
public async Task<IActionResult> ManifestProxy(string videoId, string formatId, bool useProxy = true)
|
||||
{
|
||||
YoutubePlayer player = await _youtube.GetPlayerAsync(videoId, iOS: true);
|
||||
if (!string.IsNullOrWhiteSpace(player.ErrorMessage))
|
||||
{
|
||||
Response.StatusCode = (int) HttpStatusCode.InternalServerError;
|
||||
return File(new MemoryStream(Encoding.UTF8.GetBytes(player.ErrorMessage)),
|
||||
"text/plain");
|
||||
}
|
||||
|
||||
if (player.HlsManifestUrl == null)
|
||||
{
|
||||
Response.StatusCode = (int) HttpStatusCode.NotFound;
|
||||
return File(new MemoryStream(Encoding.UTF8.GetBytes("This video does not have an HLS manifest URL")),
|
||||
"text/plain");
|
||||
}
|
||||
|
||||
string url = player.HlsManifestUrl;
|
||||
|
||||
if (!url.StartsWith("http://") && !url.StartsWith("https://"))
|
||||
url = "https://" + url;
|
||||
|
||||
HttpWebRequest request = (HttpWebRequest)WebRequest.Create(url);
|
||||
request.AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate;
|
||||
|
||||
foreach ((string header, StringValues values) in HttpContext.Request.Headers.Where(header =>
|
||||
!header.Key.StartsWith(":") && !BlockedHeaders.Contains(header.Key.ToLower())))
|
||||
foreach (string value in values)
|
||||
request.Headers.Add(header, value);
|
||||
|
||||
using HttpWebResponse response = (HttpWebResponse)request.GetResponse();
|
||||
|
||||
await using Stream stream = response.GetResponseStream();
|
||||
using StreamReader reader = new(stream);
|
||||
string manifest = await reader.ReadToEndAsync();
|
||||
StringBuilder proxyManifest = new ();
|
||||
|
||||
if (useProxy)
|
||||
foreach (string s in manifest.Split("\n"))
|
||||
{
|
||||
// also check if proxy enabled
|
||||
proxyManifest.AppendLine(!s.StartsWith("http")
|
||||
? s
|
||||
: $"https://{Request.Host}/proxy/ytmanifest?path=" + HttpUtility.UrlEncode(s[46..]));
|
||||
}
|
||||
else
|
||||
proxyManifest.Append(manifest);
|
||||
|
||||
return File(new MemoryStream(Encoding.UTF8.GetBytes(proxyManifest.ToString())),
|
||||
"application/vnd.apple.mpegurl");
|
||||
}
|
||||
|
||||
[Route("ytmanifest")]
|
||||
public async Task<IActionResult> YoutubeManifestProxy(string path)
|
||||
{
|
||||
string url = "https://manifest.googlevideo.com" + path;
|
||||
StringBuilder sb = new();
|
||||
|
||||
if (!url.StartsWith("http://") && !url.StartsWith("https://"))
|
||||
url = "https://" + url;
|
||||
|
||||
HttpWebRequest request = (HttpWebRequest)WebRequest.Create(url);
|
||||
request.AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate;
|
||||
|
||||
foreach ((string header, StringValues values) in HttpContext.Request.Headers.Where(header =>
|
||||
!header.Key.StartsWith(":") && !BlockedHeaders.Contains(header.Key.ToLower())))
|
||||
foreach (string value in values)
|
||||
request.Headers.Add(header, value);
|
||||
|
||||
using HttpWebResponse response = (HttpWebResponse)request.GetResponse();
|
||||
|
||||
await using Stream stream = response.GetResponseStream();
|
||||
using StreamReader reader = new(stream);
|
||||
string manifest = await reader.ReadToEndAsync();
|
||||
|
||||
foreach (string line in manifest.Split("\n"))
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(line))
|
||||
sb.AppendLine();
|
||||
else if (line.StartsWith("#"))
|
||||
sb.AppendLine(line);
|
||||
else
|
||||
{
|
||||
Uri u = new(line);
|
||||
sb.AppendLine($"https://{Request.Host}/proxy/videoplayback?host={u.Host}&path={HttpUtility.UrlEncode(u.PathAndQuery)}");
|
||||
}
|
||||
}
|
||||
|
||||
return File(new MemoryStream(Encoding.UTF8.GetBytes(sb.ToString())),
|
||||
"application/vnd.apple.mpegurl");
|
||||
}
|
||||
|
||||
[Route("videoplayback")]
|
||||
public async Task VideoPlaybackProxy(string path, string host)
|
||||
{
|
||||
// make sure this is only used in livestreams
|
||||
|
||||
string url = $"https://{host}{path}";
|
||||
HttpWebRequest request = (HttpWebRequest)WebRequest.Create(url);
|
||||
request.AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate;
|
||||
|
||||
foreach ((string header, StringValues values) in HttpContext.Request.Headers.Where(header =>
|
||||
!header.Key.StartsWith(":") && !BlockedHeaders.Contains(header.Key.ToLower())))
|
||||
foreach (string value in values)
|
||||
request.Headers.Add(header, value);
|
||||
|
||||
using HttpWebResponse response = (HttpWebResponse)request.GetResponse();
|
||||
|
||||
await using Stream stream = response.GetResponseStream();
|
||||
|
||||
Response.ContentType = "application/octet-stream";
|
||||
await Response.StartAsync();
|
||||
await stream.CopyToAsync(Response.Body, HttpContext.RequestAborted);
|
||||
}
|
||||
}
|
||||
}
|
67
core/LightTube/Controllers/TogglesController.cs
Normal file
67
core/LightTube/Controllers/TogglesController.cs
Normal file
|
@ -0,0 +1,67 @@
|
|||
using System;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace LightTube.Controllers
|
||||
{
|
||||
[Route("/toggles")]
|
||||
public class TogglesController : Controller
|
||||
{
|
||||
[Route("theme")]
|
||||
public IActionResult ToggleTheme(string redirectUrl)
|
||||
{
|
||||
if (Request.Cookies.TryGetValue("theme", out string theme))
|
||||
Response.Cookies.Append("theme", theme switch
|
||||
{
|
||||
"light" => "dark",
|
||||
"dark" => "light",
|
||||
var _ => "dark"
|
||||
}, new CookieOptions
|
||||
{
|
||||
Expires = DateTimeOffset.MaxValue
|
||||
});
|
||||
else
|
||||
Response.Cookies.Append("theme", "light");
|
||||
|
||||
return Redirect(redirectUrl);
|
||||
}
|
||||
|
||||
[Route("compatibility")]
|
||||
public IActionResult ToggleCompatibility(string redirectUrl)
|
||||
{
|
||||
if (Request.Cookies.TryGetValue("compatibility", out string compatibility))
|
||||
Response.Cookies.Append("compatibility", compatibility switch
|
||||
{
|
||||
"true" => "false",
|
||||
"false" => "true",
|
||||
var _ => "true"
|
||||
}, new CookieOptions
|
||||
{
|
||||
Expires = DateTimeOffset.MaxValue
|
||||
});
|
||||
else
|
||||
Response.Cookies.Append("compatibility", "true");
|
||||
|
||||
return Redirect(redirectUrl);
|
||||
}
|
||||
|
||||
[Route("collapse_guide")]
|
||||
public IActionResult ToggleCollapseGuide(string redirectUrl)
|
||||
{
|
||||
if (Request.Cookies.TryGetValue("minmode", out string minmode))
|
||||
Response.Cookies.Append("minmode", minmode switch
|
||||
{
|
||||
"true" => "false",
|
||||
"false" => "true",
|
||||
var _ => "true"
|
||||
}, new CookieOptions
|
||||
{
|
||||
Expires = DateTimeOffset.MaxValue
|
||||
});
|
||||
else
|
||||
Response.Cookies.Append("minmode", "true");
|
||||
|
||||
return Redirect(redirectUrl);
|
||||
}
|
||||
}
|
||||
}
|
226
core/LightTube/Controllers/YoutubeController.cs
Normal file
226
core/LightTube/Controllers/YoutubeController.cs
Normal file
|
@ -0,0 +1,226 @@
|
|||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using LightTube.Contexts;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using InnerTube;
|
||||
using InnerTube.Models;
|
||||
using LightTube.Database;
|
||||
|
||||
namespace LightTube.Controllers
|
||||
{
|
||||
public class YoutubeController : Controller
|
||||
{
|
||||
private readonly ILogger<YoutubeController> _logger;
|
||||
private readonly Youtube _youtube;
|
||||
|
||||
public YoutubeController(ILogger<YoutubeController> logger, Youtube youtube)
|
||||
{
|
||||
_logger = logger;
|
||||
_youtube = youtube;
|
||||
}
|
||||
|
||||
[Route("/watch")]
|
||||
public async Task<IActionResult> Watch(string v, string quality = null)
|
||||
{
|
||||
Task[] tasks = {
|
||||
_youtube.GetPlayerAsync(v, HttpContext.GetLanguage(), HttpContext.GetRegion()),
|
||||
_youtube.GetVideoAsync(v, HttpContext.GetLanguage(), HttpContext.GetRegion()),
|
||||
ReturnYouTubeDislike.GetDislikes(v)
|
||||
};
|
||||
await Task.WhenAll(tasks);
|
||||
|
||||
bool cookieCompatibility = false;
|
||||
if (Request.Cookies.TryGetValue("compatibility", out string compatibilityString))
|
||||
bool.TryParse(compatibilityString, out cookieCompatibility);
|
||||
|
||||
PlayerContext context = new()
|
||||
{
|
||||
Player = (tasks[0] as Task<YoutubePlayer>)?.Result,
|
||||
Video = (tasks[1] as Task<YoutubeVideo>)?.Result,
|
||||
Engagement = (tasks[2] as Task<YoutubeDislikes>)?.Result,
|
||||
Resolution = quality ?? (tasks[0] as Task<YoutubePlayer>)?.Result.Formats.FirstOrDefault(x => x.FormatId != "17")?.FormatNote,
|
||||
MobileLayout = Utils.IsClientMobile(Request),
|
||||
CompatibilityMode = cookieCompatibility
|
||||
};
|
||||
return View(context);
|
||||
}
|
||||
|
||||
[Route("/download")]
|
||||
public async Task<IActionResult> Download(string v)
|
||||
{
|
||||
Task[] tasks = {
|
||||
_youtube.GetPlayerAsync(v, HttpContext.GetLanguage(), HttpContext.GetRegion()),
|
||||
_youtube.GetVideoAsync(v, HttpContext.GetLanguage(), HttpContext.GetRegion()),
|
||||
ReturnYouTubeDislike.GetDislikes(v)
|
||||
};
|
||||
await Task.WhenAll(tasks);
|
||||
|
||||
bool cookieCompatibility = false;
|
||||
if (Request.Cookies.TryGetValue("compatibility", out string compatibilityString))
|
||||
bool.TryParse(compatibilityString, out cookieCompatibility);
|
||||
|
||||
PlayerContext context = new()
|
||||
{
|
||||
Player = (tasks[0] as Task<YoutubePlayer>)?.Result,
|
||||
Video = (tasks[1] as Task<YoutubeVideo>)?.Result,
|
||||
Engagement = null,
|
||||
MobileLayout = Utils.IsClientMobile(Request),
|
||||
CompatibilityMode = cookieCompatibility
|
||||
};
|
||||
return View(context);
|
||||
}
|
||||
|
||||
[Route("/embed/{v}")]
|
||||
public async Task<IActionResult> Embed(string v, string quality = null, bool compatibility = false)
|
||||
{
|
||||
Task[] tasks = {
|
||||
_youtube.GetPlayerAsync(v, HttpContext.GetLanguage(), HttpContext.GetRegion()),
|
||||
_youtube.GetVideoAsync(v, HttpContext.GetLanguage(), HttpContext.GetRegion()),
|
||||
ReturnYouTubeDislike.GetDislikes(v)
|
||||
};
|
||||
try
|
||||
{
|
||||
await Task.WhenAll(tasks);
|
||||
}
|
||||
catch { }
|
||||
|
||||
|
||||
bool cookieCompatibility = false;
|
||||
if (Request.Cookies.TryGetValue("compatibility", out string compatibilityString))
|
||||
bool.TryParse(compatibilityString, out cookieCompatibility);
|
||||
|
||||
PlayerContext context = new()
|
||||
{
|
||||
Player = (tasks[0] as Task<YoutubePlayer>)?.Result,
|
||||
Video = (tasks[1] as Task<YoutubeVideo>)?.Result,
|
||||
Engagement = (tasks[2] as Task<YoutubeDislikes>)?.Result,
|
||||
Resolution = quality ?? (tasks[0] as Task<YoutubePlayer>)?.Result.Formats.FirstOrDefault(x => x.FormatId != "17")?.FormatNote,
|
||||
CompatibilityMode = compatibility || cookieCompatibility,
|
||||
MobileLayout = Utils.IsClientMobile(Request)
|
||||
};
|
||||
return View(context);
|
||||
}
|
||||
|
||||
[Route("/results")]
|
||||
public async Task<IActionResult> Search(string search_query, string continuation = null)
|
||||
{
|
||||
SearchContext context = new()
|
||||
{
|
||||
Query = search_query,
|
||||
ContinuationKey = continuation,
|
||||
MobileLayout = Utils.IsClientMobile(Request)
|
||||
};
|
||||
if (!string.IsNullOrWhiteSpace(search_query))
|
||||
{
|
||||
context.Results = await _youtube.SearchAsync(search_query, continuation, HttpContext.GetLanguage(),
|
||||
HttpContext.GetRegion());
|
||||
Response.Cookies.Append("search_query", search_query);
|
||||
}
|
||||
else
|
||||
{
|
||||
context.Results =
|
||||
new YoutubeSearchResults
|
||||
{
|
||||
Refinements = Array.Empty<string>(),
|
||||
EstimatedResults = 0,
|
||||
Results = Array.Empty<DynamicItem>(),
|
||||
ContinuationKey = null
|
||||
};
|
||||
}
|
||||
return View(context);
|
||||
}
|
||||
|
||||
[Route("/playlist")]
|
||||
public async Task<IActionResult> Playlist(string list, string continuation = null, int? delete = null, string add = null, string remove = null)
|
||||
{
|
||||
HttpContext.TryGetUser(out LTUser user, "web");
|
||||
|
||||
YoutubePlaylist pl = list.StartsWith("LT-PL")
|
||||
? await (await DatabaseManager.Playlists.GetPlaylist(list)).ToYoutubePlaylist()
|
||||
: await _youtube.GetPlaylistAsync(list, continuation, HttpContext.GetLanguage(), HttpContext.GetRegion());
|
||||
|
||||
string message = "";
|
||||
|
||||
if (list.StartsWith("LT-PL") && (await DatabaseManager.Playlists.GetPlaylist(list)).Visibility == PlaylistVisibility.PRIVATE && pl.Channel.Name != user?.UserID)
|
||||
pl = new YoutubePlaylist
|
||||
{
|
||||
Id = null,
|
||||
Title = "",
|
||||
Description = "",
|
||||
VideoCount = "",
|
||||
ViewCount = "",
|
||||
LastUpdated = "",
|
||||
Thumbnail = Array.Empty<Thumbnail>(),
|
||||
Channel = new Channel
|
||||
{
|
||||
Name = "",
|
||||
Id = "",
|
||||
SubscriberCount = "",
|
||||
Avatars = Array.Empty<Thumbnail>()
|
||||
},
|
||||
Videos = Array.Empty<DynamicItem>(),
|
||||
ContinuationKey = null
|
||||
};
|
||||
|
||||
if (string.IsNullOrWhiteSpace(pl.Title)) message = "Playlist unavailable";
|
||||
|
||||
if (list.StartsWith("LT-PL") && pl.Channel.Name == user?.UserID)
|
||||
{
|
||||
if (delete != null)
|
||||
{
|
||||
LTVideo removed = await DatabaseManager.Playlists.RemoveVideoFromPlaylist(list, delete.Value);
|
||||
message += $"Removed video '{removed.Title}'";
|
||||
}
|
||||
|
||||
if (add != null)
|
||||
{
|
||||
LTVideo added = await DatabaseManager.Playlists.AddVideoToPlaylist(list, add);
|
||||
message += $"Added video '{added.Title}'";
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(remove))
|
||||
{
|
||||
await DatabaseManager.Playlists.DeletePlaylist(list);
|
||||
message = "Playlist deleted";
|
||||
}
|
||||
|
||||
pl = await (await DatabaseManager.Playlists.GetPlaylist(list)).ToYoutubePlaylist();
|
||||
}
|
||||
|
||||
PlaylistContext context = new()
|
||||
{
|
||||
Playlist = pl,
|
||||
Id = list,
|
||||
ContinuationToken = continuation,
|
||||
MobileLayout = Utils.IsClientMobile(Request),
|
||||
Message = message,
|
||||
Editable = list.StartsWith("LT-PL") && pl.Channel.Name == user?.UserID
|
||||
};
|
||||
return View(context);
|
||||
}
|
||||
|
||||
[Route("/channel/{id}")]
|
||||
public async Task<IActionResult> Channel(string id, string continuation = null)
|
||||
{
|
||||
ChannelContext context = new()
|
||||
{
|
||||
Channel = await _youtube.GetChannelAsync(id, ChannelTabs.Videos, continuation, HttpContext.GetLanguage(), HttpContext.GetRegion()),
|
||||
Id = id,
|
||||
ContinuationToken = continuation,
|
||||
MobileLayout = Utils.IsClientMobile(Request)
|
||||
};
|
||||
await DatabaseManager.Channels.UpdateChannel(context.Channel.Id, context.Channel.Name, context.Channel.Subscribers,
|
||||
context.Channel.Avatars.First().Url.ToString());
|
||||
return View(context);
|
||||
}
|
||||
|
||||
[Route("/shorts/{id}")]
|
||||
public IActionResult Shorts(string id)
|
||||
{
|
||||
// yea no fuck shorts
|
||||
return Redirect("/watch?v=" + id);
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue