From 81e9951bda19820641a7103bcbfe4ba85eea9c04 Mon Sep 17 00:00:00 2001 From: Kayra Uylar Date: Sun, 19 May 2024 15:25:08 +0300 Subject: [PATCH 001/114] Update InnerTube --- LightTube/LightTube.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LightTube/LightTube.csproj b/LightTube/LightTube.csproj index 34539512..ef5a8369 100644 --- a/LightTube/LightTube.csproj +++ b/LightTube/LightTube.csproj @@ -10,7 +10,7 @@ - + From 8a9e1e8a555d2657e2188ecf4d835dc52f9f2a35 Mon Sep 17 00:00:00 2001 From: Kayra Uylar Date: Sun, 19 May 2024 16:04:49 +0300 Subject: [PATCH 002/114] Fix errors and warnings in Utils and Database classes --- LightTube/Contexts/HomepageContext.cs | 2 +- LightTube/Contexts/LibraryContext.cs | 2 +- LightTube/Contexts/PlaylistContext.cs | 2 +- LightTube/Contexts/WatchContext.cs | 4 +- LightTube/Controllers/ApiController.cs | 12 +- LightTube/Controllers/ExportController.cs | 2 +- LightTube/Controllers/FeedController.cs | 13 +- LightTube/Controllers/MediaController.cs | 5 +- LightTube/Controllers/OauthApiController.cs | 15 +- LightTube/Controllers/OpenSearchController.cs | 5 +- LightTube/Controllers/SettingsController.cs | 5 +- LightTube/Controllers/YoutubeController.cs | 5 +- LightTube/Database/Models/DatabaseChannel.cs | 5 +- LightTube/Database/Models/DatabasePlaylist.cs | 28 +--- LightTube/Database/Models/DatabaseUser.cs | 88 ++++++------ LightTube/Database/Models/DatabaseVideo.cs | 17 +-- .../Database/Models/DatabaseVideoAuthor.cs | 1 + LightTube/Database/PlaylistManager.cs | 130 +++++++++++++----- LightTube/Importer/ImporterUtility.cs | 18 +-- LightTube/Program.cs | 2 +- LightTube/Resources/Localization/en.json | 2 + LightTube/Utils.cs | 71 +++++----- LightTube/YoutubeRSS.cs | 121 ---------------- LightTube/YoutubeRss.cs | 83 +++++++++++ 24 files changed, 303 insertions(+), 335 deletions(-) delete mode 100644 LightTube/YoutubeRSS.cs create mode 100644 LightTube/YoutubeRss.cs diff --git a/LightTube/Contexts/HomepageContext.cs b/LightTube/Contexts/HomepageContext.cs index 1313cb1f..d8fa16bb 100644 --- a/LightTube/Contexts/HomepageContext.cs +++ b/LightTube/Contexts/HomepageContext.cs @@ -12,7 +12,7 @@ public HomepageContext(HttpContext context) : base(context) { Videos = Task.Run(async () => { - return (await YoutubeRSS.GetMultipleFeeds(User.Subscriptions.Where(x => x.Value == SubscriptionType.NOTIFICATIONS_ON).Select(x => x.Key))).Take(context.Request.Cookies["maxvideos"] is null ? 5 : Convert.ToInt32(context.Request.Cookies["maxvideos"])).ToArray(); + return (await YoutubeRss.GetMultipleFeeds(User.Subscriptions.Where(x => x.Value == SubscriptionType.NOTIFICATIONS_ON).Select(x => x.Key))).Take(context.Request.Cookies["maxvideos"] is null ? 5 : Convert.ToInt32(context.Request.Cookies["maxvideos"])).ToArray(); }).Result; } } diff --git a/LightTube/Contexts/LibraryContext.cs b/LightTube/Contexts/LibraryContext.cs index 46228486..210a39a8 100644 --- a/LightTube/Contexts/LibraryContext.cs +++ b/LightTube/Contexts/LibraryContext.cs @@ -10,7 +10,7 @@ public class LibraryContext : BaseContext public LibraryContext(HttpContext context) : base(context) { Playlists = User != null - ? DatabaseManager.Playlists.GetUserPlaylists(User.UserID, PlaylistVisibility.PRIVATE) + ? DatabaseManager.Playlists.GetUserPlaylists(User.UserID, PlaylistVisibility.Private) : []; } } \ No newline at end of file diff --git a/LightTube/Contexts/PlaylistContext.cs b/LightTube/Contexts/PlaylistContext.cs index 2296a10b..dcf7e61e 100644 --- a/LightTube/Contexts/PlaylistContext.cs +++ b/LightTube/Contexts/PlaylistContext.cs @@ -76,7 +76,7 @@ public PlaylistContext(HttpContext context, InnerTubePlaylist playlist, InnerTub public PlaylistContext(HttpContext context, DatabasePlaylist? playlist) : base(context) { - bool visible = (playlist?.Visibility == PlaylistVisibility.PRIVATE) + bool visible = (playlist?.Visibility == PlaylistVisibility.Private) ? User != null && User.UserID == playlist.Author : true; diff --git a/LightTube/Contexts/WatchContext.cs b/LightTube/Contexts/WatchContext.cs index 1863bce5..d7b9bfb0 100644 --- a/LightTube/Contexts/WatchContext.cs +++ b/LightTube/Contexts/WatchContext.cs @@ -87,7 +87,7 @@ public class WatchContext : BaseContext context.Request.Query["q"], sponsors); Video = innerTubeNextResponse; Playlist = playlist?.GetInnerTubePlaylistInfo(innerTubePlayer.Details.Id); - if (playlist != null && playlist.Visibility == PlaylistVisibility.PRIVATE) + if (playlist != null && playlist.Visibility == PlaylistVisibility.Private) if (playlist.Author != User?.UserID) Playlist = null; Comments = comments; @@ -125,7 +125,7 @@ public class WatchContext : BaseContext Player = new PlayerContext(context, e); Video = innerTubeNextResponse; Playlist = playlist?.GetInnerTubePlaylistInfo(innerTubeNextResponse.Id); - if (playlist != null && playlist.Visibility == PlaylistVisibility.PRIVATE) + if (playlist != null && playlist.Visibility == PlaylistVisibility.Private) if (playlist.Author != User?.UserID) Playlist = null; Comments = comments; diff --git a/LightTube/Controllers/ApiController.cs b/LightTube/Controllers/ApiController.cs index 6898fda9..fb5f3c24 100644 --- a/LightTube/Controllers/ApiController.cs +++ b/LightTube/Controllers/ApiController.cs @@ -10,13 +10,12 @@ namespace LightTube.Controllers; [Route("/api")] -public class ApiController(InnerTube.InnerTube youtube) : Controller +public class ApiController(SimpleInnerTubeClient innerTube) : Controller { private const string VIDEO_ID_REGEX = @"[a-zA-Z0-9_-]{11}"; private const string CHANNEL_ID_REGEX = @"[a-zA-Z0-9_-]{24}"; private const string PLAYLIST_ID_REGEX = @"[a-zA-Z0-9_-]{34}"; - private readonly InnerTube.InnerTube _youtube = youtube; - + [Route("info")] public LightTubeInstanceInfo GetInstanceInfo() => new() @@ -43,8 +42,7 @@ private ApiResponse Error(string message, int code, HttpStatusCode statusC [Route("player")] [ApiDisableable] - public async Task> GetPlayerInfo(string? id, bool contentCheckOk = true, - bool includeHls = false) + public async Task> GetPlayerInfo(string? id, bool contentCheckOk = true) { if (id is null) return Error("Missing video ID (query parameter `id`)", 400, @@ -57,7 +55,7 @@ private ApiResponse Error(string message, int code, HttpStatusCode statusC try { InnerTubePlayer player = - await _youtube.GetPlayerAsync(id, contentCheckOk, includeHls, HttpContext.GetInnerTubeLanguage(), + await _youtube.GetPlayerAsync(id, contentCheckOk, HttpContext.GetInnerTubeLanguage(), HttpContext.GetInnerTubeRegion()); DatabaseUser? user = await DatabaseManager.Oauth2.GetUserFromHttpRequest(Request); @@ -194,7 +192,7 @@ public async Task> Playlist(string id, int? skip) return Error("The playlist does not exist.", 500, HttpStatusCode.InternalServerError); - if (playlist.Visibility == PlaylistVisibility.PRIVATE) + if (playlist.Visibility == PlaylistVisibility.Private) { if (user == null) return Error("The playlist does not exist.", 500, diff --git a/LightTube/Controllers/ExportController.cs b/LightTube/Controllers/ExportController.cs index 2416cf5c..a9100bc1 100644 --- a/LightTube/Controllers/ExportController.cs +++ b/LightTube/Controllers/ExportController.cs @@ -21,7 +21,7 @@ public IActionResult LightTubeExport() Type = $"LightTube/{Utils.GetVersion()}", Host = Request.Host.ToString(), Subscriptions = [.. context.User.Subscriptions.Keys], - Playlists = DatabaseManager.Playlists.GetUserPlaylists(context.User.UserID, PlaylistVisibility.PRIVATE) + Playlists = DatabaseManager.Playlists.GetUserPlaylists(context.User.UserID, PlaylistVisibility.Private) .Select(x => new ImportedData.Playlist { Title = x.Name, diff --git a/LightTube/Controllers/FeedController.cs b/LightTube/Controllers/FeedController.cs index 16cb2bae..96818860 100644 --- a/LightTube/Controllers/FeedController.cs +++ b/LightTube/Controllers/FeedController.cs @@ -12,15 +12,14 @@ namespace LightTube.Controllers; [Route("/feed")] -public class FeedController(InnerTube.InnerTube youtube) : Controller +public class FeedController(SimpleInnerTubeClient innerTube) : Controller { - private InnerTube.InnerTube _youtube = youtube; - + [Route("channel/{c}/rss.xml")] [HttpGet] public async Task ChannelFeed(string c) { - ChannelFeed ytchannel = await YoutubeRSS.GetChannelFeed(c); + ChannelFeed ytchannel = await YoutubeRss.GetChannelFeed(c); try { XmlDocument document = new(); @@ -93,7 +92,7 @@ public async Task Subscription() { SubscriptionsContext ctx = new(HttpContext); if (ctx.User is null) return Redirect("/account/login?redirectUrl=%2ffeed%2fsubscriptions"); - ctx.Videos = await YoutubeRSS.GetMultipleFeeds(ctx.User.Subscriptions.Keys); + ctx.Videos = await YoutubeRss.GetMultipleFeeds(ctx.User.Subscriptions.Keys); return View(ctx); } @@ -114,7 +113,7 @@ public async Task RssFeed() string username = secretDecoded.Split(':')[0]; string password = secretDecoded.Split(':')[1]; DatabaseUser? user = await DatabaseManager.Users.GetUserFromUsernamePassword(username, password) ?? throw new Exception(); - FeedVideo[] feedVideos = await YoutubeRSS.GetMultipleFeeds(user.Subscriptions.Where(x => x.Value == SubscriptionType.NOTIFICATIONS_ON).Select(x => x.Key)); + FeedVideo[] feedVideos = await YoutubeRss.GetMultipleFeeds(user.Subscriptions.Where(x => x.Value == SubscriptionType.NOTIFICATIONS_ON).Select(x => x.Key)); XmlDocument document = new(); XmlElement rss = document.CreateElement("rss"); @@ -359,7 +358,7 @@ public async Task AddToPlaylist(string v) InnerTubeNextResponse intr = await _youtube.GetVideoAsync(v); PlaylistVideoContext> pvc = new(HttpContext, intr); if (pvc.User is null) return Redirect("/account/login?redirectUrl=" + HttpUtility.UrlEncode(Request.Path + Request.Query)); - pvc.Extra = DatabaseManager.Playlists.GetUserPlaylists(pvc.User.UserID, PlaylistVisibility.PRIVATE); + pvc.Extra = DatabaseManager.Playlists.GetUserPlaylists(pvc.User.UserID, PlaylistVisibility.Private); pvc.Buttons = [ new ModalButton("", "|", ""), diff --git a/LightTube/Controllers/MediaController.cs b/LightTube/Controllers/MediaController.cs index f8409910..41a3f43d 100644 --- a/LightTube/Controllers/MediaController.cs +++ b/LightTube/Controllers/MediaController.cs @@ -11,10 +11,9 @@ namespace LightTube.Controllers; [Route("/proxy")] -public class ProxyController(InnerTube.InnerTube youtube) : Controller +public class ProxyController(SimpleInnerTubeClient innerTube) : Controller { - private readonly InnerTube.InnerTube _youtube = youtube; - private readonly HttpClient client = new(); + private readonly HttpClient client = new(); private string[] _blockedHeaders = [ diff --git a/LightTube/Controllers/OauthApiController.cs b/LightTube/Controllers/OauthApiController.cs index b3c50100..9e0b1a71 100644 --- a/LightTube/Controllers/OauthApiController.cs +++ b/LightTube/Controllers/OauthApiController.cs @@ -11,10 +11,9 @@ namespace LightTube.Controllers; [Route("/api")] [ApiDisableable] -public class OauthApiController(InnerTube.InnerTube youtube) : Controller +public class OauthApiController(SimpleInnerTubeClient innerTube) : Controller { - private readonly InnerTube.InnerTube _youtube = youtube; - + private ApiResponse Error(string message, int code, HttpStatusCode statusCode) { @@ -50,7 +49,7 @@ public async Task>> GetPlaylists() if (user is null) return Error>("Unauthorized", 401, HttpStatusCode.Unauthorized); ApiUserData? userData = ApiUserData.GetFromDatabaseUser(user); - return new ApiResponse>(user.PlaylistRenderers(PlaylistVisibility.PRIVATE).Items, + return new ApiResponse>(user.PlaylistRenderers(PlaylistVisibility.Private).Items, userData); } @@ -71,7 +70,7 @@ public async Task> CreatePlaylist([FromBody] Creat ApiUserData? userData = ApiUserData.GetFromDatabaseUser(user); DatabasePlaylist playlist = await DatabaseManager.Playlists.CreatePlaylist( Request.Headers.Authorization.ToString(), request.Title, - request.Description ?? "", request.Visibility ?? PlaylistVisibility.PRIVATE); + request.Description ?? "", request.Visibility ?? PlaylistVisibility.Private); return new ApiResponse(playlist, userData); } catch (Exception e) @@ -97,7 +96,7 @@ public async Task> UpdatePlaylist(string id, [From ApiUserData? userData = ApiUserData.GetFromDatabaseUser(user); await DatabaseManager.Playlists.EditPlaylist( Request.Headers.Authorization.ToString()!, id, request.Title, - request.Description ?? "", request.Visibility ?? PlaylistVisibility.PRIVATE); + request.Description ?? "", request.Visibility ?? PlaylistVisibility.Private); DatabasePlaylist playlist = DatabaseManager.Playlists.GetPlaylist(id)!; return new ApiResponse(playlist, userData); } @@ -222,8 +221,8 @@ public async Task> DeleteVideoFromPlaylist(string playlistId return Error("Unauthorized", 401, HttpStatusCode.Unauthorized); FeedVideo[] feed = includeNonNotification - ? await YoutubeRSS.GetMultipleFeeds(user.Subscriptions.Keys) - : await YoutubeRSS.GetMultipleFeeds(user.Subscriptions.Where(x => + ? await YoutubeRss.GetMultipleFeeds(user.Subscriptions.Keys) + : await YoutubeRss.GetMultipleFeeds(user.Subscriptions.Where(x => x.Value == SubscriptionType.NOTIFICATIONS_OFF).Select(x => x.Key)); feed = feed.Skip(skip).Take(limit).ToArray(); diff --git a/LightTube/Controllers/OpenSearchController.cs b/LightTube/Controllers/OpenSearchController.cs index 52d35954..c510b2ab 100644 --- a/LightTube/Controllers/OpenSearchController.cs +++ b/LightTube/Controllers/OpenSearchController.cs @@ -7,10 +7,9 @@ namespace LightTube.Controllers; [Route("/opensearch")] -public class OpenSearchController(InnerTube.InnerTube youtube) : Controller +public class OpenSearchController(SimpleInnerTubeClient innerTube) : Controller { - private readonly InnerTube.InnerTube _youtube = youtube; - + [Route("osdd.xml")] public IActionResult OpenSearchDescriptionDocument() { diff --git a/LightTube/Controllers/SettingsController.cs b/LightTube/Controllers/SettingsController.cs index 7844e056..0c28a8ba 100644 --- a/LightTube/Controllers/SettingsController.cs +++ b/LightTube/Controllers/SettingsController.cs @@ -10,10 +10,9 @@ namespace LightTube.Controllers; [Route("/settings")] -public class SettingsController(InnerTube.InnerTube youtube) : Controller +public class SettingsController(SimpleInnerTubeClient innerTube) : Controller { - private readonly InnerTube.InnerTube _youtube = youtube; - + [Route("/settings")] public IActionResult Settings() => RedirectPermanent("/settings/appearance"); diff --git a/LightTube/Controllers/YoutubeController.cs b/LightTube/Controllers/YoutubeController.cs index cf9b6538..3ca6248c 100644 --- a/LightTube/Controllers/YoutubeController.cs +++ b/LightTube/Controllers/YoutubeController.cs @@ -8,10 +8,9 @@ namespace LightTube.Controllers; -public class YoutubeController(InnerTube.InnerTube youtube, HttpClient client) : Controller +public class YoutubeController(SimpleInnerTubeClient innerTube, HttpClient client) : Controller { - private readonly InnerTube.InnerTube _youtube = youtube; - private readonly HttpClient _client = client; + private readonly HttpClient _client = client; [Route("/embed/{v}")] public async Task Embed(string v, bool contentCheckOk, bool compatibility = false) diff --git a/LightTube/Database/Models/DatabaseChannel.cs b/LightTube/Database/Models/DatabaseChannel.cs index 45395775..e8e54c6d 100644 --- a/LightTube/Database/Models/DatabaseChannel.cs +++ b/LightTube/Database/Models/DatabaseChannel.cs @@ -1,4 +1,5 @@ using InnerTube; +using InnerTube.Models; using MongoDB.Bson.Serialization.Attributes; namespace LightTube.Database.Models; @@ -16,11 +17,11 @@ public DatabaseChannel() } - public DatabaseChannel(InnerTubeChannelResponse channel) + public DatabaseChannel(InnerTubeChannel channel) { ChannelId = channel.Header!.Id; Name = channel.Header!.Title; Subscribers = channel.Header!.SubscriberCountText; - IconUrl = channel.Header!.Avatars.Last().Url.ToString(); + IconUrl = channel.Header!.Avatars.Last().Url; } } \ No newline at end of file diff --git a/LightTube/Database/Models/DatabasePlaylist.cs b/LightTube/Database/Models/DatabasePlaylist.cs index cfb00f68..19cafa7c 100644 --- a/LightTube/Database/Models/DatabasePlaylist.cs +++ b/LightTube/Database/Models/DatabasePlaylist.cs @@ -1,12 +1,11 @@ using InnerTube; +using InnerTube.Models; using Newtonsoft.Json.Linq; namespace LightTube.Database.Models; public class DatabasePlaylist { - private const string INNERTUBE_PLAYLIST_INFO_TEMPLATE = "{\"playlistId\":\"%%PLAYLIST_ID%%\",\"title\":\"%%TITLE%%\",\"totalVideos\":%%VIDEO_COUNT%%,\"currentIndex\":%%CURRENT_INDEX%%,\"localCurrentIndex\":%%CURRENT_INDEX%%,\"longBylineText\":{\"runs\":[{\"text\":\"%%CHANNEL_TITLE%%\",\"navigationEndpoint\":{\"browseEndpoint\":{\"browseId\":\"%%CHANNEL_ID%%\"}}}]},\"isInfinite\":false,\"isCourse\":false,\"ownerBadges\":[],\"contents\":[%%CONTENTS%%]}"; - private const string INNERTUBE_GRID_PLAYLIST_RENDERER_TEMPLATE = "{\"gridPlaylistRenderer\":{\"playlistId\":\"%%ID%%\",\"title\":{\"simpleText\":\"%%TITLE%%\"},\"videoCountShortText\":{\"simpleText\":\"%%VIDEOCOUNT%%\"},\"thumbnailRenderer\":{\"playlistVideoThumbnailRenderer\":{\"thumbnail\":{\"thumbnails\":[{\"url\":\"%%THUMBNAIL%%\",\"width\":0,\"height\":0}]}}}}}"; private const string ID_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"; public string Id; public string Name; @@ -16,25 +15,6 @@ public class DatabasePlaylist public string Author; public DateTimeOffset LastUpdated; - public InnerTubePlaylistInfo? GetInnerTubePlaylistInfo(string currentVideoId) - { - string json = INNERTUBE_PLAYLIST_INFO_TEMPLATE - .Replace("%%PLAYLIST_ID%%", Id) - .Replace("%%TITLE%%", Name.Replace("\"", "\\\"")) - .Replace("%%VIDEO_COUNT%%", VideoIds.Count.ToString()) - .Replace("%%CURRENT_INDEX%%", VideoIds.IndexOf(currentVideoId).ToString()) - .Replace("%%CHANNEL_TITLE%%", Author.Replace("\"", "\\\"")) - .Replace("%%CHANNEL_ID%%", DatabaseManager.Users.GetUserFromId(Author).Result?.LTChannelID) - .Replace("%%CONTENTS%%", DatabaseManager.Playlists.GetPlaylistPanelVideosJson(Id, currentVideoId)); - return new InnerTubePlaylistInfo(JObject.Parse(json)); - } - - public string GetInnerTubeGridPlaylistJson() => INNERTUBE_GRID_PLAYLIST_RENDERER_TEMPLATE - .Replace("%%ID%%", Id) - .Replace("%%TITLE%%", Name) - .Replace("%%VIDEOCOUNT%%", VideoIds.Count.ToString()) - .Replace("%%THUMBNAIL%%", $"https://i.ytimg.com/vi/{VideoIds.FirstOrDefault()}/hqdefault.jpg"); - public static string GenerateId() { Random rng = new(); @@ -47,7 +27,7 @@ public static string GenerateId() public enum PlaylistVisibility { - PRIVATE, - UNLISTED, - VISIBLE + Private, + Unlisted, + Visible } \ No newline at end of file diff --git a/LightTube/Database/Models/DatabaseUser.cs b/LightTube/Database/Models/DatabaseUser.cs index e03e6b1f..68682ca4 100644 --- a/LightTube/Database/Models/DatabaseUser.cs +++ b/LightTube/Database/Models/DatabaseUser.cs @@ -1,34 +1,20 @@ -using InnerTube.Renderers; +using InnerTube.Protobuf; +using InnerTube.Renderers; +using LightTube.Localization; using MongoDB.Bson.Serialization.Attributes; using Newtonsoft.Json; -using Newtonsoft.Json.Linq; namespace LightTube.Database.Models; [BsonIgnoreExtraElements] public class DatabaseUser { - private const string INNERTUBE_GRID_RENDERER_TEMPLATE = "{\"items\": [%%CONTENTS%%]}"; - - private const string INNERTUBE_MESSAGE_RENDERER_TEMPLATE = - "{\"messageRenderer\":{\"text\":{\"simpleText\":\"%%MESSAGE%%\"}}}"; - private const string ID_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"; public string UserID { get; set; } [JsonIgnore] public string PasswordHash { get; set; } [JsonIgnore] public Dictionary Subscriptions { get; set; } public string LTChannelID { get; set; } - [JsonIgnore] - [BsonIgnoreIfNull] - [Obsolete("Use Subscriptions dictionary instead")] - public string[]? SubscribedChannels; - - [JsonIgnore] - [BsonIgnoreIfNull] - [Obsolete("Use UserID instead")] - public string? Email; - public static DatabaseUser CreateUser(string userId, string password) => new() { @@ -38,28 +24,6 @@ public class DatabaseUser LTChannelID = GetChannelId(userId) }; - public void Migrate() - { -#pragma warning disable CS0618 - if (SubscribedChannels is not null) - { - Subscriptions ??= []; - foreach (string id in SubscribedChannels) - if (!Subscriptions.ContainsKey(id)) - Subscriptions.Add(id, SubscriptionType.NOTIFICATIONS_ON); - SubscribedChannels = null; - } - - if (Email is not null && UserID is null) - { - UserID = Email; - Email = null; - } -#pragma warning restore CS0618 - - LTChannelID ??= GetChannelId(UserID); - } - public static string GetChannelId(string userId) { Random rng = new(userId.GetHashCode()); @@ -69,17 +33,43 @@ public static string GetChannelId(string userId) return channelId; } - public GridRenderer PlaylistRenderers(PlaylistVisibility minVisibility = PlaylistVisibility.VISIBLE) + public List PlaylistRenderers(LocalizationManager localization, PlaylistVisibility minVisibility = PlaylistVisibility.Visible) { - IEnumerable playlists = - DatabaseManager.Playlists.GetUserPlaylists(UserID, minVisibility); - string playlistsJson = playlists.Any() - ? string.Join(',', playlists.Select(x => x.GetInnerTubeGridPlaylistJson())) - : INNERTUBE_MESSAGE_RENDERER_TEMPLATE.Replace("%%MESSAGE%%", - "This user doesn't have any public playlists."); + DatabasePlaylist[] playlists = + DatabaseManager.Playlists.GetUserPlaylists(UserID, minVisibility).ToArray(); + if (playlists.Length == 0) + { + return + [ + new RendererContainer + { + Type = "message", + OriginalType = "messageRenderer", + Data = new MessageRendererData(localization.GetRawString("channel.noplaylists")) + } + ]; + } - string json = INNERTUBE_GRID_RENDERER_TEMPLATE - .Replace("%%CONTENTS%%", playlistsJson); - return new GridRenderer(JObject.Parse(json)); + return playlists.Select(x => new RendererContainer + { + Type = "playlist", + OriginalType = "gridPlaylistRenderer", + Data = new PlaylistRendererData + { + PlaylistId = x.Id, + Thumbnails = [ + new Thumbnail + { + Url = $"https://i.ytimg.com/vi/{x.VideoIds.FirstOrDefault()}/hqdefault.jpg", + Width = 480, + Height = 360 + } + ], + Title = x.Name, + VideoCountText = string.Format(localization.GetRawString("playlist.videos.count"), x.VideoIds.Count), + SidebarThumbnails = [], + Author = null + } + }).ToList(); } } \ No newline at end of file diff --git a/LightTube/Database/Models/DatabaseVideo.cs b/LightTube/Database/Models/DatabaseVideo.cs index a984a8ee..596314be 100644 --- a/LightTube/Database/Models/DatabaseVideo.cs +++ b/LightTube/Database/Models/DatabaseVideo.cs @@ -1,4 +1,5 @@ using InnerTube; +using InnerTube.Protobuf; using MongoDB.Bson.Serialization.Attributes; namespace LightTube.Database.Models; @@ -23,24 +24,14 @@ public DatabaseVideo(InnerTubePlayer player) { Id = player.Details.Id; Title = player.Details.Title; - Thumbnails = [ - new() - { - Url = player.Details.Thumbnails[0].Url - } - ]; + Thumbnails = player.Details.Thumbnails.Thumbnails_.ToArray(); UploadedAt = ""; Views = 0; Channel = new() { - Id = player.Details.Author.Id!, + Id = player.Details.Author.Id, Name = player.Details.Author.Title, - Avatars = [ - new Thumbnail() - { - Url = player.Details.Author.Avatar! - } - ] + Avatars = player.Details.Author.Avatar! }; Duration = player.Details.Length.ToDurationString(); } diff --git a/LightTube/Database/Models/DatabaseVideoAuthor.cs b/LightTube/Database/Models/DatabaseVideoAuthor.cs index bf89f715..f3bca151 100644 --- a/LightTube/Database/Models/DatabaseVideoAuthor.cs +++ b/LightTube/Database/Models/DatabaseVideoAuthor.cs @@ -1,4 +1,5 @@ using InnerTube; +using InnerTube.Protobuf; using MongoDB.Bson.Serialization.Attributes; namespace LightTube.Database.Models; diff --git a/LightTube/Database/PlaylistManager.cs b/LightTube/Database/PlaylistManager.cs index c7feaa98..d0f967d4 100644 --- a/LightTube/Database/PlaylistManager.cs +++ b/LightTube/Database/PlaylistManager.cs @@ -1,6 +1,9 @@ using InnerTube; +using InnerTube.Models; +using InnerTube.Protobuf; using InnerTube.Renderers; using LightTube.Database.Models; +using LightTube.Localization; using MongoDB.Driver; using Newtonsoft.Json.Linq; @@ -10,7 +13,6 @@ public class PlaylistManager( IMongoCollection playlistCollection, IMongoCollection videoCacheCollection) { - private const string INNERTUBE_PLAYLIST_VIDEO_RENDERER_TEMPLATE = "{\"videoId\":\"%%ID%%\",\"isPlayable\":true,\"thumbnail\":{\"thumbnails\":[{\"url\":\"%%THUMBNAIL%%\",\"width\":0,\"height\":0}]},\"title\":{\"runs\":[{\"text\":\"%%TITLE%%\"}]},\"index\":{\"simpleText\":\"%%INDEX%%\"},\"shortBylineText\":{\"runs\":[{\"text\":\"%%CHANNEL_TITLE%%\",\"navigationEndpoint\":{\"browseEndpoint\":{\"browseId\":\"%%CHANNEL_ID%%\"}}}]},\"lengthText\":{\"simpleText\":\"%%DURATION%%\"},\"navigationEndpoint\":{\"watchEndpoint\":{\"videoId\":\"%%ID%%\"}},\"lengthSeconds\":\"%%DURATION_SECONDS%%\",\"isPlayable\":true,\"thumbnailOverlays\":[{\"thumbnailOverlayTimeStatusRenderer\":{\"text\":{\"simpleText\":\"%%DURATION%%\"}}}],\"videoInfo\":{\"runs\":[{\"text\":\"%%VIEWS%%\"},{\"text\":\" • \"},{\"text\":\"%%UPLOADED_AT%%\"}]}}"; private const string INNERTUBE_PLAYLIST_PANEL_VIDEO_RENDERER_TEMPLATE = "{\"title\":{\"simpleText\":\"%%TITLE%%\"},\"thumbnail\":{\"thumbnails\":[{\"url\":\"%%THUMBNAIL%%\",\"width\":0,\"height\":0}]},\"lengthText\":{\"simpleText\":\"%%DURATION%%\"},\"indexText\":{\"simpleText\":\"%%INDEX%%\"},\"selected\":%%SELECTED%%,\"navigationEndpoint\":{\"watchEndpoint\":{\"params\":\"OAE%3D\"}},\"videoId\":\"%%ID%%\",\"shortBylineText\":{\"runs\":[{\"text\":\"%%CHANNEL_TITLE%%\",\"navigationEndpoint\":{\"browseEndpoint\":{\"browseId\":\"%%CHANNEL_ID%%\"}}}]}}"; public IMongoCollection PlaylistCollection { get; } = playlistCollection; public IMongoCollection VideoCacheCollection { get; } = videoCacheCollection; @@ -23,55 +25,106 @@ public IEnumerable GetUserPlaylists(string userId, PlaylistVis return unfiltered.ToList().Where(x => x.Visibility >= minVisibility); } - public IEnumerable GetPlaylistVideos(string id, bool editable) + public IEnumerable GetPlaylistVideos(string id, bool editable, LocalizationManager localization) { DatabasePlaylist? pl = GetPlaylist(id); if (pl == null) return []; - List renderers = []; + List renderers = []; for (int i = 0; i < pl.VideoIds.Count; i++) { string videoId = pl.VideoIds[i]; DatabaseVideo? video = VideoCacheCollection.FindSync(x => x.Id == videoId).FirstOrDefault(); - string json = INNERTUBE_PLAYLIST_VIDEO_RENDERER_TEMPLATE - .Replace("%%ID%%", editable ? videoId + "!" : videoId) - .Replace("%%INDEX%%", (i + 1).ToString()) - .Replace("%%TITLE%%", video?.Title.Replace("\"", "\\\"") ?? "Uncached video. Click to fix") - .Replace("%%THUMBNAIL%%", video?.Thumbnails.LastOrDefault()?.Url.ToString() ?? "https://i.ytimg.com/vi//hqdefault.jpg") - .Replace("%%DURATION%%", video?.Duration ?? "00:00") - .Replace("%%DURATION_SECONDS%%", InnerTube.Utils.ParseDuration(video?.Duration ?? "00:00").TotalSeconds.ToString()) - .Replace("%%UPLADED_AT%%", video?.UploadedAt ?? "???") - .Replace("%%CHANNEL_TITLE%%", video?.Channel.Name.Replace("\"", "\\\"") ?? "???") - .Replace("%%CHANNEL_ID%%", video?.Channel.Id ?? "???") - .Replace("%%VIEWS%%", (video?.Views ?? 0).ToString()); - renderers.Add(new PlaylistVideoRenderer(JObject.Parse(json))); + RendererContainer container = new() + { + Type = "video", + OriginalType = "playlistVideoContainer", + Data = new PlaylistVideoRendererData + { + VideoId = editable ? videoId + "!" : videoId, + Title = video?.Title.Replace("\"", "\\\"") ?? localization.GetRawString("playlist.video.uncached"), + Thumbnails = + [ + new Thumbnail + { + Url = video?.Thumbnails.LastOrDefault()?.Url.ToString() ?? "https://i.ytimg.com/vi//hqdefault.jpg", + Width = 480, + Height = 360 + } + ], + Author = video?.Channel.Id != null + ? new Channel( + video.Channel.Id, + video?.Channel.Name.Replace("\"", "\\\"") ?? "???", + null, + null, + null, + null + ) + : null, + Duration = InnerTube.Utils.ParseDuration(video?.Duration ?? "00:00"), + PublishedText = video?.UploadedAt, + ViewCountText = (video?.Views ?? 0).ToString(), + Badges = [], + Description = null, + VideoIndexText = (i + 1).ToString() + } + }; + renderers.Add(container); } return renderers; } - public IEnumerable GetPlaylistPanelVideos(string id, string currentVideoId) + public List GetPlaylistPanelVideos(string id, string currentVideoId, + LocalizationManager localization) { DatabasePlaylist? pl = GetPlaylist(id); if (pl == null) return []; - List renderers = []; + List renderers = []; for (int i = 0; i < pl.VideoIds.Count; i++) { string videoId = pl.VideoIds[i]; DatabaseVideo? video = VideoCacheCollection.FindSync(x => x.Id == videoId).FirstOrDefault(); - string json = INNERTUBE_PLAYLIST_PANEL_VIDEO_RENDERER_TEMPLATE - .Replace("%%ID%%", videoId) - .Replace("%%SELECTED%%", (currentVideoId == videoId).ToString().ToLower()) - .Replace("%%INDEX%%", currentVideoId == videoId ? ">" : (i + 1).ToString()) - .Replace("%%TITLE%%", video?.Title.Replace("\"", "\\\"") ?? "Uncached video. Click to fix") - .Replace("%%THUMBNAIL%%", video?.Thumbnails.LastOrDefault()?.Url.ToString() ?? "https://i.ytimg.com/vi//hqdefault.jpg") - .Replace("%%DURATION%%", video?.Duration ?? "00:00") - .Replace("%%CHANNEL_TITLE%%", video?.Channel.Name.Replace("\"", "\\\"") ?? "???") - .Replace("%%CHANNEL_ID%%", video?.Channel.Id ?? "???"); - renderers.Add(new PlaylistPanelVideoRenderer(JObject.Parse(json))); + RendererContainer container = new() + { + Type = "video", + OriginalType = "playlistPanelVideoRenderer", + Data = new PlaylistVideoRendererData + { + VideoId = videoId, + Title = video?.Title.Replace("\"", "\\\"") ?? localization.GetRawString("playlist.video.uncached"), + Thumbnails = video?.Thumbnails ?? + [ + new Thumbnail + { + Url = $"https://i.ytimg.com/vi/{videoId}/hqdefault.jpg", + Width = 480, + Height = 360 + } + ], + Author = video?.Channel.Id != null + ? new Channel( + video.Channel.Id, + video?.Channel.Name.Replace("\"", "\\\"") ?? "???", + null, + null, + null, + null + ) + : null, + Duration = InnerTube.Utils.ParseDuration(video?.Duration ?? "00:00"), + PublishedText = video?.UploadedAt, + ViewCountText = (video?.Views ?? 0).ToString(), + Badges = [], + Description = null, + VideoIndexText = (i + 1).ToString() + } + }; + renderers.Add(container); } return renderers; @@ -144,27 +197,30 @@ public async Task AddVideoToPlaylist(string token, string playlistId, InnerTubeP playlist.LastUpdated = DateTimeOffset.UtcNow; await PlaylistCollection.ReplaceOneAsync(x => x.Id == playlistId, playlist); - await DatabaseManager.Cache.AddVideo(new DatabaseVideo() + await DatabaseManager.Cache.AddVideo(new DatabaseVideo { Id = video.Details.Id, Title = video.Details.Title, Thumbnails = [ - new() + new Thumbnail { - Url = new Uri($"https://i.ytimg.com/vi/{video.Details.Id}/hqdefault.jpg") + Url = $"https://i.ytimg.com/vi/{video.Details.Id}/hqdefault.jpg", + Width = 480, + Height = 360 } ], Views = 0, - Channel = new() + Channel = new DatabaseVideoAuthor { Id = video.Details.Author.Id!, Name = video.Details.Author.Title, - Avatars = [ - new() - { - Url = video.Details.Author.Avatar! - } - ] + Avatars = + [ + new Thumbnail + { + Url = video.Details.Author.Avatar!.First().Url + } + ] }, Duration = video.Details.Length.ToDurationString() }); diff --git a/LightTube/Importer/ImporterUtility.cs b/LightTube/Importer/ImporterUtility.cs index 66d8b22e..77881017 100644 --- a/LightTube/Importer/ImporterUtility.cs +++ b/LightTube/Importer/ImporterUtility.cs @@ -108,10 +108,10 @@ private static ImportedData ExtractTakeoutZip(byte[] data) item.TimeUpdated = DateTimeOffset.Parse(infoParts[3]); item.Visibility = infoParts[6] switch { - "Public" => PlaylistVisibility.VISIBLE, - "Unlisted" => PlaylistVisibility.UNLISTED, - "Private" => PlaylistVisibility.PRIVATE, - _ => PlaylistVisibility.PRIVATE + "Public" => PlaylistVisibility.Visible, + "Unlisted" => PlaylistVisibility.Unlisted, + "Private" => PlaylistVisibility.Private, + _ => PlaylistVisibility.Private }; item.VideoIds = videosPart.Select(x => x.Split(',')[0]).ToArray(); importedData.Playlists.Add(item); @@ -174,10 +174,10 @@ private static ImportedData ExtractInvidiousJson(byte[] data) TimeUpdated = null, Visibility = playlist["privacy"]!.ToObject()! switch { - "Public" => PlaylistVisibility.VISIBLE, - "Unlisted" => PlaylistVisibility.UNLISTED, - "Private" => PlaylistVisibility.PRIVATE, - _ => PlaylistVisibility.PRIVATE + "Public" => PlaylistVisibility.Visible, + "Unlisted" => PlaylistVisibility.Unlisted, + "Private" => PlaylistVisibility.Private, + _ => PlaylistVisibility.Private }, VideoIds = playlist["videos"]!.ToObject()! }); @@ -228,7 +228,7 @@ private static ImportedData ExtractPipedPlaylists(byte[] data) // Piped doesn't seem to have playlist privacy, and // from my testing, I could just access a playlist I // created without logging in (which makes it not private) - Visibility = PlaylistVisibility.UNLISTED, + Visibility = PlaylistVisibility.Unlisted, VideoIds = playlist["videos"]!.ToObject()!.Select(x => x.Split("?v=")[1]).ToArray() }); } diff --git a/LightTube/Program.cs b/LightTube/Program.cs index 2763c537..6bb04a0b 100644 --- a/LightTube/Program.cs +++ b/LightTube/Program.cs @@ -30,7 +30,7 @@ builder.Services.AddControllersWithViews().AddNewtonsoftJson(); InnerTubeAuthorization? auth = Configuration.InnerTubeAuthorization; - builder.Services.AddSingleton(new InnerTube.InnerTube(new InnerTubeConfiguration + builder.Services.AddSingleton(new SimpleInnerTubeClient(new InnerTubeConfiguration { Authorization = auth, CacheSize = Configuration.CacheSize, diff --git a/LightTube/Resources/Localization/en.json b/LightTube/Resources/Localization/en.json index 820fbcb3..7c0fe055 100644 --- a/LightTube/Resources/Localization/en.json +++ b/LightTube/Resources/Localization/en.json @@ -39,6 +39,7 @@ "channel.tab.store": "Store", "channel.tab.about": "About", "channel.tab.search": "Search", + "channel.noplaylists": "This user doesn't have any public playlists.", "download.title": "Download video", "download.format.select": "Please select a format to download.", @@ -109,6 +110,7 @@ "playlist.removevideo.body": "Are you sure that you want to remove this video from playlist \"{0}\"", "playlist.removevideo.confirm": "Remove", "playlist.unavailable": "Playlist unavailable", + "playlist.video.uncached": "Uncached video. Load the video to update the cache", "playlist.videos.count": "{0} videos", "playlist.view.full": "View Full Playlist", "playlist.visibility.0": "Private", diff --git a/LightTube/Utils.cs b/LightTube/Utils.cs index bbcb012a..b98f2060 100644 --- a/LightTube/Utils.cs +++ b/LightTube/Utils.cs @@ -1,13 +1,13 @@ using System.Collections.Specialized; -using System.Diagnostics; using System.Globalization; using System.Net; -using System.Reflection; using System.Security.Cryptography; using System.Text; using System.Web; using System.Xml; using InnerTube; +using InnerTube.Protobuf.Params; +using InnerTube.Protobuf.Responses; using LightTube.Database; using LightTube.Database.Models; using LightTube.Localization; @@ -17,8 +17,8 @@ namespace LightTube; public static class Utils { - private static string? _version; - private static string? _itVersion; + private static string? version; + private static string? itVersion; public static string UserIdAlphabet => "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"; public static string[] OauthScopes = @@ -32,50 +32,46 @@ public static class Utils public static string GetInnerTubeRegion(this HttpContext context) => context.Request.Headers.TryGetValue("X-Content-Region", out StringValues h) ? h.ToString() - : context.Request.Cookies.TryGetValue("gl", out string region) + : context.Request.Cookies.TryGetValue("gl", out string? region) ? region : Configuration.DefaultContentRegion; public static string GetInnerTubeLanguage(this HttpContext context) => context.Request.Headers.TryGetValue("X-Content-Language", out StringValues h) ? h.ToString() - : context.Request.Cookies.TryGetValue("hl", out string language) && language != "localized" + : context.Request.Cookies.TryGetValue("hl", out string? language) && language != "localized" ? language : LocalizationManager.GetFromHttpContext(context).GetRawString("language.innertube"); public static bool IsInnerTubeLanguageLocalized(this HttpContext context) => - context.Request.Cookies.TryGetValue("hl", out string language) ? language == "localized" : true; + !context.Request.Cookies.TryGetValue("hl", out string? language) || language == "localized"; public static bool GetDefaultRecommendationsVisibility(this HttpContext context) => - context.Request.Cookies.TryGetValue("recommendations", out string recommendations) - ? recommendations == "visible" - : true; + !context.Request.Cookies.TryGetValue("recommendations", out string? recommendations) || recommendations == "visible"; public static bool GetDefaultCompatibility(this HttpContext context) => - context.Request.Cookies.TryGetValue("compatibility", out string compatibility) - ? compatibility == "true" - : false; + context.Request.Cookies.TryGetValue("compatibility", out string? compatibility) && compatibility == "true"; public static string GetVersion() { - if (_version is null) + if (version is null) { #if DEBUG DateTime buildTime = DateTime.Today; - _version = $"{buildTime.Year}.{buildTime.Month}.{buildTime.Day} (dev)"; + version = $"{buildTime.Year}.{buildTime.Month}.{buildTime.Day} (dev)"; #else _version = Assembly.GetExecutingAssembly().GetName().Version!.ToString()[2..]; #endif } - return _version; + return version; } public static string GetInnerTubeVersion() { - _itVersion ??= typeof(InnerTube.InnerTube).Assembly.GetName().Version!.ToString(); + itVersion ??= typeof(InnerTube.InnerTube).Assembly.GetName().Version!.ToString(); - return _itVersion; + return itVersion; } public static string GetCodecFromMimeType(string mime) @@ -207,7 +203,7 @@ public static string GetDashManifest(InnerTubePlayer player, string? proxyUrl = period.AppendChild(doc.CreateComment("Audio Adaptation Sets")); List audios = player.AdaptiveFormats - .Where(x => x.AudioChannels.HasValue) + .Where(x => x.AudioChannels != null) .ToList(); IEnumerable> grouped = audios.GroupBy(x => x.AudioTrack?.Id); foreach (IGrouping formatGroup in grouped.OrderBy(x => x.First().AudioTrack?.AudioIsDefault)) @@ -215,7 +211,7 @@ public static string GetDashManifest(InnerTubePlayer player, string? proxyUrl = XmlElement audioAdaptationSet = doc.CreateElement("AdaptationSet"); audioAdaptationSet.SetAttribute("mimeType", - HttpUtility.ParseQueryString(audios.First().Url.Query).Get("mime")); + HttpUtility.ParseQueryString(audios.First().Url.Split('?')[1]).Get("mime")); audioAdaptationSet.SetAttribute("subsegmentAlignment", "true"); audioAdaptationSet.SetAttribute("contentType", "audio"); audioAdaptationSet.SetAttribute("lang", formatGroup.Key); @@ -223,27 +219,27 @@ public static string GetDashManifest(InnerTubePlayer player, string? proxyUrl = if (formatGroup.First().AudioTrack != null) { XmlElement label = doc.CreateElement("Label"); - label.InnerText = formatGroup.First().AudioTrack?.DisplayName; + label.InnerText = formatGroup.First().AudioTrack.DisplayName; audioAdaptationSet.AppendChild(label); } foreach (Format format in formatGroup) { XmlElement representation = doc.CreateElement("Representation"); - representation.SetAttribute("id", format.Itag); - representation.SetAttribute("codecs", GetCodecFromMimeType(format.MimeType)); + representation.SetAttribute("id", format.Itag.ToString()); + representation.SetAttribute("codecs", GetCodecFromMimeType(format.Mime)); representation.SetAttribute("startWithSAP", "1"); representation.SetAttribute("bandwidth", format.Bitrate.ToString()); XmlElement audioChannelConfiguration = doc.CreateElement("AudioChannelConfiguration"); audioChannelConfiguration.SetAttribute("schemeIdUri", "urn:mpeg:dash:23003:3:audio_channel_configuration:2011"); - audioChannelConfiguration.SetAttribute("value", format.AudioChannels?.ToString()); + audioChannelConfiguration.SetAttribute("value", format.AudioChannels.ToString()); representation.AppendChild(audioChannelConfiguration); XmlElement baseUrl = doc.CreateElement("BaseURL"); baseUrl.InnerText = string.IsNullOrWhiteSpace(proxyUrl) - ? format.Url.ToString() + ? format.Url : $"{proxyUrl}/media/{player.Details.Id}/{format.Itag}?audioTrackId={format.AudioTrack?.Id}"; representation.AppendChild(baseUrl); @@ -269,11 +265,11 @@ public static string GetDashManifest(InnerTubePlayer player, string? proxyUrl = period.AppendChild(doc.CreateComment("Video Adaptation Set")); - List videos = player.AdaptiveFormats.Where(x => !x.AudioChannels.HasValue).ToList(); + List videos = player.AdaptiveFormats.Where(x => x.AudioChannels == null).ToList(); XmlElement videoAdaptationSet = doc.CreateElement("AdaptationSet"); videoAdaptationSet.SetAttribute("mimeType", - HttpUtility.ParseQueryString(videos.FirstOrDefault()?.Url.Query ?? "mime=video/mp4") + HttpUtility.ParseQueryString(videos.FirstOrDefault()?.Url.Split('?')[0] ?? "mime=video/mp4") .Get("mime")); videoAdaptationSet.SetAttribute("subsegmentAlignment", "true"); videoAdaptationSet.SetAttribute("contentType", "video"); @@ -281,8 +277,8 @@ public static string GetDashManifest(InnerTubePlayer player, string? proxyUrl = foreach (Format format in videos) { XmlElement representation = doc.CreateElement("Representation"); - representation.SetAttribute("id", format.Itag); - representation.SetAttribute("codecs", GetCodecFromMimeType(format.MimeType)); + representation.SetAttribute("id", format.Itag.ToString()); + representation.SetAttribute("codecs", GetCodecFromMimeType(format.Mime)); representation.SetAttribute("startWithSAP", "1"); representation.SetAttribute("width", format.Width.ToString()); representation.SetAttribute("height", format.Height.ToString()); @@ -290,7 +286,7 @@ public static string GetDashManifest(InnerTubePlayer player, string? proxyUrl = XmlElement baseUrl = doc.CreateElement("BaseURL"); baseUrl.InnerText = string.IsNullOrWhiteSpace(proxyUrl) - ? format.Url.ToString() + ? format.Url : $"{proxyUrl}/media/{player.Details.Id}/{format.Itag}"; representation.AppendChild(baseUrl); @@ -388,17 +384,14 @@ public static SubscriptionType GetSubscriptionType(this HttpContext context, str public static string GetExtension(this Format format) { - if (format.MimeType.StartsWith("video")) - format.MimeType + if (format.Mime.StartsWith("video")) + return format.Mime .Split("/").Last() .Split(";").First(); - else - { - if (format.MimeType.Contains("opus")) - return "opus"; - if (format.MimeType.Contains("mp4a")) - return "mp3"; - } + if (format.Mime.Contains("opus")) + return "opus"; + if (format.Mime.Contains("mp4a")) + return "mp3"; return "mp4"; } diff --git a/LightTube/YoutubeRSS.cs b/LightTube/YoutubeRSS.cs deleted file mode 100644 index 6b4cc602..00000000 --- a/LightTube/YoutubeRSS.cs +++ /dev/null @@ -1,121 +0,0 @@ -using InnerTube; -using InnerTube.Renderers; - -namespace LightTube; - -public static class YoutubeRSS -{ - private static InnerTube.InnerTube _innerTube = new(); - - /* - * BANDAID CODE AFTER YOUTUBE REMOVED THE RSS ENDPOINT - * PLEASE, *PLEASE* DONT USE THIS IN v3 - * - * here have a nyan cat - * - * ⠀⠀⠀ ⠀⠀⠀⢀⣀⣀⣀⣤⣤⣤⣤⣤⣤⣤⣤⣤⣤⣤⣤⣤⣀⣀⠀⠀⠀⠀⠀ - * ⠀⠀⠀⠀⠀⠀⣾⠉⠀⠀⠀⠀⠀⠀⠀⠀⠀⣀⠀⠀⠀⠀⢀⠀⠈⡇⠀⠀⠀⠀ - * ⠀⠀⠀⠀⠀⠀⣿⠀⠁⠀⠘⠁⠀⠀⠀⠀⠀⣀⡀⠀⠀⠀⠈⠀⠀⡇⠀⠀⠀⠀ - * ⣀⣀⣀⠀⠀⠀⣿⠀⠀⠀⠀⠀⠄⠀⠀⠸⢰⡏⠉⠳⣄⠰⠀⠀⢰⣷⠶⠛⣧⠀ - * ⢻⡀⠈⠙⠲⡄⣿⠀⠀⠀⠀⠀⠀⠀⠠⠀⢸⠀⠀⠀⠈⠓⠒⠒⠛⠁⠀⠀⣿⠀ - * ⠀⠻⣄⠀⠀⠙⣿⠀⠀⠀⠈⠁⠀⢠⠄⣰⠟⠀⢀⡔⢠⠀⠀⠀⠀⣠⠠⡄⠘⢧ - * ⠀⠀⠈⠛⢦⣀⣿⠀⠀⢠⡆⠀⠀⠈⠀⣯⠀⠀⠈⠛⠛⠀⠠⢦⠄⠙⠛⠃⠀⢸ - * ⠀⠀⠀⠀⠀⠉⣿⠀⠀⠀⢠⠀⠀⢠⠀⠹⣆⠀⠀⠀⠢⢤⠠⠞⠤⡠⠄⠀⢀⡾ - * ⠀⠀⠀⠀⠀⢀⡿⠦⢤⣤⣤⣤⣤⣤⣤⣤⡼⣷⠶⠤⢤⣤⣤⡤⢤⡤⠶⠖⠋⠀ - * ⠀⠀⠀⠀⠀⠸⣤⡴⠋⠸⣇⣠⠼⠁⠀⠀⠀⠹⣄⣠⠞⠀⢾⡀⣠⠃⠀⠀⠀⠀ - * ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠉⠁⠀⠀⠀⠀⠀ - */ - public static async Task GetChannelFeed(string channelId) - { - try - { - InnerTubeChannelResponse response = await _innerTube.GetChannelAsync(channelId, ChannelTabs.Videos); - DateTimeOffset reference = DateTimeOffset.Now; - List videos = []; - - foreach (IRenderer renderer in response.Contents.Select(x => - x is RichItemRenderer rir ? rir.Content : x - )) - { - if (renderer is VideoRenderer video) - { - videos.Add(new FeedVideo - { - Id = video.Id, - Title = video.Title, - Description = video.Description, - ViewCount = (long)InnerTube.Utils.ParseNumber(video.ViewCount), - Thumbnail = video.Thumbnails.FirstOrDefault()?.Url.ToString() ?? "", - ChannelName = response.Metadata.Title, - ChannelId = response.Metadata.Id, - PublishedDate = ParseTimeAgo(reference, video.Published ?? "1 year ago") - }); - } - } - - return new ChannelFeed - { - Name = response.Metadata.Title, - Id = response.Metadata.Id, - Videos = [.. videos] - }; - } - catch (Exception) - { - return new ChannelFeed - { - Name = "Failed to get videos for channel " + channelId, - Id = channelId, - Videos = [] - }; - } - } - - public static DateTimeOffset ParseTimeAgo(DateTimeOffset reference, string time) - { - string[] parts = time.ToLower().Split(" "); - int amount = int.Parse(parts[0]); - return parts[1].TrimEnd('s') switch - { - "second" => reference.AddSeconds(-amount), - "minute" => reference.AddMinutes(-amount), - "hour" => reference.AddHours(-amount), - "day" => reference.AddDays(-amount), - "week" => reference.AddDays(-amount * 7), - "month" => reference.AddMonths(-amount), - "year" => reference.AddYears(-amount), - _ => throw new KeyNotFoundException("Unknown timeago metric, " + parts[1].TrimEnd('s')) - }; - } - - public static async Task GetMultipleFeeds(IEnumerable channelIds) - { - Task[] feeds = channelIds.Select(GetChannelFeed).ToArray(); - await Task.WhenAll(feeds); - - List videos = []; - foreach (ChannelFeed feed in feeds.Select(x => x.Result)) videos.AddRange(feed.Videos); - - videos.Sort((a, b) => DateTimeOffset.Compare(b.PublishedDate, a.PublishedDate)); - return [.. videos]; - } -} - -public class ChannelFeed -{ - public string Name; - public string Id; - public FeedVideo[] Videos; -} - -public class FeedVideo -{ - public string Id; - public string Title; - public string Description; - public long ViewCount; - public string Thumbnail; - public string ChannelName; - public string ChannelId; - public DateTimeOffset PublishedDate; -} \ No newline at end of file diff --git a/LightTube/YoutubeRss.cs b/LightTube/YoutubeRss.cs new file mode 100644 index 00000000..b49bd46e --- /dev/null +++ b/LightTube/YoutubeRss.cs @@ -0,0 +1,83 @@ +using System.Xml.Linq; + +namespace LightTube; + +public static class YoutubeRss +{ + private static HttpClient httpClient = new(); + + public static async Task GetChannelFeed(string channelId) + { + HttpResponseMessage response = + await httpClient.GetAsync("https://www.youtube.com/feeds/videos.xml?channel_id=" + channelId); + if (!response.IsSuccessStatusCode) + return new ChannelFeed( + $"Failed to get channel videos: HTTP {(int)response.StatusCode}", + channelId, + [] + ); + + string xml = await response.Content.ReadAsStringAsync(); + XDocument doc = XDocument.Parse(xml); + + ChannelFeed feed = new( + doc.Descendants().First(p => p.Name.LocalName == "title").Value, + doc.Descendants().First(p => p.Name.LocalName == "channelId").Value, doc + .Descendants() + .Where(p => p.Name.LocalName == "entry") + .Select(x => new FeedVideo( + x.Descendants().First(p => p.Name.LocalName == "videoId").Value, + x.Descendants().First(p => p.Name.LocalName == "title").Value, + x.Descendants().First(p => p.Name.LocalName == "description").Value, + long.Parse(x.Descendants().First(p => p.Name.LocalName == "statistics").Attribute("views")?.Value ?? + "-1"), + x.Descendants().First(p => p.Name.LocalName == "thumbnail").Attribute("url")?.Value ?? + $"https://i.ytimg.com/vi/{x.Descendants().First(p => p.Name.LocalName == "videoId").Value}/hqdefault.jpg", + x.Descendants().First(p => p.Name.LocalName == "name").Value, + x.Descendants().First(p => p.Name.LocalName == "channelId").Value, + DateTimeOffset.Parse(x.Descendants().First(p => p.Name.LocalName == "published").Value) + )) + ); + + return feed; + } + + public static async Task GetMultipleFeeds(IEnumerable channelIds) + { + Task[] feeds = channelIds.Select(GetChannelFeed).ToArray(); + await Task.WhenAll(feeds); + + List videos = []; + foreach (ChannelFeed feed in feeds.Select(x => x.Result)) videos.AddRange(feed.Videos); + + videos.Sort((a, b) => DateTimeOffset.Compare(b.PublishedDate, a.PublishedDate)); + return videos.ToArray(); + } +} + +public class ChannelFeed(string id, string name, IEnumerable videos) +{ + public string Name = name; + public string Id = id; + public FeedVideo[] Videos = videos.ToArray(); +} + +public class FeedVideo( + string id, + string title, + string description, + long viewCount, + string thumbnail, + string channelName, + string channelId, + DateTimeOffset publishedDate) +{ + public string Id = id; + public string Title = title; + public string Description = description; + public long ViewCount = viewCount; + public string Thumbnail = thumbnail; + public string ChannelName = channelName; + public string ChannelId = channelId; + public DateTimeOffset PublishedDate = publishedDate; +} \ No newline at end of file From 8ca34496876fda3585e333f03119c61eac2083c8 Mon Sep 17 00:00:00 2001 From: Kayra Uylar Date: Sun, 19 May 2024 16:40:14 +0300 Subject: [PATCH 003/114] Add a GetLocals method since the new InnerTube doesn't have it --- LightTube/Utils.cs | 205 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 205 insertions(+) diff --git a/LightTube/Utils.cs b/LightTube/Utils.cs index b98f2060..41c93b66 100644 --- a/LightTube/Utils.cs +++ b/LightTube/Utils.cs @@ -8,6 +8,7 @@ using InnerTube; using InnerTube.Protobuf.Params; using InnerTube.Protobuf.Responses; +using LightTube.ApiModels; using LightTube.Database; using LightTube.Database.Models; using LightTube.Localization; @@ -503,4 +504,208 @@ public static float ExtractHeaderQualityValue(string s) string[] parts = s.Split("q="); return parts.Length > 1 && float.TryParse(parts[1], out float val) ? val : 1; } + + public static ApiLocals GetLocals() => + new() + { + Languages = new Dictionary + { + ["af"] = "Afrikaans", + ["az"] = "Azərbaycan", + ["id"] = "Bahasa Indonesia", + ["ms"] = "Bahasa Malaysia", + ["bs"] = "Bosanski", + ["ca"] = "Català", + ["cs"] = "Čeština", + ["da"] = "Dansk", + ["de"] = "Deutsch", + ["et"] = "Eesti", + ["en-IN"] = "English (India)", + ["en-GB"] = "English (UK)", + ["en"] = "English (US)", + ["es"] = "Español (España)", + ["es-419"] = "Español (Latinoamérica)", + ["es-US"] = "Español (US)", + ["eu"] = "Euskara", + ["fil"] = "Filipino", + ["fr"] = "Français", + ["fr-CA"] = "Français (Canada)", + ["gl"] = "Galego", + ["hr"] = "Hrvatski", + ["zu"] = "IsiZulu", + ["is"] = "Íslenska", + ["it"] = "Italiano", + ["sw"] = "Kiswahili", + ["lv"] = "Latviešu valoda", + ["lt"] = "Lietuvių", + ["hu"] = "Magyar", + ["nl"] = "Nederlands", + ["no"] = "Norsk", + ["uz"] = "O‘zbek", + ["pl"] = "Polski", + ["pt-PT"] = "Português", + ["pt"] = "Português (Brasil)", + ["ro"] = "Română", + ["sq"] = "Shqip", + ["sk"] = "Slovenčina", + ["sl"] = "Slovenščina", + ["sr-Latn"] = "Srpski", + ["fi"] = "Suomi", + ["sv"] = "Svenska", + ["vi"] = "Tiếng Việt", + ["tr"] = "Türkçe", + ["be"] = "Беларуская", + ["bg"] = "Български", + ["ky"] = "Кыргызча", + ["kk"] = "Қазақ Тілі", + ["mk"] = "Македонски", + ["mn"] = "Монгол", + ["ru"] = "Русский", + ["sr"] = "Српски", + ["uk"] = "Українська", + ["el"] = "Ελληνικά", + ["hy"] = "Հայերեն", + ["iw"] = "עברית", + ["ur"] = "اردو", + ["ar"] = "العربية", + ["fa"] = "فارسی", + ["ne"] = "नेपाली", + ["mr"] = "मराठी", + ["hi"] = "हिन्दी", + ["as"] = "অসমীয়া", + ["bn"] = "বাংলা", + ["pa"] = "ਪੰਜਾਬੀ", + ["gu"] = "ગુજરાતી", + ["or"] = "ଓଡ଼ିଆ", + ["ta"] = "தமிழ்", + ["te"] = "తెలుగు", + ["kn"] = "ಕನ್ನಡ", + ["ml"] = "മലയാളം", + ["si"] = "සිංහල", + ["th"] = "ภาษาไทย", + ["lo"] = "ລາວ", + ["my"] = "ဗမာ", + ["ka"] = "ქართული", + ["am"] = "አማርኛ", + ["km"] = "ខ្មែរ", + ["zh-CN"] = "中文 (简体)", + ["zh-TW"] = "中文 (繁體)", + ["zh-HK"] = "中文 (香港)", + ["ja"] = "日本語", + ["ko"] = "한국어" + }, + Regions = new Dictionary + { + ["DZ"] = "Algeria", + ["AR"] = "Argentina", + ["AU"] = "Australia", + ["AT"] = "Austria", + ["AZ"] = "Azerbaijan", + ["BH"] = "Bahrain", + ["BD"] = "Bangladesh", + ["BY"] = "Belarus", + ["BE"] = "Belgium", + ["BO"] = "Bolivia", + ["BA"] = "Bosnia and Herzegovina", + ["BR"] = "Brazil", + ["BG"] = "Bulgaria", + ["KH"] = "Cambodia", + ["CA"] = "Canada", + ["CL"] = "Chile", + ["CO"] = "Colombia", + ["CR"] = "Costa Rica", + ["HR"] = "Croatia", + ["CY"] = "Cyprus", + ["CZ"] = "Czechia", + ["DK"] = "Denmark", + ["DO"] = "Dominican Republic", + ["EC"] = "Ecuador", + ["EG"] = "Egypt", + ["SV"] = "El Salvador", + ["EE"] = "Estonia", + ["FI"] = "Finland", + ["FR"] = "France", + ["GE"] = "Georgia", + ["DE"] = "Germany", + ["GH"] = "Ghana", + ["GR"] = "Greece", + ["GT"] = "Guatemala", + ["HN"] = "Honduras", + ["HK"] = "Hong Kong", + ["HU"] = "Hungary", + ["IS"] = "Iceland", + ["IN"] = "India", + ["ID"] = "Indonesia", + ["IQ"] = "Iraq", + ["IE"] = "Ireland", + ["IL"] = "Israel", + ["IT"] = "Italy", + ["JM"] = "Jamaica", + ["JP"] = "Japan", + ["JO"] = "Jordan", + ["KZ"] = "Kazakhstan", + ["KE"] = "Kenya", + ["KW"] = "Kuwait", + ["LA"] = "Laos", + ["LV"] = "Latvia", + ["LB"] = "Lebanon", + ["LY"] = "Libya", + ["LI"] = "Liechtenstein", + ["LT"] = "Lithuania", + ["LU"] = "Luxembourg", + ["MY"] = "Malaysia", + ["MT"] = "Malta", + ["MX"] = "Mexico", + ["MD"] = "Moldova", + ["ME"] = "Montenegro", + ["MA"] = "Morocco", + ["NP"] = "Nepal", + ["NL"] = "Netherlands", + ["NZ"] = "New Zealand", + ["NI"] = "Nicaragua", + ["NG"] = "Nigeria", + ["MK"] = "North Macedonia", + ["NO"] = "Norway", + ["OM"] = "Oman", + ["PK"] = "Pakistan", + ["PA"] = "Panama", + ["PG"] = "Papua New Guinea", + ["PY"] = "Paraguay", + ["PE"] = "Peru", + ["PH"] = "Philippines", + ["PL"] = "Poland", + ["PT"] = "Portugal", + ["PR"] = "Puerto Rico", + ["QA"] = "Qatar", + ["RO"] = "Romania", + ["RU"] = "Russia", + ["SA"] = "Saudi Arabia", + ["SN"] = "Senegal", + ["RS"] = "Serbia", + ["SG"] = "Singapore", + ["SK"] = "Slovakia", + ["SI"] = "Slovenia", + ["ZA"] = "South Africa", + ["KR"] = "South Korea", + ["ES"] = "Spain", + ["LK"] = "Sri Lanka", + ["SE"] = "Sweden", + ["CH"] = "Switzerland", + ["TW"] = "Taiwan", + ["TZ"] = "Tanzania", + ["TH"] = "Thailand", + ["TN"] = "Tunisia", + ["TR"] = "Turkey", + ["UG"] = "Uganda", + ["UA"] = "Ukraine", + ["AE"] = "United Arab Emirates", + ["GB"] = "United Kingdom", + ["US"] = "United States", + ["UY"] = "Uruguay", + ["VE"] = "Venezuela", + ["VN"] = "Vietnam", + ["YE"] = "Yemen", + ["ZW"] = "Zimbabwe" + } + }; } \ No newline at end of file From b14f05bfbf91bc92fc4a47e7b7e5951a025c3d37 Mon Sep 17 00:00:00 2001 From: Kayra Uylar Date: Sun, 19 May 2024 16:45:42 +0300 Subject: [PATCH 004/114] Fix errors in API endpoints --- LightTube/ApiModels/ApiChannel.cs | 125 ++-- LightTube/ApiModels/ApiLocals.cs | 7 + LightTube/ApiModels/ApiPlaylist.cs | 147 ++--- LightTube/ApiModels/ApiSearchResults.cs | 46 +- LightTube/ApiModels/ApiUserData.cs | 55 +- .../ApiModels/UpdateSubscriptionResponse.cs | 6 +- LightTube/Controllers/ApiController.cs | 615 +++++++++--------- 7 files changed, 484 insertions(+), 517 deletions(-) create mode 100644 LightTube/ApiModels/ApiLocals.cs diff --git a/LightTube/ApiModels/ApiChannel.cs b/LightTube/ApiModels/ApiChannel.cs index c33fdcd1..06d8fcd3 100644 --- a/LightTube/ApiModels/ApiChannel.cs +++ b/LightTube/ApiModels/ApiChannel.cs @@ -1,4 +1,5 @@ -using InnerTube; +using InnerTube.Models; +using InnerTube.Protobuf; using InnerTube.Renderers; using LightTube.Database.Models; @@ -6,79 +7,57 @@ namespace LightTube.ApiModels; public class ApiChannel { - public string Id { get; } - public string Title { get; } - public IEnumerable Avatars { get; } - public IEnumerable Banner { get; } - public IEnumerable Badges { get; } - public IEnumerable PrimaryLinks { get; } - public IEnumerable SecondaryLinks { get; } - public string SubscriberCountText { get; } - public IEnumerable EnabledTabs { get; } - public IEnumerable Contents { get; } - public string? Continuation { get; } + public ChannelHeader? Header { get; } + public ChannelTab[] Tabs { get; } + public ChannelMetadataRenderer? Metadata { get; } + public RendererContainer[] Contents { get; } - public ApiChannel(InnerTubeChannelResponse channel) - { - if (channel.Header != null) - { - Id = channel.Header.Id; - Title = channel.Header.Title; - Avatars = channel.Header.Avatars; - Banner = channel.Header.Banner; - Badges = channel.Header.Badges; - PrimaryLinks = channel.Header.PrimaryLinks; - SecondaryLinks = channel.Header.SecondaryLinks; - SubscriberCountText = channel.Header.SubscriberCountText; - } - else - { - Id = channel.Metadata.Id; - Title = channel.Metadata.Title; - Avatars = channel.Metadata.Avatar; - Banner = []; - Badges = []; - PrimaryLinks = []; - SecondaryLinks = []; - SubscriberCountText = "Unavailable"; - } + public ApiChannel(InnerTubeChannel channel) + { + Header = channel.Header; + Tabs = channel.Tabs.ToArray(); + Metadata = channel.Metadata; + Contents = channel.Contents; + } - EnabledTabs = channel.EnabledTabs.Select(x => x.ToString()); - Contents = channel.Contents; - Continuation = - (channel.Contents.FirstOrDefault(x => x is ContinuationItemRenderer) as ContinuationItemRenderer)?.Token; - } + public ApiChannel(ContinuationResponse continuation) + { + Header = null; + Tabs = []; + Metadata = null; + List renderers = new(); + renderers.AddRange(continuation.Results); + if (continuation.ContinuationToken != null) + renderers.Add(new RendererContainer + { + Type = "continuation", + OriginalType = "continuationItemRenderer", + Data = new ContinuationRendererData + { + ContinuationToken = continuation.ContinuationToken + } + }); + Contents = renderers.ToArray(); + } - public ApiChannel(InnerTubeContinuationResponse channel) - { - Id = ""; - Title = ""; - Avatars = []; - Banner = []; - Badges = []; - PrimaryLinks = []; - SecondaryLinks = []; - SubscriberCountText = "Unavailable"; - EnabledTabs = []; - Contents = channel.Contents; - Continuation = channel.Continuation; - } - - public ApiChannel(DatabaseUser channel) - { - Id = channel.LTChannelID; - Title = channel.UserID; - Avatars = []; - Banner = []; - Badges = []; - PrimaryLinks = []; - SecondaryLinks = []; - SubscriberCountText = "LightTube account"; - EnabledTabs = - [ - ChannelTabs.Playlists.ToString() - ]; - Contents = [channel.PlaylistRenderers()]; - Continuation = null; - } + public ApiChannel(DatabaseUser channel) + { + throw new Exception("empty ctors not implemented yet"); + /* + Id = channel.LTChannelID; + Title = channel.UserID; + Avatars = []; + Banner = []; + Badges = []; + PrimaryLinks = []; + SecondaryLinks = []; + SubscriberCountText = "LightTube account"; + EnabledTabs = + [ + ChannelTabs.Playlists.ToString() + ]; + Contents = [channel.PlaylistRenderers()]; + Continuation = null; + */ + } } \ No newline at end of file diff --git a/LightTube/ApiModels/ApiLocals.cs b/LightTube/ApiModels/ApiLocals.cs new file mode 100644 index 00000000..3924a938 --- /dev/null +++ b/LightTube/ApiModels/ApiLocals.cs @@ -0,0 +1,7 @@ +namespace LightTube.ApiModels; + +public class ApiLocals +{ + public Dictionary Languages { get; set; } + public Dictionary Regions { get; set; } +} \ No newline at end of file diff --git a/LightTube/ApiModels/ApiPlaylist.cs b/LightTube/ApiModels/ApiPlaylist.cs index 1e1ec215..625d65f7 100644 --- a/LightTube/ApiModels/ApiPlaylist.cs +++ b/LightTube/ApiModels/ApiPlaylist.cs @@ -1,91 +1,82 @@ using InnerTube; +using InnerTube.Models; +using InnerTube.Protobuf; using InnerTube.Renderers; using LightTube.Database; using LightTube.Database.Models; +using LightTube.Localization; namespace LightTube.ApiModels; public class ApiPlaylist { - public string Id { get; } - public IEnumerable Alerts { get; } - public string Title { get; } - public string Description { get; } - public IEnumerable Badges { get; } - public Channel Channel { get; } - public IEnumerable Thumbnails { get; } - public string LastUpdated { get; } - public string VideoCountText { get; } - public string ViewCountText { get; } - public PlaylistContinuationInfo? Continuation { get; } - public IEnumerable Videos { get; } + public string Id { get; } + public string[] Alerts { get; } + public RendererContainer[] Contents { get; } + public RendererContainer[] Chips { get; } + public string? Continuation { get; } + public PlaylistSidebar? Sidebar { get; } - public ApiPlaylist(InnerTubePlaylist playlist) - { - Id = playlist.Id; - Alerts = playlist.Alerts; - Title = playlist.Sidebar.Title; - Description = playlist.Sidebar.Description; - Badges = playlist.Sidebar.Badges; - Channel = playlist.Sidebar.Channel; - Thumbnails = playlist.Sidebar.Thumbnails; - LastUpdated = playlist.Sidebar.LastUpdated; - VideoCountText = playlist.Sidebar.VideoCountText; - ViewCountText = playlist.Sidebar.ViewCountText; - Continuation = playlist.Continuation; - Videos = playlist.Videos; - } + public ApiPlaylist(InnerTubePlaylist playlist) + { + Id = playlist.Id; + Alerts = playlist.Alerts; + Contents = playlist.Contents; + Chips = playlist.Chips; + Continuation = playlist.Continuation; + Sidebar = playlist.Sidebar; + } - public ApiPlaylist(InnerTubeContinuationResponse playlist) - { - Id = ""; - Alerts = []; - Title = ""; - Description = ""; - Badges = []; - Channel = new Channel(); - Thumbnails = []; - LastUpdated = ""; - VideoCountText = ""; - ViewCountText = ""; - Continuation = playlist.Continuation is not null ? InnerTube.Utils.UnpackPlaylistContinuation(playlist.Continuation) : null; - Videos = playlist.Contents.Cast(); - } + public ApiPlaylist(ContinuationResponse playlist) + { + Id = ""; + Alerts = []; + Contents = playlist.Results; + Chips = []; + Continuation = playlist.ContinuationToken; + Sidebar = null; + } - public ApiPlaylist(DatabasePlaylist playlist) - { - Id = playlist.Id; - Alerts = []; - Title = playlist.Name; - Description = playlist.Description; - Badges = []; - DatabaseUser user = DatabaseManager.Users.GetUserFromId(playlist.Author).Result!; - Channel = new Channel - { - Id = user.LTChannelID, - Title = user.UserID, - Avatar = null, - Subscribers = null, - Badges = [] - }; - Thumbnails = - [ - new Thumbnail - { - Width = null, - Height = null, - Url = new Uri($"https://i.ytimg.com/vi/{playlist.VideoIds.FirstOrDefault()}/hqdefault.jpg") - } - ]; - LastUpdated = $"Last updated on {playlist.LastUpdated:MMM d, yyyy}"; - VideoCountText = playlist.VideoIds.Count switch - { - 0 => "No videos", - 1 => "1 video", - _ => $"{playlist.VideoIds.Count} videos" - }; - ViewCountText = "LightTube playlist"; - Continuation = null; - Videos = DatabaseManager.Playlists.GetPlaylistVideos(playlist.Id, false); - } + public ApiPlaylist(DatabasePlaylist playlist, LocalizationManager localization) + { + Id = playlist.Id; + Alerts = []; + Contents = DatabaseManager.Playlists.GetPlaylistVideos(playlist.Id, false, localization).ToArray(); + Chips = []; + Continuation = null; + Sidebar = null; // TODO + /* + Title = playlist.Name; + Description = playlist.Description; + Badges = []; + DatabaseUser user = DatabaseManager.Users.GetUserFromId(playlist.Author).Result!; + Channel = new Channel + { + Id = user.LTChannelID, + Title = user.UserID, + Avatar = null, + Subscribers = null, + Badges = [] + }; + Thumbnails = + [ + new Thumbnail + { + Width = null, + Height = null, + Url = new Uri($"https://i.ytimg.com/vi/{playlist.VideoIds.FirstOrDefault()}/hqdefault.jpg") + } + ]; + LastUpdated = $"Last updated on {playlist.LastUpdated:MMM d, yyyy}"; + VideoCountText = playlist.VideoIds.Count switch + { + 0 => "No videos", + 1 => "1 video", + _ => $"{playlist.VideoIds.Count} videos" + }; + ViewCountText = "LightTube playlist"; + Continuation = null; + Videos = + */ + } } \ No newline at end of file diff --git a/LightTube/ApiModels/ApiSearchResults.cs b/LightTube/ApiModels/ApiSearchResults.cs index ff01a772..cfed7509 100644 --- a/LightTube/ApiModels/ApiSearchResults.cs +++ b/LightTube/ApiModels/ApiSearchResults.cs @@ -1,27 +1,39 @@ using InnerTube; +using InnerTube.Models; +using InnerTube.Protobuf.Params; using InnerTube.Renderers; namespace LightTube.ApiModels; public class ApiSearchResults { - public IEnumerable SearchResults { get; } - public long? EstimatedResultCount { get; } - public SearchParams? SearchParams { get; } - public string? ContinuationKey { get; } + public RendererContainer[] Results { get; } + public ShowingResultsFor? QueryCorrecter { get; } + public RendererContainer[] Chips { get; } + public string? Continuation { get; } + public string[] Refinements { get; } + public long EstimatedResults { get; } + public SearchParams? SearchParams { get; } - public ApiSearchResults(InnerTubeSearchResults results, SearchParams searchParams) - { - SearchParams = searchParams; - SearchResults = results.Results; - ContinuationKey = results.Continuation; - EstimatedResultCount = results.EstimatedResults; - } + public ApiSearchResults(InnerTubeSearchResults results, SearchParams searchParams) + { + Results = results.Results; + QueryCorrecter = results.QueryCorrecter; + Chips = results.Chips; + Continuation = results.Continuation; + Refinements = results.Refinements; + EstimatedResults = results.EstimatedResults; + SearchParams = searchParams; + } - public ApiSearchResults(InnerTubeContinuationResponse continuationResults) - { - SearchResults = continuationResults.Contents; - ContinuationKey = continuationResults.Continuation; - EstimatedResultCount = null; - } + public ApiSearchResults(ContinuationResponse continuationResults) + { + Results = continuationResults.Results; + QueryCorrecter = null; + Chips = []; + Continuation = continuationResults.ContinuationToken; + Refinements = []; + EstimatedResults = 0; + SearchParams = null; + } } \ No newline at end of file diff --git a/LightTube/ApiModels/ApiUserData.cs b/LightTube/ApiModels/ApiUserData.cs index 6e039333..68ef46bb 100644 --- a/LightTube/ApiModels/ApiUserData.cs +++ b/LightTube/ApiModels/ApiUserData.cs @@ -18,55 +18,24 @@ public class ApiUserData }; } - public void CalculateWithRenderers(IEnumerable renderers) + public void CalculateWithRenderers(IEnumerable renderers) { - foreach (IRenderer renderer in renderers) + foreach (RendererContainer renderer in renderers) CalculateWithRenderer(renderer); } - private void CalculateWithRenderer(IRenderer renderer) + private void CalculateWithRenderer(RendererContainer renderer) { - switch (renderer) + switch (renderer.Type) { - case ChannelRenderer channel: - AddInfoForChannel(channel.Id); + case "channel": + AddInfoForChannel((renderer.Data as ChannelRendererData)?.ChannelId); break; - case VideoRenderer video: - AddInfoForChannel(video.Channel.Id); + case "video": + AddInfoForChannel((renderer.Data as VideoRendererData)?.Author?.Id); break; - case CompactVideoRenderer video: - AddInfoForChannel(video.Channel.Id); - break; - case GridVideoRenderer video: - AddInfoForChannel(video.Channel.Id); - break; - case PlaylistVideoRenderer video: - AddInfoForChannel(video.Channel.Id); - break; - case PlaylistPanelVideoRenderer video: - AddInfoForChannel(video.Channel.Id); - break; - - case ShelfRenderer shelf: - CalculateWithRenderers(shelf.Items); - break; - case ReelShelfRenderer shelf: - CalculateWithRenderers(shelf.Items); - break; - case RichShelfRenderer shelf: - CalculateWithRenderers(shelf.Contents); - break; - case SectionListRenderer list: - CalculateWithRenderers(list.Contents); - break; - case ItemSectionRenderer section: - CalculateWithRenderers(section.Contents); - break; - case RichSectionRenderer section: - CalculateWithRenderer(section.Content); - break; - case GridRenderer grid: - CalculateWithRenderers(grid.Items); + case "container": + CalculateWithRenderers((renderer.Data as ContainerRendererData)?.Items ?? []); break; } } @@ -74,7 +43,7 @@ private void CalculateWithRenderer(IRenderer renderer) public void AddInfoForChannel(string? channelId) { if (channelId == null) return; - if (User.Subscriptions.ContainsKey(channelId) && !Channels.ContainsKey(channelId)) - Channels.Add(channelId, new ApiSubscriptionInfo(User.Subscriptions[channelId])); + if (User.Subscriptions.TryGetValue(channelId, out SubscriptionType value) && !Channels.ContainsKey(channelId)) + Channels.Add(channelId, new ApiSubscriptionInfo(value)); } } \ No newline at end of file diff --git a/LightTube/ApiModels/UpdateSubscriptionResponse.cs b/LightTube/ApiModels/UpdateSubscriptionResponse.cs index fe4193a4..c7741c83 100644 --- a/LightTube/ApiModels/UpdateSubscriptionResponse.cs +++ b/LightTube/ApiModels/UpdateSubscriptionResponse.cs @@ -1,4 +1,4 @@ -using InnerTube; +using InnerTube.Models; using LightTube.ApiModels; using LightTube.Database.Models; @@ -11,7 +11,7 @@ public class UpdateSubscriptionResponse public bool Subscribed { get; } public bool Notifications { get; } - public UpdateSubscriptionResponse(InnerTubeChannelResponse channel, SubscriptionType subscription) + public UpdateSubscriptionResponse(InnerTubeChannel channel, SubscriptionType subscription) { try { @@ -26,6 +26,6 @@ public UpdateSubscriptionResponse(InnerTubeChannelResponse channel, Subscription } ChannelName = channel.Metadata.Title; - ChannelAvatar = channel.Metadata.Avatar.Last().Url.ToString(); + ChannelAvatar = channel.Metadata.Avatar.Thumbnails_.First().Url; } } \ No newline at end of file diff --git a/LightTube/Controllers/ApiController.cs b/LightTube/Controllers/ApiController.cs index fb5f3c24..4fb2393a 100644 --- a/LightTube/Controllers/ApiController.cs +++ b/LightTube/Controllers/ApiController.cs @@ -1,317 +1,326 @@ using System.Net; using System.Text.RegularExpressions; using InnerTube; +using InnerTube.Models; +using InnerTube.Protobuf.Params; using LightTube.ApiModels; using LightTube.Attributes; using LightTube.Database; using LightTube.Database.Models; +using LightTube.Localization; using Microsoft.AspNetCore.Mvc; namespace LightTube.Controllers; [Route("/api")] -public class ApiController(SimpleInnerTubeClient innerTube) : Controller +public partial class ApiController(SimpleInnerTubeClient innerTube) : Controller { - private const string VIDEO_ID_REGEX = @"[a-zA-Z0-9_-]{11}"; - private const string CHANNEL_ID_REGEX = @"[a-zA-Z0-9_-]{24}"; - private const string PLAYLIST_ID_REGEX = @"[a-zA-Z0-9_-]{34}"; - - [Route("info")] - public LightTubeInstanceInfo GetInstanceInfo() => - new() - { - Type = "lighttube/2.0", - Version = Utils.GetVersion(), - Messages = Configuration.Messages, - Alert = Configuration.Alert, - Config = new Dictionary - { - ["allowsApi"] = Configuration.ApiEnabled, - ["allowsNewUsers"] = Configuration.RegistrationEnabled, - ["allowsOauthApi"] = Configuration.OauthEnabled, - ["allowsThirdPartyProxyUsage"] = Configuration.ThirdPartyProxyEnabled - } - }; - - private ApiResponse Error(string message, int code, HttpStatusCode statusCode) - { - Response.StatusCode = (int)statusCode; - return new ApiResponse(statusCode == HttpStatusCode.BadRequest ? "BAD_REQUEST" : "ERROR", - message, code); - } - - [Route("player")] - [ApiDisableable] - public async Task> GetPlayerInfo(string? id, bool contentCheckOk = true) - { - if (id is null) - return Error("Missing video ID (query parameter `id`)", 400, - HttpStatusCode.BadRequest); - - Regex regex = new(VIDEO_ID_REGEX); - if (!regex.IsMatch(id) || id.Length != 11) - return Error($"Invalid video ID: {id}", 400, HttpStatusCode.BadRequest); - - try - { - InnerTubePlayer player = - await _youtube.GetPlayerAsync(id, contentCheckOk, HttpContext.GetInnerTubeLanguage(), - HttpContext.GetInnerTubeRegion()); - - DatabaseUser? user = await DatabaseManager.Oauth2.GetUserFromHttpRequest(Request); - ApiUserData? userData = ApiUserData.GetFromDatabaseUser(user); - userData?.AddInfoForChannel(player.Details.Author.Id); - - return new ApiResponse(player, userData); - } - catch (Exception e) - { - return Error(e.Message, 500, HttpStatusCode.InternalServerError); - } - } - - [Route("video")] - [ApiDisableable] - public async Task> GetVideoInfo( - string? id, - string? playlistId = null, - int? playlistIndex = null, - string? playlistParams = null) - { - if (id is null) - return Error("Missing video ID (query parameter `id`)", 400, - HttpStatusCode.BadRequest); - - Regex regex = new(VIDEO_ID_REGEX); - if (!regex.IsMatch(id) || id.Length != 11) - return Error($"Invalid video ID: {id}", 400, HttpStatusCode.BadRequest); - - try - { - InnerTubeNextResponse video = await _youtube.GetVideoAsync(id, playlistId, playlistIndex, playlistParams, - HttpContext.GetInnerTubeLanguage(), HttpContext.GetInnerTubeRegion()); - - DatabaseUser? user = await DatabaseManager.Oauth2.GetUserFromHttpRequest(Request); - ApiUserData? userData = ApiUserData.GetFromDatabaseUser(user); - userData?.AddInfoForChannel(video.Channel.Id); - userData?.CalculateWithRenderers(video.Recommended); - - return new ApiResponse(video, userData); - } - catch (Exception e) - { - return Error(e.Message, 500, HttpStatusCode.InternalServerError); - } - } - - [Route("search")] - [ApiDisableable] - public async Task> Search(string query, string? continuation = null) - { - if (string.IsNullOrWhiteSpace(query) && string.IsNullOrWhiteSpace(continuation)) - { - return Error( - "Missing query (query parameter `query`) or continuation key (query parameter `continuation`)", - 400, HttpStatusCode.BadRequest); - } - - - ApiSearchResults result; - if (continuation is null) - { - SearchParams searchParams = Request.GetSearchParams(); - InnerTubeSearchResults results = await _youtube.SearchAsync(query, searchParams, HttpContext.GetInnerTubeLanguage(), - HttpContext.GetInnerTubeRegion()); - result = new ApiSearchResults(results, searchParams); - } - else - { - InnerTubeContinuationResponse results = await _youtube.ContinueSearchAsync(continuation, - HttpContext.GetInnerTubeLanguage(), - HttpContext.GetInnerTubeRegion()); - result = new ApiSearchResults(results); - } - - DatabaseUser? user = await DatabaseManager.Oauth2.GetUserFromHttpRequest(Request); - ApiUserData? userData = ApiUserData.GetFromDatabaseUser(user); - userData?.CalculateWithRenderers(result.SearchResults); - - return new ApiResponse(result, userData); - } - - [Route("searchSuggestions")] - [ApiDisableable] - public async Task> SearchSuggestions(string query) - { - if (string.IsNullOrWhiteSpace(query)) - return Error("Missing query (query parameter `query`)", 400, - HttpStatusCode.BadRequest); - try - { - DatabaseUser? user = await DatabaseManager.Oauth2.GetUserFromHttpRequest(Request); - ApiUserData? userData = ApiUserData.GetFromDatabaseUser(user); - return new ApiResponse(await _youtube.GetSearchAutocompleteAsync(query, - HttpContext.GetInnerTubeLanguage(), - HttpContext.GetInnerTubeRegion()), userData); - } - catch (Exception e) - { - return Error(e.Message, 500, HttpStatusCode.InternalServerError); - } - } - - [Route("playlist")] - [ApiDisableable] - public async Task> Playlist(string id, int? skip) - { - if (id.StartsWith("LT-PL")) - { - if (id.Length != 24) - return Error($"Invalid playlist ID: {id}", 400, HttpStatusCode.BadRequest); - } - else - { - Regex regex = new(PLAYLIST_ID_REGEX); - if (!regex.IsMatch(id) || id.Length != 34) - return Error($"Invalid playlist ID: {id}", 400, HttpStatusCode.BadRequest); - } - - - if (string.IsNullOrWhiteSpace(id) && skip is null) - return Error($"Invalid ID: {id}", 400, HttpStatusCode.BadRequest); - - try - { - DatabaseUser? user = await DatabaseManager.Oauth2.GetUserFromHttpRequest(Request); - ApiPlaylist result; - if (id.StartsWith("LT-PL")) - { - DatabasePlaylist? playlist = DatabaseManager.Playlists.GetPlaylist(id); - - if (playlist is null) - return Error("The playlist does not exist.", 500, - HttpStatusCode.InternalServerError); - - if (playlist.Visibility == PlaylistVisibility.Private) - { - if (user == null) - return Error("The playlist does not exist.", 500, - HttpStatusCode.InternalServerError); - - if (playlist.Author != user.UserID) - return Error("The playlist does not exist.", 500, - HttpStatusCode.InternalServerError); - } - - result = new ApiPlaylist(playlist); - } - else if (skip is null) - { - InnerTubePlaylist playlist = - await _youtube.GetPlaylistAsync(id, true, HttpContext.GetInnerTubeLanguage(), HttpContext.GetInnerTubeRegion()); - result = new ApiPlaylist(playlist); - } - else - { - InnerTubeContinuationResponse playlist = - await _youtube.ContinuePlaylistAsync(id, skip.Value, HttpContext.GetInnerTubeLanguage(), - HttpContext.GetInnerTubeRegion()); - result = new ApiPlaylist(playlist); - } - - ApiUserData? userData = ApiUserData.GetFromDatabaseUser(user); - userData?.AddInfoForChannel(result.Channel.Id); - userData?.CalculateWithRenderers(result.Videos); - return new ApiResponse(result, userData); - } - catch (Exception e) - { - return Error(e.Message, 500, HttpStatusCode.InternalServerError); - } - } - - [Route("channel")] - [ApiDisableable] - public async Task> Channel(string id, ChannelTabs tab = ChannelTabs.Home, - string? searchQuery = null, string? continuation = null) - { - if (string.IsNullOrWhiteSpace(id) && string.IsNullOrWhiteSpace(continuation)) - return Error($"Invalid request: missing both `id` and `continuation`", 400, - HttpStatusCode.BadRequest); - - try - { - ApiChannel response; - if (id?.StartsWith("LT") ?? false) - { - DatabaseUser? localUser = await DatabaseManager.Users.GetUserFromLTId(id); - if (localUser is null) - throw new Exception("This user does not exist."); - response = new ApiChannel(localUser); - } - else if (continuation is null) - { - if (!id.StartsWith("UC")) - id = await _youtube.GetChannelIdFromVanity(id) ?? id; - - InnerTubeChannelResponse channel = await _youtube.GetChannelAsync(id, tab, searchQuery, - HttpContext.GetInnerTubeLanguage(), - HttpContext.GetInnerTubeRegion()); - response = new ApiChannel(channel); - } - else - { - InnerTubeContinuationResponse channel = await _youtube.ContinueChannelAsync(continuation); - response = new ApiChannel(channel); - } - - DatabaseUser? user = await DatabaseManager.Oauth2.GetUserFromHttpRequest(Request); - ApiUserData? userData = ApiUserData.GetFromDatabaseUser(user); - userData?.AddInfoForChannel(response.Id); - userData?.CalculateWithRenderers(response.Contents); - - return new ApiResponse(response, userData); - } - catch (Exception e) - { - return Error(e.Message, 500, HttpStatusCode.InternalServerError); - } - } - - [Route("comments")] - [ApiDisableable] - public async Task> Comments(string? continuation, string? id, - CommentsContext.Types.SortOrder sort = CommentsContext.Types.SortOrder.TopComments) - { - try - { - if (id != null && continuation == null) - continuation = InnerTube.Utils.PackCommentsContinuation(id, sort); - else if (id == null && continuation == null) - return Error( - "Invalid request, either 'continuation' or 'id' must be present", 400, - HttpStatusCode.BadRequest); - - InnerTubeContinuationResponse? comments = await _youtube.GetVideoCommentsAsync(continuation!, - HttpContext.GetInnerTubeLanguage(), HttpContext.GetInnerTubeRegion()); - - DatabaseUser? user = await DatabaseManager.Oauth2.GetUserFromHttpRequest(Request); - ApiUserData? userData = ApiUserData.GetFromDatabaseUser(user); - userData?.CalculateWithRenderers(comments.Contents); - - return new ApiResponse(comments, userData); - } - catch (Exception e) - { - return Error(e.Message, 500, HttpStatusCode.InternalServerError); - } - } - - [Route("locals")] - [ApiDisableable] - public async Task Locals() - { - InnerTubeLocals locals = await _youtube.GetLocalsAsync(); - return Json(locals); - } + private readonly Regex videoIdRegex = VideoIdRegex(); + + [Route("info")] + public LightTubeInstanceInfo GetInstanceInfo() => + new() + { + Type = "lighttube/2.0", + Version = Utils.GetVersion(), + Messages = Configuration.Messages, + Alert = Configuration.Alert, + Config = new Dictionary + { + ["allowsApi"] = Configuration.ApiEnabled, + ["allowsNewUsers"] = Configuration.RegistrationEnabled, + ["allowsOauthApi"] = Configuration.OauthEnabled, + ["allowsThirdPartyProxyUsage"] = Configuration.ThirdPartyProxyEnabled + } + }; + + private ApiResponse Error(string message, int code, HttpStatusCode statusCode) + { + Response.StatusCode = (int)statusCode; + return new ApiResponse(statusCode == HttpStatusCode.BadRequest ? "BAD_REQUEST" : "ERROR", + message, code); + } + + [Route("player")] + [ApiDisableable] + public async Task> GetPlayerInfo(string? id, bool contentCheckOk = true) + { + if (id is null) + return Error("Missing video ID (query parameter `id`)", 400, + HttpStatusCode.BadRequest); + + if (!videoIdRegex.IsMatch(id) || id.Length != 11) + return Error($"Invalid video ID: {id}", 400, HttpStatusCode.BadRequest); + + try + { + InnerTubePlayer player = await innerTube.GetVideoPlayerAsync(id, contentCheckOk, + HttpContext.GetInnerTubeLanguage(), + HttpContext.GetInnerTubeRegion()); + + DatabaseUser? user = await DatabaseManager.Oauth2.GetUserFromHttpRequest(Request); + ApiUserData? userData = ApiUserData.GetFromDatabaseUser(user); + userData?.AddInfoForChannel(player.Details.Author.Id); + + return new ApiResponse(player, userData); + } + catch (Exception e) + { + return Error(e.Message, 500, HttpStatusCode.InternalServerError); + } + } + + [Route("video")] + [ApiDisableable] + public async Task> GetVideoInfo( + string? id, + bool contentCheckOk = true, + string? playlistId = null, + int? playlistIndex = null, + string? playlistParams = null, + string? continuation = null) + { + if (id is null) + return Error("Missing video ID (query parameter `id`)", 400, + HttpStatusCode.BadRequest); + + if (!videoIdRegex.IsMatch(id) || id.Length != 11) + return Error($"Invalid video ID: {id}", 400, HttpStatusCode.BadRequest); + + try + { + if (continuation == null) + { + InnerTubeVideo video = await innerTube.GetVideoDetailsAsync(id, contentCheckOk, playlistId, + playlistIndex, playlistParams, HttpContext.GetInnerTubeLanguage(), + HttpContext.GetInnerTubeRegion()); + + DatabaseUser? user = await DatabaseManager.Oauth2.GetUserFromHttpRequest(Request); + ApiUserData? userData = ApiUserData.GetFromDatabaseUser(user); + userData?.AddInfoForChannel(video.Channel.Id); + userData?.CalculateWithRenderers(video.Recommended); + + return new ApiResponse(video, userData); + } + else + { + return Error("empty ctors not implemented yet", 501, HttpStatusCode.NotImplemented); + ContinuationResponse cont = await innerTube.ContinueVideoRecommendationsAsync(id, + HttpContext.GetInnerTubeLanguage(), HttpContext.GetInnerTubeRegion()); + + DatabaseUser? user = await DatabaseManager.Oauth2.GetUserFromHttpRequest(Request); + ApiUserData? userData = ApiUserData.GetFromDatabaseUser(user); + userData?.CalculateWithRenderers(cont.Results); + + //return new ApiResponse(video, userData); + } + } + catch (Exception e) + { + return Error(e.Message, 500, HttpStatusCode.InternalServerError); + } + } + + [Route("search")] + [ApiDisableable] + public async Task> Search(string query, string? continuation = null) + { + if (string.IsNullOrWhiteSpace(query) && string.IsNullOrWhiteSpace(continuation)) + { + return Error( + "Missing query (query parameter `query`) or continuation key (query parameter `continuation`)", + 400, HttpStatusCode.BadRequest); + } + + + ApiSearchResults result; + if (continuation is null) + { + SearchParams searchParams = Request.GetSearchParams(); + InnerTubeSearchResults results = await innerTube.SearchAsync(query, searchParams, + HttpContext.GetInnerTubeLanguage(), HttpContext.GetInnerTubeRegion()); + result = new ApiSearchResults(results, searchParams); + } + else + { + ContinuationResponse results = await innerTube.ContinueSearchAsync(continuation, + HttpContext.GetInnerTubeLanguage(), HttpContext.GetInnerTubeRegion()); + result = new ApiSearchResults(results); + } + + DatabaseUser? user = await DatabaseManager.Oauth2.GetUserFromHttpRequest(Request); + ApiUserData? userData = ApiUserData.GetFromDatabaseUser(user); + userData?.CalculateWithRenderers(result.Results); + + return new ApiResponse(result, userData); + } + + /* TODO: search suggestions + [Route("searchSuggestions")] + [ApiDisableable] + public async Task> SearchSuggestions(string query) + { + if (string.IsNullOrWhiteSpace(query)) + return Error("Missing query (query parameter `query`)", 400, + HttpStatusCode.BadRequest); + try + { + DatabaseUser? user = await DatabaseManager.Oauth2.GetUserFromHttpRequest(Request); + ApiUserData? userData = ApiUserData.GetFromDatabaseUser(user); + return new ApiResponse(await _youtube.GetSearchAutocompleteAsync(query, + HttpContext.GetInnerTubeLanguage(), + HttpContext.GetInnerTubeRegion()), userData); + } + catch (Exception e) + { + return Error(e.Message, 500, HttpStatusCode.InternalServerError); + } + } + */ + + [Route("playlist")] + [ApiDisableable] + public async Task> Playlist(string id, PlaylistFilter filter = PlaylistFilter.All, + string? continuation = null) + { + if (id.StartsWith("LT-PL")) + { + if (id.Length != 24) + return Error($"Invalid playlist ID: {id}", 400, HttpStatusCode.BadRequest); + } + + if (string.IsNullOrWhiteSpace(id) && continuation is null) + return Error($"Invalid ID: {id}", 400, HttpStatusCode.BadRequest); + + try + { + DatabaseUser? user = await DatabaseManager.Oauth2.GetUserFromHttpRequest(Request); + ApiPlaylist result; + if (id.StartsWith("LT-PL")) + { + DatabasePlaylist? playlist = DatabaseManager.Playlists.GetPlaylist(id); + + if (playlist is null) + return Error("The playlist does not exist.", 404, + HttpStatusCode.InternalServerError); + + if (playlist.Visibility == PlaylistVisibility.Private) + { + if (playlist.Author != user?.UserID) + return Error("The playlist does not exist.", 404, + HttpStatusCode.InternalServerError); + } + + result = new ApiPlaylist(playlist, LocalizationManager.GetFromHttpContext(HttpContext)); + } + else if (continuation is null) + { + InnerTubePlaylist playlist = + await innerTube.GetPlaylistAsync(id, true, filter, HttpContext.GetInnerTubeLanguage(), + HttpContext.GetInnerTubeRegion()); + result = new ApiPlaylist(playlist); + } + else + { + ContinuationResponse playlist = await innerTube.ContinuePlaylistAsync(continuation, + HttpContext.GetInnerTubeLanguage(), HttpContext.GetInnerTubeRegion()); + result = new ApiPlaylist(playlist); + } + + ApiUserData? userData = ApiUserData.GetFromDatabaseUser(user); + userData?.AddInfoForChannel(result.Sidebar?.Channel?.Id); + userData?.CalculateWithRenderers(result.Contents); + return new ApiResponse(result, userData); + } + catch (Exception e) + { + return Error(e.Message, 500, HttpStatusCode.InternalServerError); + } + } + + [Route("channel")] + [ApiDisableable] + public async Task> Channel(string id, ChannelTabs tab = ChannelTabs.Featured, + string? continuation = null) + { + if (string.IsNullOrWhiteSpace(id) && string.IsNullOrWhiteSpace(continuation)) + return Error($"Invalid request: missing both `id` and `continuation`", 400, + HttpStatusCode.BadRequest); + + try + { + ApiChannel response; + if (id?.StartsWith("LT") ?? false) + { + DatabaseUser? localUser = await DatabaseManager.Users.GetUserFromLTId(id); + if (localUser is null) + return Error("This user does not exist", 404, HttpStatusCode.BadRequest); + response = new ApiChannel(localUser); + } + else if (continuation is null && id != null) + { + if (!id.StartsWith("UC")) + return Error("resolveUrl not implemented yet", 501, HttpStatusCode.NotImplemented); + //id = await innerTube.ResolveUrl(id) ?? id; + + InnerTubeChannel channel = await innerTube.GetChannelAsync(id, tab, + HttpContext.GetInnerTubeLanguage(), + HttpContext.GetInnerTubeRegion()); + response = new ApiChannel(channel); + } + else + { + ContinuationResponse channel = await innerTube.ContinueChannelAsync(continuation, + HttpContext.GetInnerTubeLanguage(), + HttpContext.GetInnerTubeRegion()); + response = new ApiChannel(channel); + } + + DatabaseUser? user = await DatabaseManager.Oauth2.GetUserFromHttpRequest(Request); + ApiUserData? userData = ApiUserData.GetFromDatabaseUser(user); + userData?.AddInfoForChannel(response.Metadata?.ExternalId); + userData?.CalculateWithRenderers(response.Contents); + + return new ApiResponse(response, userData); + } + catch (Exception e) + { + return Error(e.Message, 500, HttpStatusCode.InternalServerError); + } + } + + [Route("comments")] + [ApiDisableable] + public async Task> Comments(string? continuation, string? id, + CommentsContext.Types.SortOrder sort = CommentsContext.Types.SortOrder.TopComments) + { + try + { + if (id != null && continuation == null) + continuation = InnerTube.Utils.PackCommentsContinuation(id, sort); + else if (id == null && continuation == null) + return Error( + "Invalid request, either 'continuation' or 'id' must be present", 400, + HttpStatusCode.BadRequest); + + ContinuationResponse? comments = await innerTube.ContinueVideoCommentsAsync(continuation!); + + DatabaseUser? user = await DatabaseManager.Oauth2.GetUserFromHttpRequest(Request); + ApiUserData? userData = ApiUserData.GetFromDatabaseUser(user); + userData?.CalculateWithRenderers(comments.Results); + + return new ApiResponse(comments, userData); + } + catch (Exception e) + { + return Error(e.Message, 500, HttpStatusCode.InternalServerError); + } + } + + [Route("locals")] + [ApiDisableable] + public async Task> Locals() => new(Utils.GetLocals(), + ApiUserData.GetFromDatabaseUser(await DatabaseManager.Oauth2.GetUserFromHttpRequest(Request))); + [GeneratedRegex("[a-zA-Z0-9_-]{11}")] + private static partial Regex VideoIdRegex(); } \ No newline at end of file From 65bd24c1eba97eef0e15d1beea6132c34f7f4ea6 Mon Sep 17 00:00:00 2001 From: Kayra Uylar Date: Sun, 19 May 2024 16:46:22 +0300 Subject: [PATCH 005/114] fix this error i caused --- LightTube/Chores/DatabaseCleanupChore.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LightTube/Chores/DatabaseCleanupChore.cs b/LightTube/Chores/DatabaseCleanupChore.cs index b5f8ab3c..9fbbeb07 100644 --- a/LightTube/Chores/DatabaseCleanupChore.cs +++ b/LightTube/Chores/DatabaseCleanupChore.cs @@ -27,7 +27,7 @@ public async Task RunChore(Action updateStatus, Guid id) updateStatus("Duplicate UserID: " + user.UserID); else users.Add(user.UserID); - foreach (string channel in user.Subscriptions?.Keys.ToArray() ?? user.SubscribedChannels) + foreach (string channel in user.Subscriptions?.Keys.ToArray() ?? []) if (!channels.Contains(channel)) channels.Add(channel); } From aae42eda0285c0ea646ae91dcca15426f1d1b0b8 Mon Sep 17 00:00:00 2001 From: Kayra Uylar Date: Sun, 19 May 2024 17:14:49 +0300 Subject: [PATCH 006/114] Fix the errors in the contexts --- .../Contexts/AppearanceSettingsContext.cs | 5 +- LightTube/Contexts/ChannelContext.cs | 98 ++++++++++--------- LightTube/Contexts/EmbedContext.cs | 41 ++------ LightTube/Contexts/PlayerContext.cs | 33 ++++--- LightTube/Contexts/PlaylistContext.cs | 47 +++++---- LightTube/Contexts/PlaylistVideoContext.cs | 4 +- LightTube/Contexts/SearchContext.cs | 10 +- LightTube/Contexts/SubscriptionContext.cs | 6 +- LightTube/Contexts/WatchContext.cs | 46 +++++---- LightTube/Controllers/FeedController.cs | 2 +- LightTube/Database/Models/DatabasePlaylist.cs | 5 + LightTube/Views/Youtube/Embed.cshtml | 22 +++-- 12 files changed, 153 insertions(+), 166 deletions(-) diff --git a/LightTube/Contexts/AppearanceSettingsContext.cs b/LightTube/Contexts/AppearanceSettingsContext.cs index 9b15d144..616edfdc 100644 --- a/LightTube/Contexts/AppearanceSettingsContext.cs +++ b/LightTube/Contexts/AppearanceSettingsContext.cs @@ -1,16 +1,17 @@ using InnerTube; +using LightTube.ApiModels; using LightTube.Localization; namespace LightTube.Contexts; public class AppearanceSettingsContext( HttpContext context, - InnerTubeLocals locals, + ApiLocals locals, Dictionary customThemes, Language[] languages) : BaseContext(context) { public Language[] Languages = languages; - public InnerTubeLocals Locals = locals; + public ApiLocals Locals = locals; public Dictionary CustomThemes = customThemes; public Dictionary BuiltinThemes = new() { diff --git a/LightTube/Contexts/ChannelContext.cs b/LightTube/Contexts/ChannelContext.cs index dab5f3f3..f88f980d 100644 --- a/LightTube/Contexts/ChannelContext.cs +++ b/LightTube/Contexts/ChannelContext.cs @@ -1,6 +1,10 @@ -using InnerTube; +using System.Collections.ObjectModel; +using InnerTube; +using InnerTube.Models; +using InnerTube.Protobuf; using InnerTube.Renderers; using LightTube.Database.Models; +using Endpoint = InnerTube.Protobuf.Endpoint; namespace LightTube.Contexts; @@ -14,97 +18,97 @@ public class ChannelContext : BaseContext public bool Editable; public ChannelTabs CurrentTab; - [Obsolete] - public InnerTubeChannelResponse? Channel; - public IEnumerable Content; + public IEnumerable Content; public string Id; public string? Continuation; - public ChannelTabs[] Tabs; + public ReadOnlyCollection Tabs; - public ChannelContext(HttpContext context, ChannelTabs tab, InnerTubeChannelResponse channel, string id) : base(context) + public ChannelContext(HttpContext context, ChannelTabs tab, InnerTubeChannel channel, string id) : base(context) { Id = id; CurrentTab = tab; - BannerUrl = channel.Header?.Banner.LastOrDefault()?.Url.ToString(); - AvatarUrl = channel.Header?.Avatars.LastOrDefault()?.Url.ToString() ?? ""; + BannerUrl = channel.Header?.Banner.LastOrDefault()?.Url; + AvatarUrl = channel.Header?.Avatars.LastOrDefault()?.Url ?? ""; ChannelTitle = channel.Header?.Title ?? ""; SubscriberCountText = channel.Header?.SubscriberCountText ?? ""; LightTubeAccount = false; Editable = false; Content = channel.Contents; Continuation = - (channel.Contents.FirstOrDefault(x => x is ContinuationItemRenderer) as ContinuationItemRenderer)?.Token; - Tabs = channel.EnabledTabs; + (channel.Contents.FirstOrDefault(x => x.Type == "continuation")?.Data as ContinuationRendererData) + ?.ContinuationToken; + Tabs = channel.Tabs; AddMeta("description", channel.Metadata.Description); AddMeta("author", channel.Metadata.Title); AddMeta("og:title", channel.Metadata.Title); AddMeta("og:description", channel.Metadata.Description); - AddMeta("og:url", $"{context.Request.Scheme}://{context.Request.Host}/{context.Request.Path}{context.Request.QueryString}"); - AddMeta("og:image", channel.Header?.Avatars.Last().Url.ToString() ?? ""); - AddMeta("twitter:card", channel.Header?.Avatars.Last().Url.ToString() ?? ""); - AddRSSUrl(context.Request.Scheme + "://" + context.Request.Host + "/feed/" + Id + "/rss.xml"); + AddMeta("og:url", + $"{context.Request.Scheme}://{context.Request.Host}/{context.Request.Path}{context.Request.QueryString}"); + AddMeta("og:image", channel.Header?.Avatars.Last().Url ?? ""); + AddMeta("twitter:card", channel.Header?.Avatars.Last().Url ?? ""); + AddRSSUrl($"{context.Request.Scheme}://{context.Request.Host}/channel/{Id}.xml"); - if (channel.Contents.Any(x => x is ChannelVideoPlayerRenderer || x is ItemSectionRenderer isr && isr.Contents.Any(y => y is ChannelVideoPlayerRenderer))) + // TODO: most likely broken + if (channel.Contents.Any(x => x.OriginalType == "channelVideoPlayerRenderer")) { - AddStylesheet("/lib/videojs/video-js.min.css"); - AddStylesheet("/lib/videojs-endscreen/videojs-endscreen.css"); - AddStylesheet("/lib/videojs-vtt-thumbnails/videojs-vtt-thumbnails.min.css"); - AddStylesheet("/lib/videojs-hls-quality-selector/videojs-hls-quality-selector.css"); - AddStylesheet("/lib/silvermine-videojs-quality-selector/silvermine-videojs-quality-selector.css"); - AddStylesheet("/css/vjs-skin.css"); - - AddScript("/lib/videojs/video.min.js"); - AddScript("/lib/videojs-hotkeys/videojs.hotkeys.min.js"); - AddScript("/lib/videojs-endscreen/videojs-endscreen.js"); - AddScript("/lib/videojs-vtt-thumbnails/videojs-vtt-thumbnails.min.js"); - AddScript("/lib/videojs-contrib-quality-levels/videojs-contrib-quality-levels.min.js"); - AddScript("/lib/videojs-hls-quality-selector/videojs-hls-quality-selector.min.js"); - AddScript("/lib/silvermine-videojs-quality-selector/silvermine-videojs-quality-selector.min.js"); + AddStylesheet("/lib/ltplayer.css"); + AddScript("/lib/ltplayer.js"); AddScript("/js/player.js"); } } - public ChannelContext(HttpContext context, ChannelTabs tab, InnerTubeChannelResponse channel, InnerTubeContinuationResponse continuation, string id) : base(context) + public ChannelContext(HttpContext context, ChannelTabs tab, InnerTubeChannel channel, + ContinuationResponse continuation, string id) : base(context) { Id = id; CurrentTab = tab; - BannerUrl = channel.Header?.Banner.LastOrDefault()?.Url.ToString(); - AvatarUrl = channel.Header?.Avatars.Last().Url.ToString() ?? ""; + BannerUrl = channel.Header?.Banner.LastOrDefault()?.Url; + AvatarUrl = channel.Header?.Avatars.Last().Url ?? ""; ChannelTitle = channel.Header?.Title ?? ""; SubscriberCountText = channel.Header?.SubscriberCountText ?? ""; LightTubeAccount = false; Editable = false; - Content = continuation.Contents; - Continuation = continuation.Continuation; - Tabs = Enum.GetValues(); + Content = continuation.Results; + Continuation = continuation.ContinuationToken; + Tabs = channel.Tabs; AddMeta("description", channel.Metadata.Description); AddMeta("author", channel.Metadata.Title); AddMeta("og:title", channel.Metadata.Title); AddMeta("og:description", channel.Metadata.Description); - AddMeta("og:url", $"{context.Request.Scheme}://{context.Request.Host}/{context.Request.Path}{context.Request.QueryString}"); - AddMeta("og:image", channel.Header?.Avatars.Last().Url.ToString() ?? ""); - AddMeta("twitter:card", channel.Header?.Avatars.Last().Url.ToString() ?? ""); + AddMeta("og:url", + $"{context.Request.Scheme}://{context.Request.Host}/{context.Request.Path}{context.Request.QueryString}"); + AddMeta("og:image", channel.Header?.Avatars.Last().Url ?? ""); + AddMeta("twitter:card", channel.Header?.Avatars.Last().Url ?? ""); AddRSSUrl(context.Request.Scheme + "://" + context.Request.Host + "/feed/" + Id + "/rss.xml"); } - public ChannelContext(HttpContext context, DatabaseUser? channel, string id) : base(context) + public ChannelContext(HttpContext context, DatabaseUser channel, string id) : base(context) { Id = id; CurrentTab = ChannelTabs.Playlists; BannerUrl = null; AvatarUrl = ""; - ChannelTitle = channel?.UserID ?? ""; + ChannelTitle = channel.UserID; SubscriberCountText = "LightTube account"; LightTubeAccount = true; - Editable = channel?.UserID == User?.UserID; - Tabs = [ - ChannelTabs.Playlists - ]; + Editable = channel.UserID == User?.UserID; + Tabs = new ReadOnlyCollection([ + new ChannelTab(new TabRenderer + { + Endpoint = new Endpoint + { + BrowseEndpoint = new() + { + Params = "" + } + }, + Title = "Playlists", + Selected = true, + }) + ]); - Content = [ - channel?.PlaylistRenderers() - ]; + Content = channel.PlaylistRenderers(Localization); } } \ No newline at end of file diff --git a/LightTube/Contexts/EmbedContext.cs b/LightTube/Contexts/EmbedContext.cs index 6ce77e7e..911bc4d1 100644 --- a/LightTube/Contexts/EmbedContext.cs +++ b/LightTube/Contexts/EmbedContext.cs @@ -1,51 +1,24 @@ using InnerTube; +using InnerTube.Models; namespace LightTube.Contexts; public class EmbedContext : BaseContext { public PlayerContext Player; - public InnerTubeNextResponse Video; + public InnerTubeVideo Video; - public EmbedContext(HttpContext context, InnerTubePlayer innerTubePlayer, InnerTubeNextResponse innerTubeNextResponse, bool compatibility, SponsorBlockSegment[] sponsors) : base(context) + public EmbedContext(HttpContext context, InnerTubePlayer innerTubePlayer, InnerTubeVideo innerTubeNextResponse, + bool compatibility, SponsorBlockSegment[] sponsors) : base(context) { - Player = new PlayerContext(context, innerTubePlayer, innerTubeNextResponse, "embed", compatibility, context.Request.Query["q"], sponsors); + Player = new PlayerContext(context, innerTubePlayer, innerTubeNextResponse, "embed", compatibility, + context.Request.Query["q"], sponsors); Video = innerTubeNextResponse; - - AddMeta("description", Video.Description); - AddMeta("author", Video.Channel.Title); - AddMeta("og:title", Video.Title); - AddMeta("og:description", Video.Description); - AddMeta("og:url", - $"{context.Request.Scheme}://{context.Request.Host}/{context.Request.Path}{context.Request.QueryString}"); - AddMeta("og:image", $"{context.Request.Scheme}://{context.Request.Host}/proxy/thumbnail/{Video.Id}/-1"); - AddMeta("og:video:url", $"https://{context.Request.Host}/proxy/media/{Video.Id}/18.mp4"); - AddMeta("og:video:width", "640"); - AddMeta("og:video:height", "360"); - AddMeta("og:type", "video.other"); - AddMeta("twitter:card", $"{context.Request.Scheme}://{context.Request.Host}/proxy/thumbnail/{Video.Id}/-1"); - AddMeta("twitter:player", $"https://{context.Request.Host}/embed/{Video.Id}"); - AddMeta("twitter:player:stream", $"https://{context.Request.Host}/proxy/media/{Video.Id}/18.mp4"); } - public EmbedContext(HttpContext context, Exception e, InnerTubeNextResponse innerTubeNextResponse) : base(context) + public EmbedContext(HttpContext context, Exception e, InnerTubeVideo innerTubeNextResponse) : base(context) { Player = new PlayerContext(context, e); Video = innerTubeNextResponse; - - AddMeta("description", Video.Description); - AddMeta("author", Video.Channel.Title); - AddMeta("og:title", Video.Title); - AddMeta("og:description", Video.Description); - AddMeta("og:url", - $"{context.Request.Scheme}://{context.Request.Host}/{context.Request.Path}{context.Request.QueryString}"); - AddMeta("og:image", $"{context.Request.Scheme}://{context.Request.Host}/proxy/thumbnail/{Video.Id}/-1"); - AddMeta("og:video:url", $"https://{context.Request.Host}/proxy/media/{Video.Id}/18.mp4"); - AddMeta("og:video:width", "640"); - AddMeta("og:video:height", "360"); - AddMeta("og:type", "video.other"); - AddMeta("twitter:card", $"{context.Request.Scheme}://{context.Request.Host}/proxy/thumbnail/{Video.Id}/-1"); - AddMeta("twitter:player", $"https://{context.Request.Host}/embed/{Video.Id}"); - AddMeta("twitter:player:stream", $"https://{context.Request.Host}/proxy/media/{Video.Id}/18.mp4"); } } \ No newline at end of file diff --git a/LightTube/Contexts/PlayerContext.cs b/LightTube/Contexts/PlayerContext.cs index 75504360..74d5d2ad 100644 --- a/LightTube/Contexts/PlayerContext.cs +++ b/LightTube/Contexts/PlayerContext.cs @@ -1,4 +1,7 @@ using InnerTube; +using InnerTube.Models; +using InnerTube.Protobuf; +using InnerTube.Protobuf.Responses; using InnerTube.Renderers; using Newtonsoft.Json; @@ -7,25 +10,24 @@ namespace LightTube.Contexts; public class PlayerContext : BaseContext { public InnerTubePlayer? Player; - public InnerTubeNextResponse Video; + public InnerTubeVideo Video; public Exception? Exception; public bool UseHls; public bool UseDash; public Thumbnail[] Thumbnails = []; public string? ErrorMessage = null; - public string PreferredItag = "18"; + public int PreferredItag = 18; public bool UseEmbedUi = false; public string? ClassName; public SponsorBlockSegment[] Sponsors; - public PlayerContext(HttpContext context, InnerTubePlayer innerTubePlayer, InnerTubeNextResponse video, - string className, bool compatibility, - string preferredItag, SponsorBlockSegment[] sponsors) : base(context) + public PlayerContext(HttpContext context, InnerTubePlayer innerTubePlayer, InnerTubeVideo video, string className, + bool compatibility, string? preferredItag, SponsorBlockSegment[] sponsors) : base(context) { Player = innerTubePlayer; Video = video; ClassName = className; - PreferredItag = preferredItag; + PreferredItag = int.TryParse(preferredItag ?? "18", out int itag) ? itag : 18; Sponsors = sponsors; UseHls = !compatibility; // Prefer HLS UseDash = innerTubePlayer.AdaptiveFormats.Any() && !compatibility; @@ -40,20 +42,20 @@ public class PlayerContext : BaseContext public string GetChaptersJson() { if (Video.Chapters is null) return "[]"; - ChapterRenderer[] c = Video.Chapters.ToArray(); + VideoChapter[] c = Video.Chapters.ToArray(); List ltChapters = []; for (int i = 0; i < c.Length; i++) { - ChapterRenderer chapter = c[i]; + VideoChapter chapter = c[i]; float to = 100; if (i + 1 < c.Length) { - ChapterRenderer next = c[i + 1]; - to = next.TimeRangeStartMillis / (float)Player!.Details.Length.TotalMilliseconds * 100; + VideoChapter next = c[i + 1]; + to = (next.StartSeconds * 1000) / (float)Player!.Details.Length.TotalMilliseconds * 100; } ltChapters.Add(new LtVideoChapter { - From = chapter.TimeRangeStartMillis / (float)Player!.Details.Length.TotalMilliseconds * 100, + From = (chapter.StartSeconds * 1000) / (float)Player!.Details.Length.TotalMilliseconds * 100, To = to, Name = chapter.Title }); @@ -76,13 +78,14 @@ public PlayerContext(HttpContext context, Exception e) : base(context) Sponsors = []; } - public string? GetFirstItag() => GetPreferredFormat()?.Itag; + public int? GetFirstItag() => GetPreferredFormat()?.Itag; public Format? GetPreferredFormat() => - Player?.Formats.FirstOrDefault(x => x.Itag == PreferredItag && x.Itag != "17") ?? - Player?.Formats.FirstOrDefault(x => x.Itag != "17"); + Player?.Formats.FirstOrDefault(x => x.Itag == PreferredItag && x.Itag != 17) ?? + Player?.Formats.FirstOrDefault(x => x.Itag != 17); public string GetClass() => ClassName is not null ? $" {ClassName}" : ""; - public IEnumerable GetFormatsInPreferredOrder() => Player!.Formats.OrderBy(x => x.Itag != PreferredItag).Where(x => x.Itag != "17"); + public IEnumerable GetFormatsInPreferredOrder() => + Player!.Formats.OrderBy(x => x.Itag != PreferredItag).Where(x => x.Itag != 17); } \ No newline at end of file diff --git a/LightTube/Contexts/PlaylistContext.cs b/LightTube/Contexts/PlaylistContext.cs index dcf7e61e..97ed0d92 100644 --- a/LightTube/Contexts/PlaylistContext.cs +++ b/LightTube/Contexts/PlaylistContext.cs @@ -1,4 +1,4 @@ -using InnerTube; +using InnerTube.Models; using InnerTube.Renderers; using LightTube.Database; using LightTube.Database.Models; @@ -16,22 +16,22 @@ public class PlaylistContext : BaseContext public string ViewCountText; public string LastUpdatedText; public bool Editable; - public IEnumerable Items; - public int? Continuation; + public IEnumerable Items; + public string? Continuation; public PlaylistContext(HttpContext context, InnerTubePlaylist playlist) : base(context) { Id = playlist.Id; - PlaylistThumbnail = playlist.Sidebar.Thumbnails.Last().Url.ToString(); + PlaylistThumbnail = playlist.Sidebar.Thumbnails.Last().Url; PlaylistTitle = playlist.Sidebar.Title; PlaylistDescription = playlist.Sidebar.Description; - AuthorName = playlist.Sidebar.Channel.Title; - AuthorId = playlist.Sidebar.Channel.Id!; + AuthorName = playlist.Sidebar.Channel?.Title ?? "????"; + AuthorId = playlist.Sidebar.Channel?.Id ?? "UC"; ViewCountText = playlist.Sidebar.ViewCountText; LastUpdatedText = playlist.Sidebar.LastUpdated; Editable = false; - Items = playlist.Videos; - Continuation = playlist.Continuation?.ContinueFrom; + Items = playlist.Contents; + Continuation = playlist.Continuation; AddMeta("description", playlist.Sidebar.Description); AddMeta("author", playlist.Sidebar.Title); @@ -40,27 +40,25 @@ public PlaylistContext(HttpContext context, InnerTubePlaylist playlist) : base(c AddMeta("og:url", $"{context.Request.Scheme}://{context.Request.Host}/{context.Request.Path}{context.Request.QueryString}"); AddMeta("og:image", - $"{context.Request.Scheme}://{context.Request.Host}/proxy/thumbnail/{playlist.Videos.First().Id}/-1"); + $"{context.Request.Scheme}://{context.Request.Host}/proxy/thumbnail/{(playlist.Contents.First(x => x.Type == "video").Data as VideoRendererData)?.VideoId}/-1"); AddMeta("twitter:card", - $"{context.Request.Scheme}://{context.Request.Host}/proxy/thumbnail/{playlist.Videos.First().Id}/-1"); + $"{context.Request.Scheme}://{context.Request.Host}/proxy/thumbnail/{(playlist.Contents.First(x => x.Type == "video").Data as VideoRendererData)?.VideoId}/-1"); } - public PlaylistContext(HttpContext context, InnerTubePlaylist playlist, InnerTubeContinuationResponse continuation) + public PlaylistContext(HttpContext context, InnerTubePlaylist playlist, ContinuationResponse continuation) : base(context) { Id = playlist.Id; - PlaylistThumbnail = playlist.Sidebar.Thumbnails.Last().Url.ToString(); + PlaylistThumbnail = playlist.Sidebar.Thumbnails.Last().Url; PlaylistTitle = playlist.Sidebar.Title; PlaylistDescription = playlist.Sidebar.Description; - AuthorName = playlist.Sidebar.Channel.Title; - AuthorId = playlist.Sidebar.Channel.Id!; + AuthorName = playlist.Sidebar.Channel?.Title ?? "????"; + AuthorId = playlist.Sidebar.Channel?.Id ?? "UC"; ViewCountText = playlist.Sidebar.ViewCountText; LastUpdatedText = playlist.Sidebar.LastUpdated; Editable = false; - Items = continuation.Contents; - Continuation = continuation.Continuation is not null - ? InnerTube.Utils.UnpackPlaylistContinuation(continuation.Continuation).ContinueFrom - : null; + Items = continuation.Results; + Continuation = continuation.ContinuationToken; AddMeta("description", playlist.Sidebar.Description); AddMeta("author", playlist.Sidebar.Title); @@ -69,16 +67,14 @@ public PlaylistContext(HttpContext context, InnerTubePlaylist playlist, InnerTub AddMeta("og:url", $"{context.Request.Scheme}://{context.Request.Host}/{context.Request.Path}{context.Request.QueryString}"); AddMeta("og:image", - $"{context.Request.Scheme}://{context.Request.Host}/proxy/thumbnail/{playlist.Videos.First().Id}/-1"); + $"{context.Request.Scheme}://{context.Request.Host}/proxy/thumbnail/{(playlist.Contents.First(x => x.Type == "video").Data as VideoRendererData)?.VideoId}/-1"); AddMeta("twitter:card", - $"{context.Request.Scheme}://{context.Request.Host}/proxy/thumbnail/{playlist.Videos.First().Id}/-1"); + $"{context.Request.Scheme}://{context.Request.Host}/proxy/thumbnail/{(playlist.Contents.First(x => x.Type == "video").Data as VideoRendererData)?.VideoId}/-1"); } public PlaylistContext(HttpContext context, DatabasePlaylist? playlist) : base(context) { - bool visible = (playlist?.Visibility == PlaylistVisibility.Private) - ? User != null && User.UserID == playlist.Author - : true; + bool visible = playlist?.Visibility != PlaylistVisibility.Private || User != null && User.UserID == playlist.Author; if (visible && playlist != null) { @@ -91,11 +87,12 @@ public PlaylistContext(HttpContext context, DatabasePlaylist? playlist) : base(c ViewCountText = Localization.GetRawString("playlist.lighttube.views"); LastUpdatedText = string.Format(Localization.GetRawString("playlist.lastupdated"), playlist.LastUpdated.ToString("MMM d, yyyy")); Editable = User != null && User.UserID == playlist.Author; - Items = DatabaseManager.Playlists.GetPlaylistVideos(playlist.Id, Editable); + Items = DatabaseManager.Playlists.GetPlaylistVideos(playlist.Id, Editable, Localization); } else { - PlaylistThumbnail = $"https://i.ytimg.com/vi//hqdefault.jpg"; + Id = ""; + PlaylistThumbnail = "https://i.ytimg.com/vi//hqdefault.jpg"; PlaylistTitle = Localization.GetRawString("playlist.unavailable"); PlaylistDescription = ""; AuthorName = ""; diff --git a/LightTube/Contexts/PlaylistVideoContext.cs b/LightTube/Contexts/PlaylistVideoContext.cs index e0b0f61d..05e0852f 100644 --- a/LightTube/Contexts/PlaylistVideoContext.cs +++ b/LightTube/Contexts/PlaylistVideoContext.cs @@ -1,4 +1,4 @@ -using InnerTube; +using InnerTube.Models; using LightTube.Database.Models; namespace LightTube.Contexts; @@ -15,7 +15,7 @@ public PlaylistVideoContext(HttpContext context) : base(context) { } - public PlaylistVideoContext(HttpContext context, InnerTubeNextResponse video) : base(context) + public PlaylistVideoContext(HttpContext context, InnerTubeVideo video) : base(context) { ItemId = video.Id; ItemTitle = video.Title; diff --git a/LightTube/Contexts/SearchContext.cs b/LightTube/Contexts/SearchContext.cs index 83206969..00e96e19 100644 --- a/LightTube/Contexts/SearchContext.cs +++ b/LightTube/Contexts/SearchContext.cs @@ -1,4 +1,6 @@ using InnerTube; +using InnerTube.Models; +using InnerTube.Protobuf.Params; using InnerTube.Renderers; namespace LightTube.Contexts; @@ -8,7 +10,7 @@ public class SearchContext : BaseContext public string Query; public SearchParams? Filter; public InnerTubeSearchResults? Search; - public IEnumerable Results; + public IEnumerable Results; public string? Continuation; public SearchContext(HttpContext context, string query, SearchParams? filter, InnerTubeSearchResults search) : base(context) @@ -20,12 +22,12 @@ public SearchContext(HttpContext context, string query, SearchParams? filter, In Continuation = Search.Continuation; } - public SearchContext(HttpContext context, string query, SearchParams? filter, InnerTubeContinuationResponse search) : base(context) + public SearchContext(HttpContext context, string query, SearchParams? filter, ContinuationResponse search) : base(context) { Query = query; Filter = filter; Search = null; - Results = search.Contents; - Continuation = search.Continuation; + Results = search.Results; + Continuation = search.ContinuationToken; } } \ No newline at end of file diff --git a/LightTube/Contexts/SubscriptionContext.cs b/LightTube/Contexts/SubscriptionContext.cs index aee9b755..f329afb1 100644 --- a/LightTube/Contexts/SubscriptionContext.cs +++ b/LightTube/Contexts/SubscriptionContext.cs @@ -1,14 +1,14 @@ -using InnerTube; +using InnerTube.Models; using LightTube.Database.Models; namespace LightTube.Contexts; public class SubscriptionContext : ModalContext { - public InnerTubeChannelResponse Channel; + public InnerTubeChannel Channel; public SubscriptionType CurrentType = SubscriptionType.NONE; - public SubscriptionContext(HttpContext context, InnerTubeChannelResponse channel, SubscriptionType? subscriptionType = null) : + public SubscriptionContext(HttpContext context, InnerTubeChannel channel, SubscriptionType? subscriptionType = null) : base(context) { Channel = channel; diff --git a/LightTube/Contexts/WatchContext.cs b/LightTube/Contexts/WatchContext.cs index d7b9bfb0..e2ed1796 100644 --- a/LightTube/Contexts/WatchContext.cs +++ b/LightTube/Contexts/WatchContext.cs @@ -1,4 +1,5 @@ using InnerTube; +using InnerTube.Models; using LightTube.Database.Models; namespace LightTube.Contexts; @@ -6,21 +7,20 @@ namespace LightTube.Contexts; public class WatchContext : BaseContext { public PlayerContext Player; - public InnerTubeNextResponse Video; - public InnerTubePlaylistInfo? Playlist; - public InnerTubeContinuationResponse? Comments; + public InnerTubeVideo Video; + public VideoPlaylistInfo? Playlist; + public ContinuationResponse? Comments; public int Dislikes; public int Likes; public SponsorBlockSegment[] Sponsors; - public WatchContext(HttpContext context, InnerTubePlayer innerTubePlayer, - InnerTubeNextResponse innerTubeNextResponse, - InnerTubeContinuationResponse? comments, - bool compatibility, int dislikes, int likes, SponsorBlockSegment[] sponsors) : base(context) + public WatchContext(HttpContext context, InnerTubePlayer innerTubePlayer, InnerTubeVideo innerTubeVideo, + ContinuationResponse? comments, bool compatibility, int dislikes, int likes, + SponsorBlockSegment[] sponsors) : base(context) { - Player = new PlayerContext(context, innerTubePlayer, innerTubeNextResponse, "embed", compatibility, + Player = new PlayerContext(context, innerTubePlayer, innerTubeVideo, "embed", compatibility, context.Request.Query["q"], sponsors); - Video = innerTubeNextResponse; + Video = innerTubeVideo; Playlist = Video.Playlist; Comments = comments; Dislikes = dislikes; @@ -50,11 +50,11 @@ public class WatchContext : BaseContext AddScript("/js/player.js"); } - public WatchContext(HttpContext context, Exception e, InnerTubeNextResponse innerTubeNextResponse, - InnerTubeContinuationResponse? comments, int dislikes, int likes) : base(context) + public WatchContext(HttpContext context, Exception e, InnerTubeVideo innerTubeVideo, ContinuationResponse? comments, + int dislikes, int likes) : base(context) { Player = new PlayerContext(context, e); - Video = innerTubeNextResponse; + Video = innerTubeVideo; Playlist = Video.Playlist; Comments = comments; Dislikes = dislikes; @@ -78,15 +78,14 @@ public class WatchContext : BaseContext AddMeta("twitter:player:stream", $"https://{context.Request.Host}/proxy/media/{Video.Id}/18.mp4"); } - public WatchContext(HttpContext context, InnerTubePlayer innerTubePlayer, - InnerTubeNextResponse innerTubeNextResponse, DatabasePlaylist? playlist, - InnerTubeContinuationResponse? comments, - bool compatibility, int dislikes, int likes, SponsorBlockSegment[] sponsors) : base(context) + public WatchContext(HttpContext context, InnerTubePlayer innerTubePlayer, InnerTubeVideo innerTubeVideo, + DatabasePlaylist? playlist, ContinuationResponse? comments, bool compatibility, int dislikes, int likes, + SponsorBlockSegment[] sponsors) : base(context) { - Player = new PlayerContext(context, innerTubePlayer, innerTubeNextResponse, "embed", compatibility, + Player = new PlayerContext(context, innerTubePlayer, innerTubeVideo, "embed", compatibility, context.Request.Query["q"], sponsors); - Video = innerTubeNextResponse; - Playlist = playlist?.GetInnerTubePlaylistInfo(innerTubePlayer.Details.Id); + Video = innerTubeVideo; + Playlist = playlist?.GetVideoPlaylistInfo(innerTubePlayer.Details.Id); if (playlist != null && playlist.Visibility == PlaylistVisibility.Private) if (playlist.Author != User?.UserID) Playlist = null; @@ -118,13 +117,12 @@ public class WatchContext : BaseContext AddScript("/js/player.js"); } - public WatchContext(HttpContext context, Exception e, InnerTubeNextResponse innerTubeNextResponse, - DatabasePlaylist? playlist, - InnerTubeContinuationResponse? comments, int dislikes, int likes) : base(context) + public WatchContext(HttpContext context, Exception e, InnerTubeVideo innerTubeVideo, DatabasePlaylist? playlist, + ContinuationResponse? comments, int dislikes, int likes) : base(context) { Player = new PlayerContext(context, e); - Video = innerTubeNextResponse; - Playlist = playlist?.GetInnerTubePlaylistInfo(innerTubeNextResponse.Id); + Video = innerTubeVideo; + Playlist = playlist?.GetVideoPlaylistInfo(innerTubeVideo.Id); if (playlist != null && playlist.Visibility == PlaylistVisibility.Private) if (playlist.Author != User?.UserID) Playlist = null; diff --git a/LightTube/Controllers/FeedController.cs b/LightTube/Controllers/FeedController.cs index 96818860..5e1415e5 100644 --- a/LightTube/Controllers/FeedController.cs +++ b/LightTube/Controllers/FeedController.cs @@ -15,7 +15,7 @@ namespace LightTube.Controllers; public class FeedController(SimpleInnerTubeClient innerTube) : Controller { - [Route("channel/{c}/rss.xml")] + [Route("channel/{c}.xml")] [HttpGet] public async Task ChannelFeed(string c) { diff --git a/LightTube/Database/Models/DatabasePlaylist.cs b/LightTube/Database/Models/DatabasePlaylist.cs index 19cafa7c..9ba9687a 100644 --- a/LightTube/Database/Models/DatabasePlaylist.cs +++ b/LightTube/Database/Models/DatabasePlaylist.cs @@ -23,6 +23,11 @@ public static string GenerateId() playlistId += ID_ALPHABET[rng.Next(0, ID_ALPHABET.Length)]; return playlistId; } + + public VideoPlaylistInfo? GetVideoPlaylistInfo(string detailsId) + { + throw new NotImplementedException("empty ctor not implemented"); + } } public enum PlaylistVisibility diff --git a/LightTube/Views/Youtube/Embed.cshtml b/LightTube/Views/Youtube/Embed.cshtml index 3188259c..ed6e57e0 100644 --- a/LightTube/Views/Youtube/Embed.cshtml +++ b/LightTube/Views/Youtube/Embed.cshtml @@ -10,15 +10,19 @@ - - - - - - - - - + + + + + + + + + + + + + @Model.Player.Player?.Details.Title - LightTube