Skip to content

Commit

Permalink
LTPlayer (again) (#93)
Browse files Browse the repository at this point in the history
* Cache external JavaScript libraries instead of having them inside the repo
* Switch to LTPlayer
* SponsorBlock support
* Fix #76
* Style segments
* Add custom styling to the skip button
* Add timestamp links
* Fix #90
* Add a loading spinner & implement storyboards
  • Loading branch information
kuylar authored Nov 17, 2023
1 parent f9bfbd5 commit 25b73b3
Show file tree
Hide file tree
Showing 127 changed files with 302 additions and 8,717 deletions.
1 change: 1 addition & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,5 @@ RUN dotnet publish "LightTube.csproj" -c Release -o /app/publish /p:Version=`dat
FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
RUN chmod 777 -R /tmp && chmod o+t -R /tmp
CMD ASPNETCORE_URLS=http://*:$PORT dotnet LightTube.dll
4 changes: 2 additions & 2 deletions LightTube/Contexts/EmbedContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@ public class EmbedContext : BaseContext
public PlayerContext Player;
public InnerTubeNextResponse Video;

public EmbedContext(HttpContext context, InnerTubePlayer innerTubePlayer, InnerTubeNextResponse innerTubeNextResponse, bool compatibility) : base(context)
public EmbedContext(HttpContext context, InnerTubePlayer innerTubePlayer, InnerTubeNextResponse innerTubeNextResponse, bool compatibility, SponsorBlockSegment[] sponsors) : base(context)
{
Player = new PlayerContext(context, innerTubePlayer, "embed", compatibility, context.Request.Query["q"]);
Player = new PlayerContext(context, innerTubePlayer, innerTubeNextResponse, "embed", compatibility, context.Request.Query["q"], sponsors);
Video = innerTubeNextResponse;
}

Expand Down
55 changes: 46 additions & 9 deletions LightTube/Contexts/PlayerContext.cs
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
using InnerTube;
using InnerTube.Renderers;
using Newtonsoft.Json;

namespace LightTube.Contexts;

public class PlayerContext : BaseContext
{
public InnerTubePlayer? Player;
public InnerTubeNextResponse Video;
public Exception? Exception;
public bool UseHls;
public bool UseDash;
Expand All @@ -13,16 +16,19 @@ public class PlayerContext : BaseContext
public string PreferredItag = "18";
public bool UseEmbedUi = false;
public string? ClassName;
public SponsorBlockSegment[] Sponsors;

public PlayerContext(HttpContext context, InnerTubePlayer innerTubePlayer, string className, bool compatibility,
string preferredItag) : base(context)
public PlayerContext(HttpContext context, InnerTubePlayer innerTubePlayer, InnerTubeNextResponse video,
string className, bool compatibility,
string preferredItag, SponsorBlockSegment[] sponsors) : base(context)
{
Player = innerTubePlayer;
Video = video;
ClassName = className;
PreferredItag = preferredItag;
UseHls = innerTubePlayer.DashManifestUrl is not null && !compatibility; // Prefer HLS if the video is live
// Live videos contain a DASH manifest URL
UseDash = innerTubePlayer.AdaptiveFormats.Any() && !compatibility; // Prefer DASH if we can provide Adaptive
Sponsors = sponsors;
UseHls = !compatibility; // Prefer HLS
UseDash = innerTubePlayer.AdaptiveFormats.Any() && !compatibility;
// Formats
if (Configuration.GetVariable("LIGHTTUBE_DISABLE_PROXY", "false") != "false")
{
Expand All @@ -31,9 +37,43 @@ public PlayerContext(HttpContext context, InnerTubePlayer innerTubePlayer, strin
}
}

public string GetChaptersJson()
{
if (Video.Chapters is null) return "[]";
ChapterRenderer[] c = Video.Chapters.ToArray();
List<LtVideoChapter> ltChapters = new();
for (int i = 0; i < c.Length; i++)
{
ChapterRenderer 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;
}
ltChapters.Add(new LtVideoChapter
{
From = chapter.TimeRangeStartMillis / (float)Player!.Details.Length.TotalMilliseconds * 100,
To = to,
Name = chapter.Title
});
}

return JsonConvert.SerializeObject(ltChapters);
}

private class LtVideoChapter
{
[JsonProperty("from")] public float From;
[JsonProperty("to")] public float To;
[JsonProperty("name")] public string Name;
}

public PlayerContext(HttpContext context, Exception e) : base(context)
{
Exception = e;
Video = null!;
Sponsors = Array.Empty<SponsorBlockSegment>();
}

public string? GetFirstItag() => GetPreferredFormat()?.Itag;
Expand All @@ -44,8 +84,5 @@ public PlayerContext(HttpContext context, Exception e) : base(context)

public string GetClass() => ClassName is not null ? $" {ClassName}" : "";

public IEnumerable<Format> GetFormatsInPreferredOrder()
{
return Player!.Formats.OrderBy(x => x.Itag != PreferredItag);
}
public IEnumerable<Format> GetFormatsInPreferredOrder() => Player!.Formats.OrderBy(x => x.Itag != PreferredItag).Where(x => x.Itag != "17");
}
46 changes: 15 additions & 31 deletions LightTube/Contexts/WatchContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,19 @@ public class WatchContext : BaseContext
public InnerTubeNextResponse Video;
public InnerTubePlaylistInfo? Playlist;
public InnerTubeContinuationResponse? Comments;

public int Dislikes;
public SponsorBlockSegment[] Sponsors;

public WatchContext(HttpContext context, InnerTubePlayer innerTubePlayer, InnerTubeNextResponse innerTubeNextResponse,
InnerTubeContinuationResponse? comments,
bool compatibility, int dislikes) : base(context)
bool compatibility, int dislikes, SponsorBlockSegment[] sponsors) : base(context)
{
Player = new PlayerContext(context, innerTubePlayer, "embed", compatibility, context.Request.Query["q"]);
Player = new PlayerContext(context, innerTubePlayer, innerTubeNextResponse, "embed", compatibility, context.Request.Query["q"], sponsors);
Video = innerTubeNextResponse;
Playlist = Video.Playlist;
Comments = comments;
Dislikes = dislikes;
Sponsors = sponsors;
GuideHidden = true;

AddMeta("description", Video.Description);
Expand All @@ -33,20 +34,10 @@ public WatchContext(HttpContext context, InnerTubePlayer innerTubePlayer, InnerT
AddMeta("twitter:player", $"https://{context.Request.Host}/embed/${Video.Id}");
AddMeta("twitter:player:stream", $"https://{context.Request.Host}/proxy/media/${Video.Id}/18");

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");
AddStylesheet("/lib/ltplayer.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");
AddScript("/lib/ltplayer.js");
AddScript("/lib/hls.js");
AddScript("/js/player.js");
}

Expand All @@ -58,6 +49,7 @@ public WatchContext(HttpContext context, Exception e, InnerTubeNextResponse inne
Playlist = Video.Playlist;
Comments = comments;
Dislikes = dislikes;
Sponsors = Array.Empty<SponsorBlockSegment>();
GuideHidden = true;

AddMeta("description", Video.Description);
Expand All @@ -73,16 +65,17 @@ public WatchContext(HttpContext context, Exception e, InnerTubeNextResponse inne

public WatchContext(HttpContext context, InnerTubePlayer innerTubePlayer, InnerTubeNextResponse innerTubeNextResponse, DatabasePlaylist? playlist,
InnerTubeContinuationResponse? comments,
bool compatibility, int dislikes) : base(context)
bool compatibility, int dislikes, SponsorBlockSegment[] sponsors) : base(context)
{
Player = new PlayerContext(context, innerTubePlayer, "embed", compatibility, context.Request.Query["q"]);
Player = new PlayerContext(context, innerTubePlayer, innerTubeNextResponse, "embed", compatibility, context.Request.Query["q"], sponsors);
Video = innerTubeNextResponse;
Playlist = playlist?.GetInnerTubePlaylistInfo(innerTubePlayer.Details.Id);
if (playlist != null && playlist.Visibility == PlaylistVisibility.PRIVATE)
if (playlist.Author != User?.UserID)
Playlist = null;
Comments = comments;
Dislikes = dislikes;
Sponsors = sponsors;
GuideHidden = true;

AddMeta("description", Video.Description);
Expand All @@ -95,20 +88,10 @@ public WatchContext(HttpContext context, InnerTubePlayer innerTubePlayer, InnerT
AddMeta("twitter:player", $"https://{context.Request.Host}/embed/${Video.Id}");
AddMeta("twitter:player:stream", $"https://{context.Request.Host}/proxy/media/${Video.Id}/18");

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");
AddStylesheet("/lib/ltplayer.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");
AddScript("/lib/ltplayer.js");
AddScript("/lib/hls.js");
AddScript("/js/player.js");
}

Expand All @@ -123,6 +106,7 @@ public WatchContext(HttpContext context, Exception e, InnerTubeNextResponse inne
Playlist = null;
Comments = comments;
Dislikes = dislikes;
Sponsors = Array.Empty<SponsorBlockSegment>();
GuideHidden = true;

AddMeta("description", Video.Description);
Expand Down
16 changes: 16 additions & 0 deletions LightTube/Controllers/HomeController.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System.Text;
using LightTube.Contexts;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Net.Http.Headers;

namespace LightTube.Controllers;

Expand Down Expand Up @@ -37,4 +38,19 @@ public IActionResult CustomCss()

return NotFound();
}

[Route("/lib/{name}")]
public IActionResult CachedJs(string name)
{
try
{
return File(Encoding.UTF8.GetBytes(JsCache.GetJsFileContents(name)),
name.EndsWith(".css") ? "text/css" : "text/javascript",
JsCache.CacheUpdateTime, new EntityTagHeaderValue($"\"{JsCache.GetHash(name)}\""));
}
catch (Exception e)
{
return NotFound();
}
}
}
11 changes: 6 additions & 5 deletions LightTube/Controllers/MediaController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -305,25 +305,26 @@ public async Task HlsSegmentProxy(string path)
}
}

[Route("caption/{videoId}/{language}")]
public async Task<IActionResult> SubtitleProxy(string videoId, string language)
[Route("caption/{videoId}/{vssId}")]
public async Task<IActionResult> SubtitleProxy(string videoId, string vssId)
{
try
{
InnerTubePlayer player = await _youtube.GetPlayerAsync(videoId);
InnerTubePlayer.VideoCaption?
subtitle = player.Captions.FirstOrDefault(x => x.LanguageCode == language);
subtitle = player.Captions.FirstOrDefault(x => x.VssId == vssId);

if (subtitle is null)
{
Response.StatusCode = (int)HttpStatusCode.NotFound;
return File(
new MemoryStream(Encoding.UTF8.GetBytes(
$"There are no available subtitles for {language}. Available language codes are: {string.Join(", ", player.Captions.Select(x => $"{x.LanguageCode} \"{x.Label}\""))}")),
$"There are no available subtitles for '{vssId}'. Available subtitle IDs are: {string.Join(", ", player.Captions.Select(x => $"{x.VssId} \"{x.Label}\""))}")),
"text/plain");
}

string url = subtitle.BaseUrl.ToString().Replace("fmt=srv3", "fmt=vtt");
string url = subtitle.BaseUrl.ToString();
url = url.Contains("fmt=") ? url.Replace("fmt=srv3", "fmt=vtt") : url + "&fmt=vtt";

if (!url.StartsWith("http://") && !url.StartsWith("https://"))
url = "https://" + url;
Expand Down
26 changes: 23 additions & 3 deletions LightTube/Controllers/YoutubeController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,22 @@ public async Task<IActionResult> Embed(string v, bool contentCheckOk, bool compa
player = null;
e = ex;
}

SponsorBlockSegment[] sponsors;
try
{
sponsors = await SponsorBlockSegment.GetSponsors(v);
}
catch
{
sponsors = Array.Empty<SponsorBlockSegment>();
}

InnerTubeNextResponse video =
await _youtube.GetVideoAsync(v, language: HttpContext.GetLanguage(), region: HttpContext.GetRegion());
if (player is null || e is not null)
return View(new EmbedContext(HttpContext, e ?? new Exception("player is null"), video));
return View(new EmbedContext(HttpContext, player, video, compatibility));
return View(new EmbedContext(HttpContext, player, video, compatibility, sponsors));
}

[Route("/watch")]
Expand Down Expand Up @@ -86,6 +96,16 @@ await _youtube.GetVideoAsync(v, localPlaylist ? null : list, language: HttpConte
catch
{
dislikes = -1;
}

SponsorBlockSegment[] sponsors;
try
{
sponsors = await SponsorBlockSegment.GetSponsors(v);
}
catch
{
sponsors = Array.Empty<SponsorBlockSegment>();
}

if (player is not null)
Expand All @@ -97,14 +117,14 @@ await _youtube.GetVideoAsync(v, localPlaylist ? null : list, language: HttpConte
if (player is null || e is not null)
return View(new WatchContext(HttpContext, e ?? new Exception("player is null"), video, pl, comments,
dislikes));
return View(new WatchContext(HttpContext, player, video, pl, comments, compatibility, dislikes));
return View(new WatchContext(HttpContext, player, video, pl, comments, compatibility, dislikes, sponsors));
}
else
{
if (player is null || e is not null)
return View(new WatchContext(HttpContext, e ?? new Exception("player is null"), video, comments,
dislikes));
return View(new WatchContext(HttpContext, player, video, comments, compatibility, dislikes));
return View(new WatchContext(HttpContext, player, video, comments, compatibility, dislikes, sponsors));
}
}

Expand Down
49 changes: 49 additions & 0 deletions LightTube/JsCache.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
using System.Security.Cryptography;
using System.Text;
using System.Web;

namespace LightTube;

public static class JsCache
{
private static Dictionary<string, Uri> LibraryUrls = new()
{
["hls.js"] = new Uri("https://cdn.jsdelivr.net/npm/hls.js@1.4.0/dist/hls.min.js"),
["ltplayer.js"] = new Uri("https://raw.githubusercontent.com/kuylar/LTPlayer/master/dist/player.min.js"),
["ltplayer.css"] = new Uri("https://raw.githubusercontent.com/kuylar/LTPlayer/master/dist/player.min.css"),
};
private static Dictionary<string, string> Hashes = new();
public static DateTimeOffset CacheUpdateTime = DateTimeOffset.MinValue;

public static async Task DownloadLibraries()
{
HttpClient client = new();
Directory.CreateDirectory("/tmp/lighttube/jsCache");
Console.WriteLine("[JsCache] Downloading libraries...");
foreach ((string? name, Uri? url) in LibraryUrls)
{
Console.WriteLine($"[JsCache] Downloading '{name}' from {url}");

HttpResponseMessage response = await client.GetAsync(url);
string jsData = await response.Content.ReadAsStringAsync();
await File.WriteAllTextAsync($"/tmp/lighttube/jsCache/{name}", jsData);
Console.WriteLine($"[JsCache] Calculating the MD5 hash of {name}...");

using MD5 md5 = MD5.Create();
byte[] inputBytes = Encoding.ASCII.GetBytes(jsData);
byte[] hashBytes = md5.ComputeHash(inputBytes);
string hash = Convert.ToHexString(hashBytes);

Hashes[name] = hash;

Console.WriteLine($"[JsCache] Downloaded '{name}'.");
}
CacheUpdateTime = DateTimeOffset.UtcNow;
}

public static string GetJsFileContents(string name) => File.ReadAllText($"/tmp/lighttube/jsCache/{HttpUtility.UrlEncode(name)}");
public static Uri GetUrl(string name) => LibraryUrls.TryGetValue(name, out Uri? url) ? url : new Uri("/");

public static string GetHash(string name) =>
Hashes.TryGetValue(name, out string? h) ? h : "68b329da9893e34099c7d8ad5cb9c940"; // md5 sum of an empty buffer
}
1 change: 1 addition & 0 deletions LightTube/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
}));
builder.Services.AddSingleton(new HttpClient());

await JsCache.DownloadLibraries();
ChoreManager.RegisterChores();
DatabaseManager.Init(Configuration.GetVariable("LIGHTTUBE_MONGODB_CONNSTR"));

Expand Down
Loading

0 comments on commit 25b73b3

Please sign in to comment.