Skip to content
This repository has been archived by the owner on Nov 27, 2024. It is now read-only.

Commit

Permalink
Audio only video player (#165)
Browse files Browse the repository at this point in the history
* Implement audio only watch pages

* Add quality selector labels for audio formats

* Fix video posters

* Add buttons in thumbnails for shortcuts

* Make thumbnails clickable again
  • Loading branch information
kuylar authored Aug 15, 2024
1 parent bd2efa5 commit 3e36eea
Show file tree
Hide file tree
Showing 12 changed files with 170 additions and 55 deletions.
4 changes: 2 additions & 2 deletions LightTube/Contexts/EmbedContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@ public class EmbedContext : BaseContext
public InnerTubeVideo Video;

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

Expand Down
19 changes: 9 additions & 10 deletions LightTube/Contexts/PlayerContext.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
using InnerTube;
using InnerTube.Models;
using InnerTube.Models;
using InnerTube.Protobuf;
using InnerTube.Protobuf.Responses;
using InnerTube.Renderers;
using Newtonsoft.Json;

namespace LightTube.Contexts;
Expand All @@ -14,15 +12,16 @@ public class PlayerContext : BaseContext
public Exception? Exception;
public bool UseHls;
public bool UseDash;
public Thumbnail[] Thumbnails = [];
public Thumbnail[] Thumbnails;
public string? ErrorMessage = null;
public int PreferredItag = 18;
public bool UseEmbedUi = false;
public string? ClassName;
public SponsorBlockSegment[] Sponsors;
public bool AudioOnly;

public PlayerContext(HttpContext context, InnerTubePlayer innerTubePlayer, InnerTubeVideo? video, string className,
bool compatibility, string? preferredItag, SponsorBlockSegment[] sponsors) : base(context)
bool compatibility, string? preferredItag, SponsorBlockSegment[] sponsors, bool audioOnly) : base(context)
{
Player = innerTubePlayer;
Video = video;
Expand All @@ -31,6 +30,8 @@ public PlayerContext(HttpContext context, InnerTubePlayer innerTubePlayer, Inner
Sponsors = sponsors;
UseHls = !compatibility && !string.IsNullOrWhiteSpace(innerTubePlayer.HlsManifestUrl); // Prefer HLS
UseDash = innerTubePlayer.AdaptiveFormats.Any() && !compatibility;
AudioOnly = audioOnly;
Thumbnails = innerTubePlayer.Details.Thumbnails;
// Formats
if (!Configuration.ProxyEnabled)
{
Expand Down Expand Up @@ -81,11 +82,9 @@ public PlayerContext(HttpContext context, Exception e) : base(context)
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);
AudioOnly
? Player?.AdaptiveFormats.FirstOrDefault(x => x.Mime.StartsWith("audio/"))
: Player?.Formats.FirstOrDefault();

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

public IEnumerable<Format> GetFormatsInPreferredOrder() =>
Player!.Formats.OrderBy(x => x.Itag != PreferredItag).Where(x => x.Itag != 17);
}
8 changes: 4 additions & 4 deletions LightTube/Contexts/WatchContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,10 @@ public class WatchContext : BaseContext

public WatchContext(HttpContext context, InnerTubePlayer innerTubePlayer, InnerTubeVideo innerTubeVideo,
ContinuationResponse? comments, bool compatibility, int dislikes,
SponsorBlockSegment[] sponsors) : base(context)
SponsorBlockSegment[] sponsors, bool audioOnly) : base(context)
{
Player = new PlayerContext(context, innerTubePlayer, innerTubeVideo, "embed", compatibility,
context.Request.Query["q"], sponsors);
context.Request.Query["q"], sponsors, audioOnly);
Video = innerTubeVideo;
Playlist = Video.Playlist;
Comments = comments;
Expand Down Expand Up @@ -80,10 +80,10 @@ public WatchContext(HttpContext context, Exception e, InnerTubeVideo innerTubeVi

public WatchContext(HttpContext context, InnerTubePlayer innerTubePlayer, InnerTubeVideo innerTubeVideo,
DatabasePlaylist? playlist, ContinuationResponse? comments, bool compatibility, int dislikes,
SponsorBlockSegment[] sponsors) : base(context)
SponsorBlockSegment[] sponsors, bool audioOnly) : base(context)
{
Player = new PlayerContext(context, innerTubePlayer, innerTubeVideo, "embed", compatibility,
context.Request.Query["q"], sponsors);
context.Request.Query["q"], sponsors, audioOnly);
Video = innerTubeVideo;
Playlist = playlist?.GetVideoPlaylistInfo(innerTubeVideo.Id,
DatabaseManager.Users.GetUserFromId(playlist.Author).Result!,
Expand Down
12 changes: 7 additions & 5 deletions LightTube/Controllers/YoutubeController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ namespace LightTube.Controllers;
public class YoutubeController(SimpleInnerTubeClient innerTube, HttpClient client) : Controller
{
[Route("/embed/{v}")]
public async Task<IActionResult> Embed(string v, bool contentCheckOk, bool compatibility = false)
public async Task<IActionResult> Embed(string v, bool contentCheckOk, bool compatibility = false,
bool audioOnly = false)
{
InnerTubePlayer? player;
Exception? e;
Expand Down Expand Up @@ -49,11 +50,12 @@ public async Task<IActionResult> Embed(string v, bool contentCheckOk, bool compa
language: HttpContext.GetInnerTubeLanguage(), region: HttpContext.GetInnerTubeRegion());
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, sponsors));
return View(new EmbedContext(HttpContext, player, video, compatibility, sponsors, audioOnly));
}

[Route("/watch")]
public async Task<IActionResult> Watch(string v, string? list, bool contentCheckOk, bool compatibility = false)
public async Task<IActionResult> Watch(string v, string? list, bool contentCheckOk, bool compatibility = false,
bool audioOnly = false)
{
InnerTubePlayer? player;
Exception? e;
Expand Down Expand Up @@ -127,14 +129,14 @@ public async Task<IActionResult> Watch(string v, string? list, bool contentCheck
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,
sponsors));
sponsors, audioOnly));
}

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, sponsors));
new WatchContext(HttpContext, player, video, comments, compatibility, dislikes, sponsors, audioOnly));
}

[Route("/results")]
Expand Down
5 changes: 5 additions & 0 deletions LightTube/Resources/Localization/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,8 @@
"pagination.first": "First Page",
"pagination.next": "Next Page",

"player.audioQuality": "(Audio only) {0}kbps",

"playlist.add.title": "Add video to playlist",
"playlist.add.body": "Please select the playlist to add this video into.",
"playlist.add.confirm": "Add",
Expand Down Expand Up @@ -245,6 +247,9 @@
"video.unavailable": "Unavailable",
"video.trailer.title": "Trailer",
"video.trailer.body": "Livestream will begin in {0}",
"video.watch": "Watch",
"video.listen": "Listen",
"video.download": "Download",

"watch.like.unavailable": "Like",
"watch.dislike.unavailable": "Dislike",
Expand Down
32 changes: 7 additions & 25 deletions LightTube/Views/Shared/Player.cshtml
Original file line number Diff line number Diff line change
Expand Up @@ -75,24 +75,9 @@
</div>
}
}
else if ((Model.UseHls || Model.UseDash) && !Model.Player.Formats.Any())
else if ((Model.UseHls || Model.UseDash) && !Model.Player.Formats.Any() && !Model.AudioOnly)
{
<video controls id="player" class="player @(Model.GetClass())" poster="@Model.Thumbnails.LastOrDefault()?.Url">
@if (Configuration.ProxyEnabled)
{
foreach (Format format in Model.GetFormatsInPreferredOrder())
{
@:<source label="@format.QualityLabel" type="@format.Mime" src="/proxy/media/@Model.Player.Details.Id/@format.Itag">
}
}
else
{
foreach (Format format in Model.GetFormatsInPreferredOrder())
{
@:<source label="@format.QualityLabel" type="@format.Mime" src="@format.Url">
}
}

@foreach (InnerTubePlayer.VideoCaption subtitle in Model.Player.Captions)
{
@:<track src="/proxy/caption/@Model.Player.Details.Id/@subtitle.VssId" label="@subtitle.Label" kind="subtitles">
Expand All @@ -103,18 +88,15 @@ else if ((Model.UseHls || Model.UseDash) && !Model.Player.Formats.Any())
else if (Model.Player.Formats.Any())
{
<video controls id="player" class="player @(Model.GetClass())" poster="@Model.Thumbnails.LastOrDefault()?.Url">
@if (Configuration.ProxyEnabled)
@foreach (Format format in Model.AudioOnly ? Model.Player.AdaptiveFormats.Where(x => x.Mime.StartsWith("audio/")).OrderByDescending(x => x.Bitrate).ToArray() : Model.Player.Formats)
{
foreach (Format format in Model.GetFormatsInPreferredOrder())
if (Configuration.ProxyEnabled)
{
@:<source label="@format.QualityLabel" type="@format.Mime" src="/proxy/media/@Model.Player.Details.Id/@format.Itag">
@:<source label="@Model.Localization.FormatString("player.audioQuality", Math.Round(format.Bitrate / 1000f))" type="@format.Mime" src="/proxy/media/@Model.Player.Details.Id/@format.Itag">
}
}
else
{
foreach (Format format in Model.GetFormatsInPreferredOrder())
else
{
@:<source label="@format.QualityLabel" type="@format.Mime" src="@format.Url">
@:<source label="@Model.Localization.FormatString("player.audioQuality", Math.Round(format.Bitrate / 1000f))" type="@format.Mime" src="@format.Url">
}
}

Expand Down Expand Up @@ -150,7 +132,7 @@ else
href: "/channel/@Model.Player!.Details.Author.Id"
},
title: "@Model.Player!.Details.Title", // .Replace("\"", "\\\"")
@if (Model.UseHls)
@if (Model.UseHls && !Model.AudioOnly)
{
@:hlsManifest: "/proxy/media/@(Model.Player.Details.Id).m3u8",
}
Expand Down
22 changes: 20 additions & 2 deletions LightTube/Views/Shared/Renderers/Channel/ChannelGrid.cshtml
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,30 @@
case "video":
VideoRendererData video = (VideoRendererData)renderer.Data;
<div class="renderer-gridvideorenderer">
<a href="/[email protected]" class="grid-thumbnail">
<div href="/[email protected]" class="grid-thumbnail">
<img loading="lazy" src="@video.Thumbnails.LastOrDefault()?.Url" alt="@video.Title">
<div class="compact-thumbnail__duration__container">
<span class="compact-thumbnail__duration">@video.Duration.ToDurationString()</span>
</div>
</a>
<a href="/[email protected]" class="thumbnail__video-href"></a>
<div class="thumbnail__buttons__container">
<a href="/[email protected]" class="thumbnail__button" title="@Model.Localization.GetString("watch.save")">
<svg class="bi" width="20" height="20" fill="currentColor">
<use xlink:href="/svg/bootstrap-icons.svg#folder-plus"/>
</svg>
</a>
<a href="/[email protected]&audioOnly=true" class="thumbnail__button" title="@Model.Localization.GetString("video.listen")">
<svg class="bi" width="20" height="20" fill="currentColor">
<use xlink:href="/svg/bootstrap-icons.svg#headphones"/>
</svg>
</a>
<a href="/download/@video.VideoId" class="thumbnail__button" title="@Model.Localization.GetString("video.download")">
<svg class="bi" width="20" height="20" fill="currentColor">
<use xlink:href="/svg/bootstrap-icons.svg#download"/>
</svg>
</a>
</div>
</div>
<div class="info">
<a href="/[email protected]" class="ml-2 title" title="@video.Title">
@video.Title
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
// im sorry
InnerTubePlayer player = Context.RequestServices.GetService<SimpleInnerTubeClient>()
!.GetVideoPlayerAsync(video.VideoId, false).Result;
PlayerContext playerContext = new(Context, player, null, "channel-promoted-video", false, "18", []);
PlayerContext playerContext = new(Context, player, null, "channel-promoted-video", false, "18", [], false);
<partial name="Player" model="playerContext"/>
}
catch (Exception ex)
Expand Down
22 changes: 20 additions & 2 deletions LightTube/Views/Shared/Renderers/ContainerRenderer.cshtml
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,30 @@
case "video":
VideoRendererData video = (VideoRendererData)renderer.Data;
<div class="renderer-gridvideorenderer">
<a href="/[email protected]" class="grid-thumbnail">
<div href="/[email protected]" class="grid-thumbnail">
<img loading="lazy" src="@video.Thumbnails.LastOrDefault()?.Url" alt="@video.Title">
<div class="compact-thumbnail__duration__container">
<span class="compact-thumbnail__duration">@video.Duration.ToDurationString()</span>
</div>
</a>
<a href="/[email protected]" class="thumbnail__video-href"></a>
<div class="thumbnail__buttons__container">
<a href="/[email protected]" class="thumbnail__button" title="@Model.Localization.GetString("watch.save")">
<svg class="bi" width="20" height="20" fill="currentColor">
<use xlink:href="/svg/bootstrap-icons.svg#folder-plus"/>
</svg>
</a>
<a href="/[email protected]&audioOnly=true" class="thumbnail__button" title="@Model.Localization.GetString("video.listen")">
<svg class="bi" width="20" height="20" fill="currentColor">
<use xlink:href="/svg/bootstrap-icons.svg#headphones"/>
</svg>
</a>
<a href="/download/@video.VideoId" class="thumbnail__button" title="@Model.Localization.GetString("video.download")">
<svg class="bi" width="20" height="20" fill="currentColor">
<use xlink:href="/svg/bootstrap-icons.svg#download"/>
</svg>
</a>
</div>
</div>
<div class="info">
<a href="/[email protected]" class="ml-2 title" title="@video.Title">
@video.Title
Expand Down
22 changes: 20 additions & 2 deletions LightTube/Views/Shared/Renderers/SearchRenderer.cshtml
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,30 @@
case "video":
VideoRendererData video = (VideoRendererData)Model.Renderer.Data;
<div class="renderer-videorenderer">
<a class="thumbnail" href="/[email protected]">
<div class="thumbnail" href="/[email protected]">
<img loading="lazy" src="@video.Thumbnails.LastOrDefault()?.Url" alt="@video.Title">
<div class="thumbnail__duration__container">
<span class="thumbnail__duration">@video.Duration.ToDurationString()</span>
</div>
</a>
<a href="/[email protected]" class="thumbnail__video-href"></a>
<div class="thumbnail__buttons__container">
<a href="/[email protected]" class="thumbnail__button" title="@Model.Localization.GetString("watch.save")">
<svg class="bi" width="20" height="20" fill="currentColor">
<use xlink:href="/svg/bootstrap-icons.svg#folder-plus"/>
</svg>
</a>
<a href="/[email protected]&audioOnly=true" class="thumbnail__button" title="@Model.Localization.GetString("video.listen")">
<svg class="bi" width="20" height="20" fill="currentColor">
<use xlink:href="/svg/bootstrap-icons.svg#headphones"/>
</svg>
</a>
<a href="/download/@video.VideoId" class="thumbnail__button" title="@Model.Localization.GetString("video.download")">
<svg class="bi" width="20" height="20" fill="currentColor">
<use xlink:href="/svg/bootstrap-icons.svg#download"/>
</svg>
</a>
</div>
</div>
<div class="info">
<a href="/[email protected]" class="ml-2 title" title="@video.Title">
@video.Title
Expand Down
41 changes: 39 additions & 2 deletions LightTube/Views/Youtube/Watch.cshtml
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,25 @@
</div>
</a>
</div>
@if (Model.Player.AudioOnly) {
<div class="interaction-buttons">
<a class="interaction-button" href="/watch@(Context.Request.QueryString.ToString().Replace("&audioOnly=true", ""))" title="@Model.Localization.GetString("video.watch")">
<svg class="icon" width="20" height="20" fill="currentColor">
<use xlink:href="/svg/bootstrap-icons.svg#film"/>
</svg>
</a>
</div>
}
else
{
<div class="interaction-buttons">
<a class="interaction-button" href="/watch@(Context.Request.QueryString)&audioOnly=true" title="@Model.Localization.GetString("video.listen")">
<svg class="icon" width="20" height="20" fill="currentColor">
<use xlink:href="/svg/bootstrap-icons.svg#headphones"/>
</svg>
</a>
</div>
}
<div class="interaction-buttons">
<a class="interaction-button" href="https://youtube.com/watch@(Context.Request.QueryString)" title="@Model.Localization.GetString("watch.youtube")">
<svg class="icon" width="20" height="20" fill="currentColor">
Expand Down Expand Up @@ -264,12 +283,30 @@
case "video":
VideoRendererData video = (VideoRendererData)renderer.Data;
<div class="renderer-compactvideorenderer">
<a class="compact-thumbnail" href="/[email protected]">
<div class="compact-thumbnail">
<img loading="lazy" src="@video.Thumbnails.LastOrDefault()?.Url" alt="@video.Title">
<div class="compact-thumbnail__duration__container">
<span class="compact-thumbnail__duration">@video.Duration.ToDurationString()</span>
</div>
</a>
<a href="/[email protected]" class="thumbnail__video-href"></a>
<div class="thumbnail__buttons__container">
<a href="/[email protected]" class="thumbnail__button" title="@Model.Localization.GetString("watch.save")">
<svg class="bi" width="20" height="20" fill="currentColor">
<use xlink:href="/svg/bootstrap-icons.svg#folder-plus"/>
</svg>
</a>
<a href="/[email protected]&audioOnly=true" class="thumbnail__button" title="@Model.Localization.GetString("video.listen")">
<svg class="bi" width="20" height="20" fill="currentColor">
<use xlink:href="/svg/bootstrap-icons.svg#headphones"/>
</svg>
</a>
<a href="/download/@video.VideoId" class="thumbnail__button" title="@Model.Localization.GetString("video.download")">
<svg class="bi" width="20" height="20" fill="currentColor">
<use xlink:href="/svg/bootstrap-icons.svg#download"/>
</svg>
</a>
</div>
</div>
<div class="info">
<a class="ml-2 title" href="/[email protected]" title="@video.Title">
@video.Title
Expand Down
Loading

0 comments on commit 3e36eea

Please sign in to comment.