mirror of
https://codeberg.org/ashley/poke
synced 2026-03-03 20:03:44 +00:00
owo
This commit is contained in:
80
core/LightTube/Views/Youtube/Channel.cshtml
Normal file
80
core/LightTube/Views/Youtube/Channel.cshtml
Normal file
@@ -0,0 +1,80 @@
|
||||
@using InnerTube.Models
|
||||
@using System.Web
|
||||
@model LightTube.Contexts.ChannelContext
|
||||
|
||||
@{
|
||||
ViewBag.Metadata = new Dictionary<string, string>();
|
||||
ViewBag.Metadata["og:title"] = Model.Channel.Name;
|
||||
ViewBag.Metadata["og:url"] = $"{Url.ActionContext.HttpContext.Request.Scheme}://{Url.ActionContext.HttpContext.Request.Host}{Url.ActionContext.HttpContext.Request.Path}{Url.ActionContext.HttpContext.Request.QueryString}";
|
||||
ViewBag.Metadata["og:image"] = $"{Url.ActionContext.HttpContext.Request.Scheme}://{Url.ActionContext.HttpContext.Request.Host}/proxy/image?url={HttpUtility.UrlEncode(Model.Channel.Avatars.FirstOrDefault()?.Url?.ToString())}";
|
||||
ViewBag.Metadata["twitter:card"] = $"{Url.ActionContext.HttpContext.Request.Scheme}://{Url.ActionContext.HttpContext.Request.Host}/proxy/image?url={HttpUtility.UrlEncode(Model.Channel.Avatars.LastOrDefault()?.Url?.ToString())}";
|
||||
ViewBag.Metadata["og:description"] = Model.Channel.Description;
|
||||
ViewBag.Title = Model.Channel.Name;
|
||||
Layout = "_Layout";
|
||||
|
||||
DynamicItem[] contents;
|
||||
try
|
||||
{
|
||||
contents = ((ItemSectionItem)((ItemSectionItem)Model.Channel.Videos[0]).Contents[0]).Contents;
|
||||
}
|
||||
catch
|
||||
{
|
||||
contents = Model.Channel.Videos;
|
||||
}
|
||||
}
|
||||
|
||||
<div class="channel-page">
|
||||
@if (Model.Channel.Banners.Length > 0)
|
||||
{
|
||||
<img class="channel-banner" alt="Channel Banner" src="@Model.Channel.Banners.Last().Url">
|
||||
}
|
||||
|
||||
<div class="channel-info-container">
|
||||
<div class="channel-info">
|
||||
<a href="/channel/@Model.Channel.Id" class="avatar">
|
||||
<img src="@Model.Channel.Avatars.LastOrDefault()?.Url" alt="Channel Avatar">
|
||||
</a>
|
||||
<div class="name">
|
||||
<a>@Model.Channel.Name</a>
|
||||
<span>@Model.Channel.Subscribers</span>
|
||||
</div>
|
||||
<button class="subscribe-button" data-cid="@Model.Channel.Id">Subscribe</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3>About</h3>
|
||||
<p>@Html.Raw(Model.Channel.GetHtmlDescription())</p>
|
||||
<br><br>
|
||||
<h3>Uploads</h3>
|
||||
<div class="video-grid">
|
||||
@foreach (VideoItem video in contents.Where(x => x is VideoItem).Cast<VideoItem>())
|
||||
{
|
||||
<a href="/watch?v=@video.Id" class="video">
|
||||
<div class="thumbnail" style="background-image: url('@video.Thumbnails.LastOrDefault()?.Url')"><span class="video-length">@video.Duration</span></div>
|
||||
<div class="info">
|
||||
<span class="title max-lines-2">@video.Title</span>
|
||||
<div>
|
||||
<div>
|
||||
<span>@video.Views views</span>
|
||||
<span>@video.UploadedAt</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="pagination-buttons">
|
||||
@if (!string.IsNullOrWhiteSpace(Model.ContinuationToken))
|
||||
{
|
||||
<a href="/channel?id=@Model.Id">First Page</a>
|
||||
}
|
||||
<div class="divider"></div>
|
||||
<span>•</span>
|
||||
<div class="divider"></div>
|
||||
@if (!string.IsNullOrWhiteSpace(contents.FirstOrDefault(x => x is ContinuationItem)?.Id))
|
||||
{
|
||||
<a href="/channel/@Model.Id?continuation=@(contents.FirstOrDefault(x => x is ContinuationItem)?.Id)">Next Page</a>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
95
core/LightTube/Views/Youtube/Download.cshtml
Normal file
95
core/LightTube/Views/Youtube/Download.cshtml
Normal file
@@ -0,0 +1,95 @@
|
||||
@using System.Web
|
||||
@using InnerTube
|
||||
@using InnerTube.Models
|
||||
@model LightTube.Contexts.PlayerContext
|
||||
|
||||
@{
|
||||
ViewBag.Metadata = new Dictionary<string, string>();
|
||||
ViewBag.Metadata["author"] = Model.Video.Channel.Name;
|
||||
ViewBag.Metadata["og:title"] = Model.Player.Title;
|
||||
ViewBag.Metadata["og:url"] = $"{Url.ActionContext.HttpContext.Request.Scheme}://{Url.ActionContext.HttpContext.Request.Host}{Url.ActionContext.HttpContext.Request.Path}{Url.ActionContext.HttpContext.Request.QueryString}";
|
||||
ViewBag.Metadata["og:image"] = $"{Url.ActionContext.HttpContext.Request.Scheme}://{Url.ActionContext.HttpContext.Request.Host}/proxy/image?url={HttpUtility.UrlEncode(Model.Player.Thumbnails.FirstOrDefault()?.Url?.ToString())}";
|
||||
ViewBag.Metadata["twitter:card"] = $"{Url.ActionContext.HttpContext.Request.Scheme}://{Url.ActionContext.HttpContext.Request.Host}/proxy/image?url={HttpUtility.UrlEncode(Model.Player.Thumbnails.LastOrDefault()?.Url?.ToString())}";
|
||||
ViewBag.Title = Model.Player.Title;
|
||||
Layout = "_Layout";
|
||||
}
|
||||
|
||||
<div class="playlist-page">
|
||||
<div class="playlist-info">
|
||||
<div class="thumbnail" style="background-image: url('@Model.Player.Thumbnails.Last().Url')">
|
||||
<a href="/watch?v=@Model.Player.Id">Watch</a>
|
||||
</div>
|
||||
<p class="title">@Model.Player.Title</p>
|
||||
<span class="info">@Model.Video.Views • @Model.Video.UploadDate</span>
|
||||
<div class="channel-info">
|
||||
<a href="/channel/@Model.Player.Channel.Id" class="avatar">
|
||||
<img src="@Model.Player.Channel.Avatars.LastOrDefault()?.Url">
|
||||
</a>
|
||||
<div class="name">
|
||||
<a class="name" href="/channel/@Model.Player.Channel.Id">@Model.Player.Channel.Name</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="video-list download-list playlist-video-list">
|
||||
<div class="format-list">
|
||||
<h2>Muxed formats</h2>
|
||||
<p>These downloads have both video and audio in them</p>
|
||||
@foreach (Format format in Model.Player.Formats)
|
||||
{
|
||||
<div class="download-format">
|
||||
<div>
|
||||
@format.FormatNote
|
||||
</div>
|
||||
<a href="/proxy/download/@Model.Video.Id/@format.FormatId/@(HttpUtility.UrlEncode(Model.Video.Title)).@format.GetExtension()">
|
||||
<i class="bi bi-download"></i>
|
||||
Download through LightTube
|
||||
</a>
|
||||
<a href="@format.Url">
|
||||
<i class="bi bi-cloud-download"></i>
|
||||
Download through YouTube
|
||||
</a>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<div class="format-list">
|
||||
<h2>Audio only formats</h2>
|
||||
<p>These downloads have only have audio in them</p>
|
||||
@foreach (Format format in Model.Player.AdaptiveFormats.Where(x => x.VideoCodec == "none"))
|
||||
{
|
||||
<div class="download-format">
|
||||
<div>
|
||||
@format.FormatNote (Codec: @format.AudioCodec, Sample Rate: @format.AudioSampleRate)
|
||||
</div>
|
||||
<a href="/proxy/download/@Model.Video.Id/@format.FormatId/@(HttpUtility.UrlEncode(Model.Video.Title)).@format.GetExtension()">
|
||||
<i class="bi bi-download"></i>
|
||||
Download through LightTube
|
||||
</a>
|
||||
<a href="@format.Url">
|
||||
<i class="bi bi-cloud-download"></i>
|
||||
Download through YouTube
|
||||
</a>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<div class="format-list">
|
||||
<h2>Video only formats</h2>
|
||||
<p>These downloads have only have video in them</p>
|
||||
@foreach (Format format in Model.Player.AdaptiveFormats.Where(x => x.AudioCodec == "none"))
|
||||
{
|
||||
<div class="download-format">
|
||||
<div>
|
||||
@format.FormatNote (Codec: @format.VideoCodec)
|
||||
</div>
|
||||
<a href="/proxy/download/@Model.Video.Id/@format.FormatId/@(HttpUtility.UrlEncode(Model.Video.Title)).@format.GetExtension()">
|
||||
<i class="bi bi-download"></i>
|
||||
Download through LightTube
|
||||
</a>
|
||||
<a href="@format.Url">
|
||||
<i class="bi bi-cloud-download"></i>
|
||||
Download through YouTube
|
||||
</a>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
146
core/LightTube/Views/Youtube/Embed.cshtml
Normal file
146
core/LightTube/Views/Youtube/Embed.cshtml
Normal file
@@ -0,0 +1,146 @@
|
||||
@using System.Collections.Specialized
|
||||
@using System.Web
|
||||
@using InnerTube.Models
|
||||
@model LightTube.Contexts.PlayerContext
|
||||
|
||||
@{
|
||||
ViewBag.Metadata = new Dictionary<string, string>();
|
||||
ViewBag.Metadata["author"] = Model.Video.Channel.Name;
|
||||
ViewBag.Metadata["og:title"] = Model.Player.Title;
|
||||
ViewBag.Metadata["og:url"] = $"{Url.ActionContext.HttpContext.Request.Scheme}://{Url.ActionContext.HttpContext.Request.Host}{Url.ActionContext.HttpContext.Request.Path}{Url.ActionContext.HttpContext.Request.QueryString}";
|
||||
ViewBag.Metadata["og:image"] = $"{Url.ActionContext.HttpContext.Request.Scheme}://{Url.ActionContext.HttpContext.Request.Host}/proxy/image?url={HttpUtility.UrlEncode(Model.Player.Thumbnails.FirstOrDefault()?.Url?.ToString())}";
|
||||
ViewBag.Metadata["twitter:card"] = $"{Url.ActionContext.HttpContext.Request.Scheme}://{Url.ActionContext.HttpContext.Request.Host}/proxy/image?url={HttpUtility.UrlEncode(Model.Player.Thumbnails.LastOrDefault()?.Url?.ToString())}";
|
||||
ViewBag.Metadata["og:description"] = Model.Player.Description;
|
||||
ViewBag.Title = Model.Player.Title;
|
||||
|
||||
Layout = null;
|
||||
try
|
||||
{
|
||||
ViewBag.Metadata["og:video"] = $"/proxy/video?url={HttpUtility.UrlEncode(Model.Player.Formats.First().Url.ToString())}";
|
||||
Model.Resolution ??= Model.Player.Formats.First().FormatNote;
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
bool live = Model.Player.Formats.Length == 0 && Model.Player.AdaptiveFormats.Length > 0;
|
||||
bool canPlay = true;
|
||||
}
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||
<meta property="og:site_name" content="lighttube"/>
|
||||
<meta property="og:type" content="website"/>
|
||||
@if (ViewBag.Metadata is not null)
|
||||
{
|
||||
@foreach (KeyValuePair<string, string> metaTag in ViewBag.Metadata)
|
||||
{
|
||||
if (metaTag.Key.StartsWith("og:"))
|
||||
{
|
||||
<meta property="@metaTag.Key" content="@metaTag.Value"/>
|
||||
}
|
||||
else
|
||||
{
|
||||
<meta name="@metaTag.Key" content="@metaTag.Value"/>
|
||||
}
|
||||
}
|
||||
}
|
||||
<meta property="theme-color" content="#AA0000"/>
|
||||
<title>@ViewData["Title"] - lighttube</title>
|
||||
<link rel="stylesheet" href="~/css/bootstrap-icons/bootstrap-icons.css"/>
|
||||
<link rel="stylesheet" href="~/css/desktop.css" asp-append-version="true"/>
|
||||
<link rel="stylesheet" href="~/css/lt-video/player-desktop.css" asp-append-version="true"/>
|
||||
<link rel="icon" href="~/favicon.ico">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@if (live)
|
||||
{
|
||||
<video class="player" poster="@Model.Player.Thumbnails.LastOrDefault()?.Url">
|
||||
</video>
|
||||
}
|
||||
else if (Model.Player.Formats.Length > 0)
|
||||
{
|
||||
<video class="player" controls src="/proxy/media/@Model.Player.Id/@HttpUtility.UrlEncode(Model.Player.Formats.First(x => x.FormatNote == Model.Resolution && x.FormatId != "17").FormatId)" poster="@Model.Player.Thumbnails.LastOrDefault()?.Url">
|
||||
@foreach (Subtitle subtitle in Model.Player.Subtitles ?? Array.Empty<Subtitle>())
|
||||
{
|
||||
@:<track src="/proxy/caption/@Model.Player.Id/@HttpUtility.UrlEncode(subtitle.Language).Replace("+", "%20")" label="@subtitle.Language" kind="subtitles">
|
||||
}
|
||||
</video>
|
||||
}
|
||||
else
|
||||
{
|
||||
canPlay = false;
|
||||
<div id="player" class="player error" style="background-image: url('@Model.Player.Thumbnails.LastOrDefault()?.Url')">
|
||||
@if (string.IsNullOrWhiteSpace(Model.Player.ErrorMessage))
|
||||
{
|
||||
<span>
|
||||
No playable streams returned from the API (@Model.Player.Formats.Length/@Model.Player.AdaptiveFormats.Length)
|
||||
</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span>
|
||||
@Model.Player.ErrorMessage
|
||||
</span>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (canPlay)
|
||||
{
|
||||
<script src="/js/lt-video/player-desktop.js"></script>
|
||||
@if (!Model.CompatibilityMode && !live)
|
||||
{
|
||||
<script src="/js/shaka-player/shaka-player.compiled.min.js"></script>
|
||||
<script>
|
||||
let player = undefined;
|
||||
loadPlayerWithShaka("video", {
|
||||
"id": "@Model.Video.Id",
|
||||
"title": "@Html.Raw(Model.Video.Title.Replace("\"", "\\\""))",
|
||||
"embed": true,
|
||||
"live": false,
|
||||
"storyboard": "/proxy/image?url=@HttpUtility.UrlEncode(Model.Player.Storyboards.FirstOrDefault())"
|
||||
}, [
|
||||
@foreach(Format f in Model.Player.Formats.Reverse())
|
||||
{
|
||||
@:{"height": @f.Resolution.Split("x")[1],"label":"@f.FormatName","src": "/proxy/video?url=@HttpUtility.UrlEncode(f.Url)"},
|
||||
}
|
||||
], "https://@(Context.Request.Host)/manifest/@(Model.Video.Id).mpd").then(x => player = x).catch(alert);;
|
||||
</script>
|
||||
}
|
||||
else if (live)
|
||||
{
|
||||
<script src="/js/hls.js/hls.min.js"></script>
|
||||
<script>
|
||||
let player = undefined;
|
||||
loadPlayerWithHls("video", {
|
||||
"id": "@(Model.Video.Id)",
|
||||
"title": "@Html.Raw(Model.Video.Title.Replace("\"", "\\\""))",
|
||||
"embed": true,
|
||||
"live": true
|
||||
}, "https://@(Context.Request.Host)/manifest/@(Model.Video.Id).m3u8").then(x => player = x).catch(alert);
|
||||
</script>
|
||||
}
|
||||
else
|
||||
{
|
||||
<script>
|
||||
const player = new Player("video", {
|
||||
"id": "@Model.Video.Id",
|
||||
"title": "@Html.Raw(Model.Video.Title.Replace("\"", "\\\""))",
|
||||
"embed": true,
|
||||
"live": false,
|
||||
"storyboard": "/proxy/image?url=@HttpUtility.UrlEncode(Model.Player.Storyboards.FirstOrDefault())"
|
||||
}, [
|
||||
@foreach(Format f in Model.Player.Formats.Reverse())
|
||||
{
|
||||
@:{"height": @f.Resolution.Split("x")[1],"label":"@f.FormatName","src": "/proxy/video?url=@HttpUtility.UrlEncode(f.Url)"},
|
||||
}
|
||||
]);
|
||||
</script>
|
||||
}
|
||||
}
|
||||
</body>
|
||||
</html>
|
||||
85
core/LightTube/Views/Youtube/Playlist.cshtml
Normal file
85
core/LightTube/Views/Youtube/Playlist.cshtml
Normal file
@@ -0,0 +1,85 @@
|
||||
@using InnerTube.Models
|
||||
@using System.Web
|
||||
@model LightTube.Contexts.PlaylistContext
|
||||
|
||||
@{
|
||||
ViewBag.Title = Model.Playlist.Title;
|
||||
ViewBag.Metadata = new Dictionary<string, string>();
|
||||
ViewBag.Metadata["og:title"] = Model.Playlist.Title;
|
||||
ViewBag.Metadata["og:url"] = $"{Url.ActionContext.HttpContext.Request.Scheme}://{Url.ActionContext.HttpContext.Request.Host}{Url.ActionContext.HttpContext.Request.Path}{Url.ActionContext.HttpContext.Request.QueryString}";
|
||||
ViewBag.Metadata["og:image"] = $"{Url.ActionContext.HttpContext.Request.Scheme}://{Url.ActionContext.HttpContext.Request.Host}/proxy/image?url={HttpUtility.UrlEncode(Model.Playlist.Thumbnail.FirstOrDefault()?.Url?.ToString())}";
|
||||
ViewBag.Metadata["twitter:card"] = $"{Url.ActionContext.HttpContext.Request.Scheme}://{Url.ActionContext.HttpContext.Request.Host}/proxy/image?url={HttpUtility.UrlEncode(Model.Playlist.Thumbnail.LastOrDefault()?.Url?.ToString())}";
|
||||
ViewBag.Metadata["og:description"] = Model.Playlist.Description;
|
||||
Layout = "_Layout";
|
||||
}
|
||||
|
||||
@if (!string.IsNullOrWhiteSpace(Model.Message))
|
||||
{
|
||||
<div class="playlist-message" style="padding: 16px;background-color: var(--border-color); color: var(--text-primary);">
|
||||
@Model.Message
|
||||
</div>
|
||||
}
|
||||
<div class="playlist-page">
|
||||
<div class="playlist-info">
|
||||
<div class="thumbnail" style="background-image: url('@Model.Playlist.Thumbnail.LastOrDefault()?.Url')">
|
||||
<a href="/watch?v=@Model.Playlist.Videos.FirstOrDefault()?.Id&list=@Model.Id">Play all</a>
|
||||
</div>
|
||||
<p class="title">@Model.Playlist.Title</p>
|
||||
<span class="info">@Model.Playlist.VideoCount videos • @Model.Playlist.ViewCount views • @Model.Playlist.LastUpdated</span>
|
||||
<span class="description">@Html.Raw(Model.Playlist.GetHtmlDescription())</span>
|
||||
<a href="/playlist?list=@Model.Id&remove=true" class="login-button" style="margin:unset;">
|
||||
<i class="bi bi-trash"></i>
|
||||
Delete playlist
|
||||
</a>
|
||||
<div class="channel-info">
|
||||
<a href="/channel/@Model.Playlist.Channel.Id" class="avatar">
|
||||
<img src="@Model.Playlist.Channel.Avatars.LastOrDefault()?.Url">
|
||||
</a>
|
||||
<div class="name">
|
||||
<a class="name" href="/channel/@Model.Playlist.Channel.Id">@Model.Playlist.Channel.Name</a>
|
||||
</div>
|
||||
<button class="subscribe-button" data-cid="@Model.Playlist.Channel.Id">Subscribe</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="video-list playlist-video-list">
|
||||
@foreach (PlaylistVideoItem video in Model.Playlist.Videos.Cast<PlaylistVideoItem>())
|
||||
{
|
||||
<div class="playlist-video">
|
||||
<a href="/watch?v=@video.Id&list=@Model.Id" class="index">
|
||||
@video.Index
|
||||
</a>
|
||||
<a href="/watch?v=@video.Id&list=@Model.Id" class="thumbnail"
|
||||
style="background-image: url('@video.Thumbnails.LastOrDefault()?.Url')">
|
||||
<span class="video-length">@video.Duration</span>
|
||||
</a>
|
||||
<div class="info">
|
||||
<a href="/watch?v=@video.Id&list=@Model.Id" class="title max-lines-2">
|
||||
@video.Title
|
||||
</a>
|
||||
<div>
|
||||
<a href="/channel/@video.Channel.Name">@video.Channel.Name</a>
|
||||
</div>
|
||||
</div>
|
||||
@if (Model.Editable)
|
||||
{
|
||||
<a href="/playlist?list=@Model.Id&delete=@(video.Index - 1)" class="edit">
|
||||
<i class="bi bi-trash"></i>
|
||||
</a>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div class="pagination-buttons">
|
||||
@if (!string.IsNullOrWhiteSpace(Model.ContinuationToken))
|
||||
{
|
||||
<a href="/playlist?list=@Model.Id">First Page</a>
|
||||
}
|
||||
<div class="divider"></div>
|
||||
<span>•</span>
|
||||
<div class="divider"></div>
|
||||
@if (!string.IsNullOrWhiteSpace(Model.Playlist.ContinuationKey))
|
||||
{
|
||||
<a href="/playlist?list=@Model.Id&continuation=@Model.Playlist.ContinuationKey">Next Page</a>
|
||||
}
|
||||
</div>
|
||||
28
core/LightTube/Views/Youtube/Search.cshtml
Normal file
28
core/LightTube/Views/Youtube/Search.cshtml
Normal file
@@ -0,0 +1,28 @@
|
||||
@using InnerTube.Models
|
||||
@model LightTube.Contexts.SearchContext
|
||||
|
||||
@{
|
||||
ViewBag.Title = Model.Query;
|
||||
Layout = "_Layout";
|
||||
ViewData["UseFullSizeSearchBar"] = Model.MobileLayout;
|
||||
}
|
||||
|
||||
<div class="video-list">
|
||||
@foreach (DynamicItem preview in Model.Results.Results)
|
||||
{
|
||||
@preview.GetHtml()
|
||||
}
|
||||
</div>
|
||||
<div class="pagination-buttons">
|
||||
@if (!string.IsNullOrWhiteSpace(Model.ContinuationKey))
|
||||
{
|
||||
<a href="/results?search_query=@Model.Query">First Page</a>
|
||||
}
|
||||
<div class="divider"></div>
|
||||
<span>•</span>
|
||||
<div class="divider"></div>
|
||||
@if (!string.IsNullOrWhiteSpace(Model.Results.ContinuationKey))
|
||||
{
|
||||
<a href="/results?search_query=@Model.Query&continuation=@Model.Results.ContinuationKey">Next Page</a>
|
||||
}
|
||||
</div>
|
||||
325
core/LightTube/Views/Youtube/Watch.cshtml
Normal file
325
core/LightTube/Views/Youtube/Watch.cshtml
Normal file
@@ -0,0 +1,325 @@
|
||||
@using System.Text.RegularExpressions
|
||||
@using System.Web
|
||||
@using InnerTube.Models
|
||||
@model LightTube.Contexts.PlayerContext
|
||||
|
||||
@{
|
||||
bool compatibility = false;
|
||||
if (Context.Request.Cookies.TryGetValue("compatibility", out string compatibilityString))
|
||||
bool.TryParse(compatibilityString, out compatibility);
|
||||
|
||||
ViewBag.Metadata = new Dictionary<string, string>();
|
||||
ViewBag.Metadata["author"] = Model.Video.Channel.Name;
|
||||
ViewBag.Metadata["og:title"] = Model.Player.Title;
|
||||
ViewBag.Metadata["og:url"] = $"{Url.ActionContext.HttpContext.Request.Scheme}://{Url.ActionContext.HttpContext.Request.Host}{Url.ActionContext.HttpContext.Request.Path}{Url.ActionContext.HttpContext.Request.QueryString}";
|
||||
ViewBag.Metadata["og:image"] = $"{Url.ActionContext.HttpContext.Request.Scheme}://{Url.ActionContext.HttpContext.Request.Host}/proxy/image?url={HttpUtility.UrlEncode(Model.Player.Thumbnails.FirstOrDefault()?.Url?.ToString())}";
|
||||
ViewBag.Metadata["twitter:card"] = $"{Url.ActionContext.HttpContext.Request.Scheme}://{Url.ActionContext.HttpContext.Request.Host}/proxy/image?url={HttpUtility.UrlEncode(Model.Player.Thumbnails.LastOrDefault()?.Url?.ToString())}";
|
||||
ViewBag.Metadata["og:description"] = Model.Player.Description;
|
||||
ViewBag.Title = Model.Player.Title;
|
||||
|
||||
Layout = "_Layout";
|
||||
try
|
||||
{
|
||||
ViewBag.Metadata["og:video"] = $"/proxy/video?url={HttpUtility.UrlEncode(Model.Player.Formats.First().Url.ToString())}";
|
||||
Model.Resolution ??= Model.Player.Formats.First().FormatNote;
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
ViewData["HideGuide"] = true;
|
||||
|
||||
bool live = Model.Player.Formats.Length == 0 && Model.Player.AdaptiveFormats.Length > 0;
|
||||
string description = Model.Video.GetHtmlDescription();
|
||||
const string youtubePattern = @"[w.]*youtube[-nockie]*\.com";
|
||||
|
||||
// turn URLs into hyperlinks
|
||||
Regex urlRegex = new(youtubePattern, RegexOptions.IgnoreCase);
|
||||
Match m;
|
||||
for (m = urlRegex.Match(description); m.Success; m = m.NextMatch())
|
||||
description = description.Replace(m.Groups[0].ToString(),
|
||||
$"{Url.ActionContext.HttpContext.Request.Host}");
|
||||
|
||||
bool canPlay = true;
|
||||
}
|
||||
|
||||
<!-- TODO: chapters -->
|
||||
<div class="watch-page">
|
||||
<div class="primary">
|
||||
<div class="video-player-container">
|
||||
|
||||
@if (live)
|
||||
{
|
||||
<video class="player" poster="@Model.Player.Thumbnails.LastOrDefault()?.Url">
|
||||
</video>
|
||||
}
|
||||
else if (Model.Player.Formats.Length > 0)
|
||||
{
|
||||
<video class="player" controls src="/proxy/media/@Model.Player.Id/@HttpUtility.UrlEncode(Model.Player.Formats.First(x => x.FormatNote == Model.Resolution && x.FormatId != "17").FormatId)" poster="@Model.Player.Thumbnails.LastOrDefault()?.Url">
|
||||
@foreach (Subtitle subtitle in Model.Player.Subtitles ?? Array.Empty<Subtitle>())
|
||||
{
|
||||
@:<track src="/proxy/caption/@Model.Player.Id/@HttpUtility.UrlEncode(subtitle.Language).Replace("+", "%20")" label="@subtitle.Language" kind="subtitles">
|
||||
}
|
||||
</video>
|
||||
}
|
||||
else
|
||||
{
|
||||
canPlay = false;
|
||||
<div id="player" class="player error" style="background-image: url('@Model.Player.Thumbnails.LastOrDefault()?.Url')">
|
||||
@if (string.IsNullOrWhiteSpace(Model.Player.ErrorMessage))
|
||||
{
|
||||
<span>
|
||||
No playable streams returned from the API (@Model.Player.Formats.Length/@Model.Player.AdaptiveFormats.Length)
|
||||
</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span>
|
||||
@Model.Player.ErrorMessage
|
||||
</span>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
@if (Model.MobileLayout)
|
||||
{
|
||||
<div class="video-info">
|
||||
<div class="video-title">@Model.Video.Title</div>
|
||||
<div class="video-info-bar">
|
||||
<span>@Model.Video.Views</span>
|
||||
<span>Published @Model.Video.UploadDate</span>
|
||||
<div class="divider"></div>
|
||||
<div class="video-info-buttons">
|
||||
<div>
|
||||
<i class="bi bi-hand-thumbs-up"></i><span>@Model.Engagement.Likes</span>
|
||||
</div>
|
||||
<div>
|
||||
<i class="bi bi-hand-thumbs-down"></i><span>@Model.Engagement.Dislikes</span>
|
||||
</div>
|
||||
<a href="/download?v=@Model.Video.Id">
|
||||
<i class="bi bi-download"></i>
|
||||
Download
|
||||
</a>
|
||||
<a href="/Account/AddVideoToPlaylist?v=@Model.Video.Id">
|
||||
<i class="bi bi-folder-plus"></i>
|
||||
Save
|
||||
</a>
|
||||
<a href="https://www.youtube.com/watch?v=@Model.Video.Id">
|
||||
<i class="bi bi-share"></i>
|
||||
YouTube link
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="channel-info">
|
||||
<a href="/channel/@Model.Video.Channel.Id" class="avatar">
|
||||
<img src="@Model.Video.Channel.Avatars.LastOrDefault()?.Url">
|
||||
</a>
|
||||
<div class="name">
|
||||
<a href="/channel/@Model.Video.Channel.Id">@Model.Video.Channel.Name</a>
|
||||
</div>
|
||||
<button class="subscribe-button" data-cid="@Model.Video.Channel.Id">Subscribe</button>
|
||||
</div>
|
||||
<p class="description">@Html.Raw(description)</p>
|
||||
</div>
|
||||
<hr>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="video-info">
|
||||
<div class="video-title">@Model.Video.Title</div>
|
||||
<p class="video-sub-info description">
|
||||
<span>@Model.Video.Views @Model.Video.UploadDate</span> @Html.Raw(description)
|
||||
</p>
|
||||
<div class="video-info-buttons">
|
||||
<div>
|
||||
<i class="bi bi-hand-thumbs-up"></i>
|
||||
@Model.Engagement.Likes
|
||||
</div>
|
||||
<div>
|
||||
<i class="bi bi-hand-thumbs-down"></i>
|
||||
@Model.Engagement.Dislikes
|
||||
</div>
|
||||
<a href="/download?v=@Model.Player.Id">
|
||||
<i class="bi bi-download"></i>
|
||||
Download
|
||||
</a>
|
||||
<a href="/Account/AddVideoToPlaylist?v=@Model.Video.Id">
|
||||
<i class="bi bi-folder-plus"></i>
|
||||
Save
|
||||
</a>
|
||||
<a href="https://www.youtube.com/watch?v=@Model.Video.Id">
|
||||
<i class="bi bi-share"></i>
|
||||
YouTube link
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="channel-info__bordered">
|
||||
<a href="/channel/@Model.Video.Channel.Id" class="avatar">
|
||||
<img src="@Model.Video.Channel.Avatars.FirstOrDefault()?.Url">
|
||||
</a>
|
||||
<div class="name">
|
||||
<a href="/channel/@Model.Video.Channel.Id">@Model.Video.Channel.Name</a>
|
||||
</div>
|
||||
<div class="subscriber-count">
|
||||
@Model.Video.Channel.SubscriberCount
|
||||
</div>
|
||||
<button class="subscribe-button" data-cid="@Model.Video.Channel.Id">Subscribe</button>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<div class="secondary">
|
||||
<noscript>
|
||||
<div class="resolutions-list">
|
||||
<h3>Change Resolution</h3>
|
||||
<div>
|
||||
@foreach (Format format in Model.Player.Formats.Where(x => x.FormatId != "17"))
|
||||
{
|
||||
@if (format.FormatNote == Model.Resolution)
|
||||
{
|
||||
<b>@format.FormatNote (current)</b>
|
||||
}
|
||||
else
|
||||
{
|
||||
<a href="/watch?v=@Model.Player.Id&quality=@format.FormatNote">@format.FormatNote</a>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</noscript>
|
||||
<div class="recommended-list">
|
||||
|
||||
@if (Model.Video.Recommended.Length == 0)
|
||||
{
|
||||
<p style="text-align: center">None :(<br>This is most likely an age-restricted video</p>
|
||||
}
|
||||
@foreach (DynamicItem recommendation in Model.Video.Recommended)
|
||||
{
|
||||
switch (recommendation)
|
||||
{
|
||||
case VideoItem video:
|
||||
<div class="video">
|
||||
<a href="/watch?v=@video.Id" class="thumbnail" style="background-image: url('@video.Thumbnails.LastOrDefault()?.Url')">
|
||||
<span class="video-length">@video.Duration</span>
|
||||
</a>
|
||||
<div class="info">
|
||||
<a href="/watch?v=@video.Id" class="title max-lines-2">@video.Title</a>
|
||||
<div>
|
||||
<a href="/channel/@video.Channel.Id" class="max-lines-1">@video.Channel.Name</a>
|
||||
<div>
|
||||
<span>@video.Views views</span>
|
||||
<span>•</span>
|
||||
<span>@video.UploadedAt</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
break;
|
||||
case PlaylistItem playlist:
|
||||
<div class="playlist">
|
||||
<a href="/watch?v=@playlist.FirstVideoId&list=@playlist.Id" class="thumbnail" style="background-image: url('@playlist.Thumbnails.LastOrDefault()?.Url')">
|
||||
<div>
|
||||
<span>@playlist.VideoCount</span>
|
||||
<span>VIDEOS</span>
|
||||
</div>
|
||||
</a>
|
||||
<div class="info">
|
||||
<a href="/watch?v=@playlist.FirstVideoId&list=@playlist.Id" class="title max-lines-2">@playlist.Title</a>
|
||||
<div>
|
||||
<a href="/channel/@playlist.Channel.Id">@playlist.Channel.Name</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
break;
|
||||
case RadioItem radio:
|
||||
<div class="playlist">
|
||||
<a href="/watch?v=@radio.FirstVideoId&list=@radio.Id" class="thumbnail" style="background-image: url('@radio.Thumbnails.LastOrDefault()?.Url')">
|
||||
<div>
|
||||
<span>MIX</span>
|
||||
</div>
|
||||
</a>
|
||||
<div class="info">
|
||||
<a href="/watch?v=@radio.FirstVideoId&list=@radio.Id" class="title max-lines-2">@radio.Title</a>
|
||||
<div>
|
||||
<span>@radio.Channel.Name</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
break;
|
||||
case ContinuationItem continuationItem:
|
||||
break;
|
||||
default:
|
||||
<div class="video">
|
||||
<div class="thumbnail" style="background-image: url('@recommendation.Thumbnails?.LastOrDefault()?.Url')"></div>
|
||||
<div class="info">
|
||||
<span class="title max-lines-2">@recommendation.GetType().Name</span>
|
||||
<div>
|
||||
<b>WARNING:</b> Unknown recommendation type: @recommendation.Id
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
break;
|
||||
}
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (canPlay)
|
||||
{
|
||||
@if (Model.MobileLayout)
|
||||
{
|
||||
<script src="/js/lt-video/player-mobile.js"></script>
|
||||
}
|
||||
else
|
||||
{
|
||||
<script src="/js/lt-video/player-desktop.js"></script>
|
||||
}
|
||||
@if (!Model.CompatibilityMode && !live)
|
||||
{
|
||||
<script src="/js/shaka-player/shaka-player.compiled.min.js"></script>
|
||||
<script>
|
||||
let player = undefined;
|
||||
loadPlayerWithShaka("video", {
|
||||
"id": "@Model.Video.Id",
|
||||
"title": "@Html.Raw(Model.Video.Title.Replace("\"", "\\\""))",
|
||||
"embed": false,
|
||||
"live": false,
|
||||
"storyboard": "/proxy/image?url=@HttpUtility.UrlEncode(Model.Player.Storyboards.FirstOrDefault())"
|
||||
}, [
|
||||
@foreach (Format f in Model.Player.Formats.Reverse())
|
||||
{
|
||||
@:{"height": @f.Resolution.Split("x")[1],"label":"@f.FormatName","src": "/proxy/video?url=@HttpUtility.UrlEncode(f.Url)"},
|
||||
}
|
||||
], "https://@(Context.Request.Host)/manifest/@(Model.Video.Id).mpd").then(x => player = x).catch(alert);
|
||||
</script>
|
||||
}
|
||||
else if (live)
|
||||
{
|
||||
<script src="/js/hls.js/hls.min.js"></script>
|
||||
<script>
|
||||
let player = undefined;
|
||||
loadPlayerWithHls("video", {
|
||||
"id": "@(Model.Video.Id)",
|
||||
"title": "@Html.Raw(Model.Video.Title.Replace("\"", "\\\""))",
|
||||
"embed": false,
|
||||
"live": true
|
||||
}, "https://@(Context.Request.Host)/manifest/@(Model.Video.Id).m3u8").then(x => player = x).catch(alert);
|
||||
</script>
|
||||
}
|
||||
else
|
||||
{
|
||||
<script>
|
||||
const player = new Player("video", {
|
||||
"id": "@Model.Video.Id",
|
||||
"title": "@Html.Raw(Model.Video.Title.Replace("\"", "\\\""))",
|
||||
"embed": false,
|
||||
"live": false,
|
||||
"storyboard": "/proxy/image?url=@HttpUtility.UrlEncode(Model.Player.Storyboards.FirstOrDefault())"
|
||||
}, [
|
||||
@foreach (Format f in Model.Player.Formats.Reverse())
|
||||
{
|
||||
@:{"height": @f.Resolution.Split("x")[1],"label":"@f.FormatName","src": "/proxy/media/@(Model.Player.Id)/@(f.FormatId)"},
|
||||
}
|
||||
]);
|
||||
</script>
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user