1
0
Fork 0
mirror of https://codeberg.org/ashley/poke.git synced 2025-06-21 15:07:01 -04:00
This commit is contained in:
Ashley 2022-08-05 22:33:38 +03:00 committed by GitHub
parent f431111611
commit 72143fede3
100 changed files with 12438 additions and 0 deletions

View 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}");
}
}
}

View 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);
}
}
}

View 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);
}
}
}
}

View 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");
}
}
}

View 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)
});
}
}
}

View 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);
}
}
}

View 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);
}
}
}

View 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);
}
}
}

View 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);
}
}
}