mirror of
https://codeberg.org/ashley/poke
synced 2026-03-03 18:13:45 +00:00
owo
This commit is contained in:
46
core/LightTube/Database/ChannelManager.cs
Normal file
46
core/LightTube/Database/ChannelManager.cs
Normal file
@@ -0,0 +1,46 @@
|
||||
using System.Threading.Tasks;
|
||||
using MongoDB.Driver;
|
||||
|
||||
namespace LightTube.Database
|
||||
{
|
||||
public class ChannelManager
|
||||
{
|
||||
private static IMongoCollection<LTChannel> _channelCacheCollection;
|
||||
|
||||
public ChannelManager(IMongoCollection<LTChannel> channelCacheCollection)
|
||||
{
|
||||
_channelCacheCollection = channelCacheCollection;
|
||||
}
|
||||
|
||||
public LTChannel GetChannel(string id)
|
||||
{
|
||||
LTChannel res = _channelCacheCollection.FindSync(x => x.ChannelId == id).FirstOrDefault();
|
||||
return res ?? new LTChannel
|
||||
{
|
||||
Name = "Unknown Channel",
|
||||
ChannelId = id,
|
||||
IconUrl = "",
|
||||
Subscribers = ""
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<LTChannel> UpdateChannel(string id, string name, string subscribers, string iconUrl)
|
||||
{
|
||||
LTChannel channel = new()
|
||||
{
|
||||
ChannelId = id,
|
||||
Name = name,
|
||||
Subscribers = subscribers,
|
||||
IconUrl = iconUrl
|
||||
};
|
||||
if (channel.IconUrl is null && !string.IsNullOrWhiteSpace(GetChannel(id).IconUrl))
|
||||
channel.IconUrl = GetChannel(id).IconUrl;
|
||||
if (await _channelCacheCollection.CountDocumentsAsync(x => x.ChannelId == id) > 0)
|
||||
await _channelCacheCollection.ReplaceOneAsync(x => x.ChannelId == id, channel);
|
||||
else
|
||||
await _channelCacheCollection.InsertOneAsync(channel);
|
||||
|
||||
return channel;
|
||||
}
|
||||
}
|
||||
}
|
||||
154
core/LightTube/Database/DatabaseManager.cs
Normal file
154
core/LightTube/Database/DatabaseManager.cs
Normal file
@@ -0,0 +1,154 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using InnerTube;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.Primitives;
|
||||
using MongoDB.Driver;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace LightTube.Database
|
||||
{
|
||||
public static class DatabaseManager
|
||||
{
|
||||
public static readonly string ApiUaRegex = "LightTubeApiClient\\/([0-9.]*) ([\\S]+?)\\/([0-9.]*) \\(([\\s\\S]+?)\\)";
|
||||
|
||||
private static IMongoCollection<LTUser> _userCollection;
|
||||
private static IMongoCollection<LTLogin> _tokenCollection;
|
||||
private static IMongoCollection<LTChannel> _channelCacheCollection;
|
||||
private static IMongoCollection<LTPlaylist> _playlistCollection;
|
||||
private static IMongoCollection<LTVideo> _videoCacheCollection;
|
||||
public static LoginManager Logins { get; private set; }
|
||||
public static ChannelManager Channels { get; private set; }
|
||||
public static PlaylistManager Playlists { get; private set; }
|
||||
|
||||
public static void Init(string connstr, Youtube youtube)
|
||||
{
|
||||
MongoClient client = new(connstr);
|
||||
IMongoDatabase database = client.GetDatabase("lighttube");
|
||||
_userCollection = database.GetCollection<LTUser>("users");
|
||||
_tokenCollection = database.GetCollection<LTLogin>("tokens");
|
||||
_playlistCollection = database.GetCollection<LTPlaylist>("playlists");
|
||||
_channelCacheCollection = database.GetCollection<LTChannel>("channelCache");
|
||||
_videoCacheCollection = database.GetCollection<LTVideo>("videoCache");
|
||||
Logins = new LoginManager(_userCollection, _tokenCollection);
|
||||
Channels = new ChannelManager(_channelCacheCollection);
|
||||
Playlists = new PlaylistManager(_userCollection, _playlistCollection, _videoCacheCollection, youtube);
|
||||
}
|
||||
|
||||
public static void CreateLocalAccount(this HttpContext context)
|
||||
{
|
||||
bool accountExists = false;
|
||||
|
||||
// Check local account
|
||||
if (context.Request.Cookies.TryGetValue("account_data", out string accountJson))
|
||||
{
|
||||
try
|
||||
{
|
||||
if (accountJson != null)
|
||||
{
|
||||
LTUser tempUser = JsonConvert.DeserializeObject<LTUser>(accountJson) ?? new LTUser();
|
||||
if (tempUser.UserID == "Local Account" && tempUser.PasswordHash == "local_account")
|
||||
accountExists = true;
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
|
||||
// Account already exists, just leave it there
|
||||
if (accountExists) return;
|
||||
|
||||
LTUser user = new()
|
||||
{
|
||||
UserID = "Local Account",
|
||||
PasswordHash = "local_account",
|
||||
SubscribedChannels = new List<string>()
|
||||
};
|
||||
|
||||
context.Response.Cookies.Append("account_data", JsonConvert.SerializeObject(user), new CookieOptions
|
||||
{
|
||||
Expires = DateTimeOffset.MaxValue
|
||||
});
|
||||
}
|
||||
|
||||
public static bool TryGetUser(this HttpContext context, out LTUser user, string requiredScope)
|
||||
{
|
||||
// Check local account
|
||||
if (context.Request.Cookies.TryGetValue("account_data", out string accountJson))
|
||||
{
|
||||
try
|
||||
{
|
||||
if (accountJson != null)
|
||||
{
|
||||
LTUser tempUser = JsonConvert.DeserializeObject<LTUser>(accountJson) ?? new LTUser();
|
||||
if (tempUser.UserID == "Local Account" && tempUser.PasswordHash == "local_account")
|
||||
{
|
||||
user = tempUser;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
user = null;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Check cloud account
|
||||
if (!context.Request.Cookies.TryGetValue("token", out string token))
|
||||
if (context.Request.Headers.TryGetValue("Authorization", out StringValues tokens))
|
||||
token = tokens.ToString();
|
||||
else
|
||||
{
|
||||
user = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (token != null)
|
||||
{
|
||||
user = Logins.GetUserFromToken(token).Result;
|
||||
LTLogin login = Logins.GetLoginFromToken(token).Result;
|
||||
if (login.Scopes.Contains(requiredScope))
|
||||
{
|
||||
#pragma warning disable 4014
|
||||
login.UpdateLastAccess(DateTimeOffset.Now);
|
||||
#pragma warning restore 4014
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
user = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
user = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
public static bool TryGetRssUser(string token, out LTUser user)
|
||||
{
|
||||
if (token is null)
|
||||
{
|
||||
user = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
user = Logins.GetUserFromRssToken(token).Result;
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
user = null;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
31
core/LightTube/Database/LTChannel.cs
Normal file
31
core/LightTube/Database/LTChannel.cs
Normal file
@@ -0,0 +1,31 @@
|
||||
using System.Xml;
|
||||
using MongoDB.Bson.Serialization.Attributes;
|
||||
|
||||
namespace LightTube.Database
|
||||
{
|
||||
[BsonIgnoreExtraElements]
|
||||
public class LTChannel
|
||||
{
|
||||
public string ChannelId;
|
||||
public string Name;
|
||||
public string Subscribers;
|
||||
public string IconUrl;
|
||||
|
||||
public XmlNode GetXmlElement(XmlDocument doc)
|
||||
{
|
||||
XmlElement item = doc.CreateElement("Channel");
|
||||
item.SetAttribute("id", ChannelId);
|
||||
item.SetAttribute("subscribers", Subscribers);
|
||||
|
||||
XmlElement title = doc.CreateElement("Name");
|
||||
title.InnerText = Name;
|
||||
item.AppendChild(title);
|
||||
|
||||
XmlElement thumbnail = doc.CreateElement("Avatar");
|
||||
thumbnail.InnerText = IconUrl;
|
||||
item.AppendChild(thumbnail);
|
||||
|
||||
return item;
|
||||
}
|
||||
}
|
||||
}
|
||||
84
core/LightTube/Database/LTLogin.cs
Normal file
84
core/LightTube/Database/LTLogin.cs
Normal file
@@ -0,0 +1,84 @@
|
||||
using System;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using System.Web;
|
||||
using System.Xml;
|
||||
using Humanizer;
|
||||
using MongoDB.Bson.Serialization.Attributes;
|
||||
using MyCSharp.HttpUserAgentParser;
|
||||
|
||||
namespace LightTube.Database
|
||||
{
|
||||
[BsonIgnoreExtraElements]
|
||||
public class LTLogin
|
||||
{
|
||||
public string Identifier;
|
||||
public string Email;
|
||||
public string Token;
|
||||
public string UserAgent;
|
||||
public string[] Scopes;
|
||||
public DateTimeOffset Created = DateTimeOffset.MinValue;
|
||||
public DateTimeOffset LastSeen = DateTimeOffset.MinValue;
|
||||
|
||||
public XmlDocument GetXmlElement()
|
||||
{
|
||||
XmlDocument doc = new();
|
||||
XmlElement login = doc.CreateElement("Login");
|
||||
login.SetAttribute("id", Identifier);
|
||||
login.SetAttribute("user", Email);
|
||||
|
||||
XmlElement token = doc.CreateElement("Token");
|
||||
token.InnerText = Token;
|
||||
login.AppendChild(token);
|
||||
|
||||
XmlElement scopes = doc.CreateElement("Scopes");
|
||||
foreach (string scope in Scopes)
|
||||
{
|
||||
XmlElement scopeElement = doc.CreateElement("Scope");
|
||||
scopeElement.InnerText = scope;
|
||||
login.AppendChild(scopeElement);
|
||||
}
|
||||
login.AppendChild(scopes);
|
||||
|
||||
doc.AppendChild(login);
|
||||
return doc;
|
||||
}
|
||||
|
||||
public string GetTitle()
|
||||
{
|
||||
Match match = Regex.Match(UserAgent, DatabaseManager.ApiUaRegex);
|
||||
if (match.Success)
|
||||
return $"API App: {match.Groups[2]} {match.Groups[3]}";
|
||||
|
||||
HttpUserAgentInformation client = HttpUserAgentParser.Parse(UserAgent);
|
||||
StringBuilder sb = new($"{client.Name} {client.Version}");
|
||||
if (client.Platform.HasValue)
|
||||
sb.Append($" on {client.Platform.Value.PlatformType.ToString()}");
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
public string GetDescription()
|
||||
{
|
||||
StringBuilder sb = new();
|
||||
sb.AppendLine($"Created: {Created.Humanize(DateTimeOffset.Now)}");
|
||||
sb.AppendLine($"Last seen: {LastSeen.Humanize(DateTimeOffset.Now)}");
|
||||
|
||||
Match match = Regex.Match(UserAgent, DatabaseManager.ApiUaRegex);
|
||||
if (match.Success)
|
||||
{
|
||||
sb.AppendLine($"API version: {HttpUtility.HtmlEncode(match.Groups[1])}");
|
||||
sb.AppendLine($"App info: {HttpUtility.HtmlEncode(match.Groups[4])}");
|
||||
sb.AppendLine("Allowed scopes:");
|
||||
foreach (string scope in Scopes) sb.AppendLine($"- {scope}");
|
||||
}
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
public async Task UpdateLastAccess(DateTimeOffset newTime)
|
||||
{
|
||||
await DatabaseManager.Logins.UpdateLastAccess(Identifier, newTime);
|
||||
}
|
||||
}
|
||||
}
|
||||
68
core/LightTube/Database/LTPlaylist.cs
Normal file
68
core/LightTube/Database/LTPlaylist.cs
Normal file
@@ -0,0 +1,68 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using InnerTube.Models;
|
||||
|
||||
namespace LightTube.Database
|
||||
{
|
||||
public class LTPlaylist
|
||||
{
|
||||
public string Id;
|
||||
public string Name;
|
||||
public string Description;
|
||||
public PlaylistVisibility Visibility;
|
||||
public List<string> VideoIds;
|
||||
public string Author;
|
||||
public DateTimeOffset LastUpdated;
|
||||
|
||||
public async Task<YoutubePlaylist> ToYoutubePlaylist()
|
||||
{
|
||||
List<Thumbnail> t = new();
|
||||
if (VideoIds.Count > 0)
|
||||
t.Add(new Thumbnail { Url = $"https://i.ytimg.com/vi_webp/{VideoIds.First()}/maxresdefault.webp" });
|
||||
YoutubePlaylist playlist = new()
|
||||
{
|
||||
Id = Id,
|
||||
Title = Name,
|
||||
Description = Description,
|
||||
VideoCount = VideoIds.Count.ToString(),
|
||||
ViewCount = "0",
|
||||
LastUpdated = "Last updated " + LastUpdated.ToString("MMMM dd, yyyy"),
|
||||
Thumbnail = t.ToArray(),
|
||||
Channel = new Channel
|
||||
{
|
||||
Name = Author,
|
||||
Id = GenerateChannelId(),
|
||||
SubscriberCount = "0 subscribers",
|
||||
Avatars = Array.Empty<Thumbnail>()
|
||||
},
|
||||
Videos = (await DatabaseManager.Playlists.GetPlaylistVideos(Id)).Select(x =>
|
||||
{
|
||||
x.Index = VideoIds.IndexOf(x.Id) + 1;
|
||||
return x;
|
||||
}).Cast<DynamicItem>().ToArray(),
|
||||
ContinuationKey = null
|
||||
};
|
||||
return playlist;
|
||||
}
|
||||
|
||||
private string GenerateChannelId()
|
||||
{
|
||||
StringBuilder sb = new("LTU-" + Author.Trim() + "_");
|
||||
|
||||
string alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
|
||||
Random rng = new(Author.GetHashCode());
|
||||
while (sb.Length < 32) sb.Append(alphabet[rng.Next(0, alphabet.Length)]);
|
||||
return sb.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
public enum PlaylistVisibility
|
||||
{
|
||||
PRIVATE,
|
||||
UNLISTED,
|
||||
VISIBLE
|
||||
}
|
||||
}
|
||||
102
core/LightTube/Database/LTUser.cs
Normal file
102
core/LightTube/Database/LTUser.cs
Normal file
@@ -0,0 +1,102 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Threading.Tasks;
|
||||
using System.Xml;
|
||||
using MongoDB.Bson.Serialization.Attributes;
|
||||
|
||||
namespace LightTube.Database
|
||||
{
|
||||
[BsonIgnoreExtraElements]
|
||||
public class LTUser
|
||||
{
|
||||
public string UserID;
|
||||
public string PasswordHash;
|
||||
public List<string> SubscribedChannels;
|
||||
public bool ApiAccess;
|
||||
public string RssToken;
|
||||
|
||||
public async Task<string> GenerateRssFeed(string hostUrl, int limit)
|
||||
{
|
||||
XmlDocument document = new();
|
||||
XmlElement rss = document.CreateElement("rss");
|
||||
rss.SetAttribute("version", "2.0");
|
||||
|
||||
XmlElement channel = document.CreateElement("channel");
|
||||
|
||||
XmlElement title = document.CreateElement("title");
|
||||
title.InnerText = "LightTube subscriptions RSS feed for " + UserID;
|
||||
channel.AppendChild(title);
|
||||
|
||||
XmlElement description = document.CreateElement("description");
|
||||
description.InnerText = $"LightTube subscriptions RSS feed for {UserID} with {SubscribedChannels.Count} channels";
|
||||
channel.AppendChild(description);
|
||||
|
||||
FeedVideo[] feeds = await YoutubeRSS.GetMultipleFeeds(SubscribedChannels);
|
||||
IEnumerable<FeedVideo> feedVideos = feeds.Take(limit);
|
||||
|
||||
foreach (FeedVideo video in feedVideos)
|
||||
{
|
||||
XmlElement item = document.CreateElement("item");
|
||||
|
||||
XmlElement id = document.CreateElement("id");
|
||||
id.InnerText = $"id:video:{video.Id}";
|
||||
item.AppendChild(id);
|
||||
|
||||
XmlElement vtitle = document.CreateElement("title");
|
||||
vtitle.InnerText = video.Title;
|
||||
item.AppendChild(vtitle);
|
||||
|
||||
XmlElement vdescription = document.CreateElement("description");
|
||||
vdescription.InnerText = video.Description;
|
||||
item.AppendChild(vdescription);
|
||||
|
||||
XmlElement link = document.CreateElement("link");
|
||||
link.InnerText = $"https://{hostUrl}/watch?v={video.Id}";
|
||||
item.AppendChild(link);
|
||||
|
||||
XmlElement published = document.CreateElement("pubDate");
|
||||
published.InnerText = video.PublishedDate.ToString("R");
|
||||
item.AppendChild(published);
|
||||
|
||||
XmlElement author = document.CreateElement("author");
|
||||
|
||||
XmlElement name = document.CreateElement("name");
|
||||
name.InnerText = video.ChannelName;
|
||||
author.AppendChild(name);
|
||||
|
||||
XmlElement uri = document.CreateElement("uri");
|
||||
uri.InnerText = $"https://{hostUrl}/channel/{video.ChannelId}";
|
||||
author.AppendChild(uri);
|
||||
|
||||
item.AppendChild(author);
|
||||
/*
|
||||
XmlElement mediaGroup = document.CreateElement("media_group");
|
||||
|
||||
XmlElement mediaTitle = document.CreateElement("media_title");
|
||||
mediaTitle.InnerText = video.Title;
|
||||
mediaGroup.AppendChild(mediaTitle);
|
||||
|
||||
XmlElement mediaThumbnail = document.CreateElement("media_thumbnail");
|
||||
mediaThumbnail.SetAttribute("url", video.Thumbnail);
|
||||
mediaGroup.AppendChild(mediaThumbnail);
|
||||
|
||||
XmlElement mediaContent = document.CreateElement("media_content");
|
||||
mediaContent.SetAttribute("url", $"https://{hostUrl}/embed/{video.Id}");
|
||||
mediaContent.SetAttribute("type", "text/html");
|
||||
mediaGroup.AppendChild(mediaContent);
|
||||
|
||||
item.AppendChild(mediaGroup);
|
||||
*/
|
||||
channel.AppendChild(item);
|
||||
}
|
||||
|
||||
rss.AppendChild(channel);
|
||||
|
||||
document.AppendChild(rss);
|
||||
return document.OuterXml;//.Replace("<media_", "<media:").Replace("</media_", "</media:");
|
||||
}
|
||||
}
|
||||
}
|
||||
39
core/LightTube/Database/LTVideo.cs
Normal file
39
core/LightTube/Database/LTVideo.cs
Normal file
@@ -0,0 +1,39 @@
|
||||
using System;
|
||||
using System.Xml;
|
||||
using InnerTube.Models;
|
||||
|
||||
namespace LightTube.Database
|
||||
{
|
||||
public class LTVideo : PlaylistVideoItem
|
||||
{
|
||||
public string UploadedAt;
|
||||
public long Views;
|
||||
|
||||
public override XmlElement GetXmlElement(XmlDocument doc)
|
||||
{
|
||||
XmlElement item = doc.CreateElement("Video");
|
||||
item.SetAttribute("id", Id);
|
||||
item.SetAttribute("duration", Duration);
|
||||
item.SetAttribute("views", Views.ToString());
|
||||
item.SetAttribute("uploadedAt", UploadedAt);
|
||||
item.SetAttribute("index", Index.ToString());
|
||||
|
||||
XmlElement title = doc.CreateElement("Title");
|
||||
title.InnerText = Title;
|
||||
item.AppendChild(title);
|
||||
if (Channel is not null)
|
||||
item.AppendChild(Channel.GetXmlElement(doc));
|
||||
|
||||
foreach (Thumbnail t in Thumbnails ?? Array.Empty<Thumbnail>())
|
||||
{
|
||||
XmlElement thumbnail = doc.CreateElement("Thumbnail");
|
||||
thumbnail.SetAttribute("width", t.Width.ToString());
|
||||
thumbnail.SetAttribute("height", t.Height.ToString());
|
||||
thumbnail.InnerText = t.Url;
|
||||
item.AppendChild(thumbnail);
|
||||
}
|
||||
|
||||
return item;
|
||||
}
|
||||
}
|
||||
}
|
||||
172
core/LightTube/Database/LoginManager.cs
Normal file
172
core/LightTube/Database/LoginManager.cs
Normal file
@@ -0,0 +1,172 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Data;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using InnerTube.Models;
|
||||
using MongoDB.Driver;
|
||||
|
||||
namespace LightTube.Database
|
||||
{
|
||||
public class LoginManager
|
||||
{
|
||||
private IMongoCollection<LTUser> _userCollection;
|
||||
private IMongoCollection<LTLogin> _tokenCollection;
|
||||
|
||||
public LoginManager(IMongoCollection<LTUser> userCollection, IMongoCollection<LTLogin> tokenCollection)
|
||||
{
|
||||
_userCollection = userCollection;
|
||||
_tokenCollection = tokenCollection;
|
||||
}
|
||||
|
||||
public async Task<LTLogin> CreateToken(string email, string password, string userAgent, string[] scopes)
|
||||
{
|
||||
IAsyncCursor<LTUser> users = await _userCollection.FindAsync(x => x.UserID == email);
|
||||
if (!await users.AnyAsync())
|
||||
throw new UnauthorizedAccessException("Invalid credentials");
|
||||
LTUser user = (await _userCollection.FindAsync(x => x.UserID == email)).First();
|
||||
if (!BCrypt.Net.BCrypt.Verify(password, user.PasswordHash))
|
||||
throw new UnauthorizedAccessException("Invalid credentials");
|
||||
if (!scopes.Contains("web") && !user.ApiAccess)
|
||||
throw new InvalidOperationException("This user has API access disabled");
|
||||
|
||||
LTLogin login = new()
|
||||
{
|
||||
Identifier = Guid.NewGuid().ToString(),
|
||||
Email = email,
|
||||
Token = GenerateToken(256),
|
||||
UserAgent = userAgent,
|
||||
Scopes = scopes.ToArray(),
|
||||
Created = DateTimeOffset.Now,
|
||||
LastSeen = DateTimeOffset.Now
|
||||
};
|
||||
await _tokenCollection.InsertOneAsync(login);
|
||||
return login;
|
||||
}
|
||||
|
||||
public async Task UpdateLastAccess(string id, DateTimeOffset offset)
|
||||
{
|
||||
LTLogin login = (await _tokenCollection.FindAsync(x => x.Identifier == id)).First();
|
||||
login.LastSeen = offset;
|
||||
await _tokenCollection.ReplaceOneAsync(x => x.Identifier == id, login);
|
||||
}
|
||||
|
||||
public async Task RemoveToken(string token)
|
||||
{
|
||||
await _tokenCollection.FindOneAndDeleteAsync(t => t.Token == token);
|
||||
}
|
||||
|
||||
public async Task RemoveToken(string email, string password, string identifier)
|
||||
{
|
||||
IAsyncCursor<LTUser> users = await _userCollection.FindAsync(x => x.UserID == email);
|
||||
if (!await users.AnyAsync())
|
||||
throw new KeyNotFoundException("Invalid credentials");
|
||||
LTUser user = (await _userCollection.FindAsync(x => x.UserID == email)).First();
|
||||
if (!BCrypt.Net.BCrypt.Verify(password, user.PasswordHash))
|
||||
throw new UnauthorizedAccessException("Invalid credentials");
|
||||
|
||||
await _tokenCollection.FindOneAndDeleteAsync(t => t.Identifier == identifier && t.Email == user.UserID);
|
||||
}
|
||||
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public async Task RemoveTokenFromId(string sourceToken, string identifier)
|
||||
{
|
||||
LTLogin login = (await _tokenCollection.FindAsync(x => x.Token == sourceToken)).First();
|
||||
LTLogin deletedLogin = (await _tokenCollection.FindAsync(x => x.Identifier == identifier)).First();
|
||||
|
||||
if (login.Email == deletedLogin.Email)
|
||||
await _tokenCollection.FindOneAndDeleteAsync(t => t.Identifier == identifier);
|
||||
else
|
||||
throw new UnauthorizedAccessException(
|
||||
"Logged in user does not match the token that is supposed to be deleted");
|
||||
}
|
||||
|
||||
public async Task<LTUser> GetUserFromToken(string token)
|
||||
{
|
||||
string email = (await _tokenCollection.FindAsync(x => x.Token == token)).First().Email;
|
||||
return (await _userCollection.FindAsync(u => u.UserID == email)).First();
|
||||
}
|
||||
|
||||
public async Task<LTUser> GetUserFromRssToken(string token) => (await _userCollection.FindAsync(u => u.RssToken == token)).First();
|
||||
|
||||
public async Task<LTLogin> GetLoginFromToken(string token)
|
||||
{
|
||||
var res = await _tokenCollection.FindAsync(x => x.Token == token);
|
||||
return res.First();
|
||||
}
|
||||
|
||||
public async Task<List<LTLogin>> GetAllUserTokens(string token)
|
||||
{
|
||||
string email = (await _tokenCollection.FindAsync(x => x.Token == token)).First().Email;
|
||||
return await (await _tokenCollection.FindAsync(u => u.Email == email)).ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<string> GetCurrentLoginId(string token)
|
||||
{
|
||||
return (await _tokenCollection.FindAsync(t => t.Token == token)).First().Identifier;
|
||||
}
|
||||
|
||||
public async Task<(LTChannel channel, bool subscribed)> SubscribeToChannel(LTUser user, YoutubeChannel channel)
|
||||
{
|
||||
LTChannel ltChannel = await DatabaseManager.Channels.UpdateChannel(channel.Id, channel.Name, channel.Subscribers,
|
||||
channel.Avatars.FirstOrDefault()?.Url);
|
||||
|
||||
if (user.SubscribedChannels.Contains(ltChannel.ChannelId))
|
||||
user.SubscribedChannels.Remove(ltChannel.ChannelId);
|
||||
else
|
||||
user.SubscribedChannels.Add(ltChannel.ChannelId);
|
||||
|
||||
await _userCollection.ReplaceOneAsync(x => x.UserID == user.UserID, user);
|
||||
return (ltChannel, user.SubscribedChannels.Contains(ltChannel.ChannelId));
|
||||
}
|
||||
|
||||
public async Task SetApiAccess(LTUser user, bool access)
|
||||
{
|
||||
user.ApiAccess = access;
|
||||
await _userCollection.ReplaceOneAsync(x => x.UserID == user.UserID, user);
|
||||
}
|
||||
|
||||
public async Task DeleteUser(string email, string password)
|
||||
{
|
||||
IAsyncCursor<LTUser> users = await _userCollection.FindAsync(x => x.UserID == email);
|
||||
if (!await users.AnyAsync())
|
||||
throw new KeyNotFoundException("Invalid credentials");
|
||||
LTUser user = (await _userCollection.FindAsync(x => x.UserID == email)).First();
|
||||
if (!BCrypt.Net.BCrypt.Verify(password, user.PasswordHash))
|
||||
throw new UnauthorizedAccessException("Invalid credentials");
|
||||
|
||||
await _userCollection.DeleteOneAsync(x => x.UserID == email);
|
||||
await _tokenCollection.DeleteManyAsync(x => x.Email == email);
|
||||
foreach (LTPlaylist pl in await DatabaseManager.Playlists.GetUserPlaylists(email))
|
||||
await DatabaseManager.Playlists.DeletePlaylist(pl.Id);
|
||||
}
|
||||
|
||||
public async Task CreateUser(string email, string password)
|
||||
{
|
||||
IAsyncCursor<LTUser> users = await _userCollection.FindAsync(x => x.UserID == email);
|
||||
if (await users.AnyAsync())
|
||||
throw new DuplicateNameException("A user with that email already exists");
|
||||
|
||||
LTUser user = new()
|
||||
{
|
||||
UserID = email,
|
||||
PasswordHash = BCrypt.Net.BCrypt.HashPassword(password),
|
||||
SubscribedChannels = new List<string>(),
|
||||
RssToken = GenerateToken(32)
|
||||
};
|
||||
await _userCollection.InsertOneAsync(user);
|
||||
}
|
||||
|
||||
private string GenerateToken(int length)
|
||||
{
|
||||
string tokenAlphabet = @"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-+*/()[]{}";
|
||||
Random rng = new();
|
||||
StringBuilder sb = new();
|
||||
for (int i = 0; i < length; i++)
|
||||
sb.Append(tokenAlphabet[rng.Next(0, tokenAlphabet.Length)]);
|
||||
return sb.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
161
core/LightTube/Database/PlaylistManager.cs
Normal file
161
core/LightTube/Database/PlaylistManager.cs
Normal file
@@ -0,0 +1,161 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Net.Http;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using InnerTube;
|
||||
using InnerTube.Models;
|
||||
using MongoDB.Driver;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace LightTube.Database
|
||||
{
|
||||
public class PlaylistManager
|
||||
{
|
||||
private IMongoCollection<LTUser> _userCollection;
|
||||
private IMongoCollection<LTPlaylist> _playlistCollection;
|
||||
private IMongoCollection<LTVideo> _videoCacheCollection;
|
||||
private Youtube _youtube;
|
||||
|
||||
public PlaylistManager(IMongoCollection<LTUser> userCollection, IMongoCollection<LTPlaylist> playlistCollection,
|
||||
IMongoCollection<LTVideo> videoCacheCollection, Youtube youtube)
|
||||
{
|
||||
_userCollection = userCollection;
|
||||
_playlistCollection = playlistCollection;
|
||||
_videoCacheCollection = videoCacheCollection;
|
||||
_youtube = youtube;
|
||||
}
|
||||
|
||||
public async Task<LTPlaylist> CreatePlaylist(LTUser user, string name, string description,
|
||||
PlaylistVisibility visibility, string idPrefix = null)
|
||||
{
|
||||
if (await _userCollection.CountDocumentsAsync(x => x.UserID == user.UserID) == 0)
|
||||
throw new UnauthorizedAccessException("Local accounts cannot create playlists");
|
||||
|
||||
LTPlaylist pl = new()
|
||||
{
|
||||
Id = GenerateAuthorId(idPrefix),
|
||||
Name = name,
|
||||
Description = description,
|
||||
Visibility = visibility,
|
||||
VideoIds = new List<string>(),
|
||||
Author = user.UserID,
|
||||
LastUpdated = DateTimeOffset.Now
|
||||
};
|
||||
|
||||
await _playlistCollection.InsertOneAsync(pl).ConfigureAwait(false);
|
||||
|
||||
return pl;
|
||||
}
|
||||
|
||||
public async Task<LTPlaylist> GetPlaylist(string id)
|
||||
{
|
||||
IAsyncCursor<LTPlaylist> cursor = await _playlistCollection.FindAsync(x => x.Id == id);
|
||||
return await cursor.FirstOrDefaultAsync() ?? new LTPlaylist
|
||||
{
|
||||
Id = null,
|
||||
Name = "",
|
||||
Description = "",
|
||||
Visibility = PlaylistVisibility.VISIBLE,
|
||||
VideoIds = new List<string>(),
|
||||
Author = "",
|
||||
LastUpdated = DateTimeOffset.MinValue
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<List<LTVideo>> GetPlaylistVideos(string id)
|
||||
{
|
||||
LTPlaylist pl = await GetPlaylist(id);
|
||||
List<LTVideo> videos = new();
|
||||
|
||||
foreach (string videoId in pl.VideoIds)
|
||||
{
|
||||
IAsyncCursor<LTVideo> cursor = await _videoCacheCollection.FindAsync(x => x.Id == videoId);
|
||||
videos.Add(await cursor.FirstAsync());
|
||||
}
|
||||
|
||||
return videos;
|
||||
}
|
||||
|
||||
public async Task<LTVideo> AddVideoToPlaylist(string playlistId, string videoId)
|
||||
{
|
||||
LTPlaylist pl = await GetPlaylist(playlistId);
|
||||
YoutubeVideo vid = await _youtube.GetVideoAsync(videoId);
|
||||
JObject ytPlayer = await InnerTube.Utils.GetAuthorizedPlayer(videoId, new HttpClient());
|
||||
|
||||
if (string.IsNullOrEmpty(vid.Id))
|
||||
throw new KeyNotFoundException($"Couldn't find a video with ID '{videoId}'");
|
||||
|
||||
LTVideo v = new()
|
||||
{
|
||||
Id = vid.Id,
|
||||
Title = vid.Title,
|
||||
Thumbnails = ytPlayer?["videoDetails"]?["thumbnail"]?["thumbnails"]?.ToObject<Thumbnail[]>() ?? new []
|
||||
{
|
||||
new Thumbnail { Url = $"https://i.ytimg.com/vi_webp/{vid.Id}/maxresdefault.webp" }
|
||||
},
|
||||
UploadedAt = vid.UploadDate,
|
||||
Views = long.Parse(vid.Views.Split(" ")[0].Replace(",", "").Replace(".", "")),
|
||||
Channel = vid.Channel,
|
||||
Duration = GetDurationString(ytPlayer?["videoDetails"]?["lengthSeconds"]?.ToObject<long>() ?? 0),
|
||||
Index = pl.VideoIds.Count
|
||||
};
|
||||
pl.VideoIds.Add(vid.Id);
|
||||
|
||||
if (await _videoCacheCollection.CountDocumentsAsync(x => x.Id == vid.Id) == 0)
|
||||
await _videoCacheCollection.InsertOneAsync(v);
|
||||
else
|
||||
await _videoCacheCollection.FindOneAndReplaceAsync(x => x.Id == vid.Id, v);
|
||||
|
||||
UpdateDefinition<LTPlaylist> update = Builders<LTPlaylist>.Update
|
||||
.Push(x => x.VideoIds, vid.Id);
|
||||
_playlistCollection.FindOneAndUpdate(x => x.Id == playlistId, update);
|
||||
|
||||
return v;
|
||||
}
|
||||
|
||||
public async Task<LTVideo> RemoveVideoFromPlaylist(string playlistId, int videoIndex)
|
||||
{
|
||||
LTPlaylist pl = await GetPlaylist(playlistId);
|
||||
|
||||
IAsyncCursor<LTVideo> cursor = await _videoCacheCollection.FindAsync(x => x.Id == pl.VideoIds[videoIndex]);
|
||||
LTVideo v = await cursor.FirstAsync();
|
||||
pl.VideoIds.RemoveAt(videoIndex);
|
||||
|
||||
await _playlistCollection.FindOneAndReplaceAsync(x => x.Id == playlistId, pl);
|
||||
|
||||
return v;
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<LTPlaylist>> GetUserPlaylists(string userId)
|
||||
{
|
||||
IAsyncCursor<LTPlaylist> cursor = await _playlistCollection.FindAsync(x => x.Author == userId);
|
||||
|
||||
return cursor.ToEnumerable();
|
||||
}
|
||||
|
||||
private string GetDurationString(long length)
|
||||
{
|
||||
string s = TimeSpan.FromSeconds(length).ToString();
|
||||
while (s.StartsWith("00:") && s.Length > 5) s = s[3..];
|
||||
return s;
|
||||
}
|
||||
|
||||
public static string GenerateAuthorId(string prefix)
|
||||
{
|
||||
StringBuilder sb = new(string.IsNullOrWhiteSpace(prefix) || prefix.Trim().Length > 20
|
||||
? "LT-PL"
|
||||
: "LT-PL-" + prefix.Trim() + "_");
|
||||
|
||||
string alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
|
||||
Random rng = new();
|
||||
while (sb.Length < 32) sb.Append(alphabet[rng.Next(0, alphabet.Length)]);
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
public async Task DeletePlaylist(string playlistId)
|
||||
{
|
||||
await _playlistCollection.DeleteOneAsync(x => x.Id == playlistId);
|
||||
}
|
||||
}
|
||||
}
|
||||
18
core/LightTube/Database/SubscriptionChannels.cs
Normal file
18
core/LightTube/Database/SubscriptionChannels.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
using System.Xml;
|
||||
|
||||
namespace LightTube.Database
|
||||
{
|
||||
public class SubscriptionChannels
|
||||
{
|
||||
public LTChannel[] Channels { get; set; }
|
||||
|
||||
public XmlNode GetXmlDocument()
|
||||
{
|
||||
XmlDocument doc = new();
|
||||
XmlElement feed = doc.CreateElement("Subscriptions");
|
||||
foreach (LTChannel channel in Channels) feed.AppendChild(channel.GetXmlElement(doc));
|
||||
doc.AppendChild(feed);
|
||||
return doc;
|
||||
}
|
||||
}
|
||||
}
|
||||
18
core/LightTube/Database/SubscriptionFeed.cs
Normal file
18
core/LightTube/Database/SubscriptionFeed.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
using System.Xml;
|
||||
|
||||
namespace LightTube.Database
|
||||
{
|
||||
public class SubscriptionFeed
|
||||
{
|
||||
public FeedVideo[] videos;
|
||||
|
||||
public XmlDocument GetXmlDocument()
|
||||
{
|
||||
XmlDocument doc = new();
|
||||
XmlElement feed = doc.CreateElement("Feed");
|
||||
foreach (FeedVideo feedVideo in videos) feed.AppendChild(feedVideo.GetXmlElement(doc));
|
||||
doc.AppendChild(feed);
|
||||
return doc;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user