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,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;
}
}
}

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

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

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

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

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

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

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

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

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

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