Player shortcuts, #build
Changed: - Added player shortcuts, '?' - Fixed playlist missing channel metadata - Changed arm64 build to use patched ffmpeg
This commit is contained in:
commit
3eacc06383
25
Dockerfile
25
Dockerfile
|
@ -6,8 +6,8 @@
|
||||||
FROM python:3.10.8-slim-bullseye AS builder
|
FROM python:3.10.8-slim-bullseye AS builder
|
||||||
ARG TARGETPLATFORM
|
ARG TARGETPLATFORM
|
||||||
|
|
||||||
RUN apt-get update
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
RUN apt-get install -y --no-install-recommends build-essential gcc libldap2-dev libsasl2-dev libssl-dev
|
build-essential gcc libldap2-dev libsasl2-dev libssl-dev
|
||||||
|
|
||||||
# install requirements
|
# install requirements
|
||||||
COPY ./tubearchivist/requirements.txt /requirements.txt
|
COPY ./tubearchivist/requirements.txt /requirements.txt
|
||||||
|
@ -32,19 +32,22 @@ RUN apt-get clean && apt-get -y update && apt-get -y install --no-install-recomm
|
||||||
curl \
|
curl \
|
||||||
xz-utils && rm -rf /var/lib/apt/lists/*
|
xz-utils && rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
# get newest patched ffmpeg and ffprobe builds for amd64 fall back to repo ffmpeg for arm64
|
# install patched ffmpeg build, default to linux64
|
||||||
RUN if [ "$TARGETPLATFORM" = "linux/amd64" ] ; then \
|
RUN if [ "$TARGETPLATFORM" = "linux/arm64" ] ; then \
|
||||||
|
curl -s https://api.github.com/repos/yt-dlp/FFmpeg-Builds/releases/latest \
|
||||||
|
| grep browser_download_url \
|
||||||
|
| grep ".*master.*linuxarm64.*tar.xz" \
|
||||||
|
| cut -d '"' -f 4 \
|
||||||
|
| xargs curl -L --output ffmpeg.tar.xz ; \
|
||||||
|
else \
|
||||||
curl -s https://api.github.com/repos/yt-dlp/FFmpeg-Builds/releases/latest \
|
curl -s https://api.github.com/repos/yt-dlp/FFmpeg-Builds/releases/latest \
|
||||||
| grep browser_download_url \
|
| grep browser_download_url \
|
||||||
| grep ".*master.*linux64.*tar.xz" \
|
| grep ".*master.*linux64.*tar.xz" \
|
||||||
| cut -d '"' -f 4 \
|
| cut -d '"' -f 4 \
|
||||||
| xargs curl -L --output ffmpeg.tar.xz && \
|
| xargs curl -L --output ffmpeg.tar.xz ; \
|
||||||
tar -xf ffmpeg.tar.xz --strip-components=2 --no-anchored -C /usr/bin/ "ffmpeg" && \
|
fi && \
|
||||||
tar -xf ffmpeg.tar.xz --strip-components=2 --no-anchored -C /usr/bin/ "ffprobe" && \
|
tar -xf ffmpeg.tar.xz --strip-components=2 --no-anchored -C /usr/bin/ "ffmpeg" && \
|
||||||
rm ffmpeg.tar.xz \
|
tar -xf ffmpeg.tar.xz --strip-components=2 --no-anchored -C /usr/bin/ "ffprobe"
|
||||||
; elif [ "$TARGETPLATFORM" = "linux/arm64" ] ; then \
|
|
||||||
apt-get -y update && apt-get -y install --no-install-recommends ffmpeg && rm -rf /var/lib/apt/lists/* \
|
|
||||||
; fi
|
|
||||||
|
|
||||||
# install debug tools for testing environment
|
# install debug tools for testing environment
|
||||||
RUN if [ "$INSTALL_DEBUG" ] ; then \
|
RUN if [ "$INSTALL_DEBUG" ] ; then \
|
||||||
|
|
|
@ -18,7 +18,10 @@ class YoutubePlaylist(YouTubeItem):
|
||||||
|
|
||||||
es_path = False
|
es_path = False
|
||||||
index_name = "ta_playlist"
|
index_name = "ta_playlist"
|
||||||
yt_obs = {"extract_flat": True}
|
yt_obs = {
|
||||||
|
"extract_flat": True,
|
||||||
|
"allow_playlist_files": True,
|
||||||
|
}
|
||||||
yt_base = "https://www.youtube.com/playlist?list="
|
yt_base = "https://www.youtube.com/playlist?list="
|
||||||
|
|
||||||
def __init__(self, youtube_id):
|
def __init__(self, youtube_id):
|
||||||
|
|
|
@ -2,7 +2,9 @@
|
||||||
{% block content %}
|
{% block content %}
|
||||||
{% load static %}
|
{% load static %}
|
||||||
{% load humanize %}
|
{% load humanize %}
|
||||||
<div class="video-main"></div>
|
<div class="video-main">
|
||||||
|
<div class="video-modal"><span class="video-modal-text"></span></div>
|
||||||
|
</div>
|
||||||
<div class="notifications" id="notifications"></div>
|
<div class="notifications" id="notifications"></div>
|
||||||
<div class="sponsorblock" id="sponsorblock">
|
<div class="sponsorblock" id="sponsorblock">
|
||||||
{% if video.sponsorblock.is_enabled %}
|
{% if video.sponsorblock.is_enabled %}
|
||||||
|
@ -67,7 +69,7 @@
|
||||||
<p class="thumb-icon"><img class="dislike" src="{% static 'img/icon-thumb.svg' %}" alt="thumbs-down">: {{ video.stats.dislike_count|intcomma }}</p>
|
<p class="thumb-icon"><img class="dislike" src="{% static 'img/icon-thumb.svg' %}" alt="thumbs-down">: {{ video.stats.dislike_count|intcomma }}</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if video.stats.average_rating %}
|
{% if video.stats.average_rating %}
|
||||||
<p class="rating-stars">Rating:
|
<p class="rating-stars">Rating:
|
||||||
{% for star in video.stats.average_rating %}
|
{% for star in video.stats.average_rating %}
|
||||||
<img src="/static/img/icon-star-{{ star }}.svg" alt="{{ star }}">
|
<img src="/static/img/icon-star-{{ star }}.svg" alt="{{ star }}">
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
@ -90,7 +92,7 @@
|
||||||
<a href="{% url 'playlist_id' playlist_item.playlist_meta.playlist_id %}">
|
<a href="{% url 'playlist_id' playlist_item.playlist_meta.playlist_id %}">
|
||||||
<h3>Playlist [{{ playlist_item.playlist_meta.current_idx|add:"1" }}]: {{ playlist_item.playlist_meta.playlist_name }}</h3>
|
<h3>Playlist [{{ playlist_item.playlist_meta.current_idx|add:"1" }}]: {{ playlist_item.playlist_meta.playlist_name }}</h3>
|
||||||
</a>
|
</a>
|
||||||
<div class="playlist-nav">
|
<div class="playlist-nav">
|
||||||
<div class="playlist-nav-item">
|
<div class="playlist-nav-item">
|
||||||
{% if playlist_item.playlist_previous %}
|
{% if playlist_item.playlist_previous %}
|
||||||
<a href="{% url 'video' playlist_item.playlist_previous.youtube_id %}">
|
<a href="{% url 'video' playlist_item.playlist_previous.youtube_id %}">
|
||||||
|
|
|
@ -388,6 +388,7 @@ button:hover {
|
||||||
display: grid;
|
display: grid;
|
||||||
align-content: space-evenly;
|
align-content: space-evenly;
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
|
position: relative; /* needed for modal */
|
||||||
}
|
}
|
||||||
|
|
||||||
.notifications {
|
.notifications {
|
||||||
|
@ -744,6 +745,22 @@ video:-webkit-full-screen {
|
||||||
/* video page */
|
/* video page */
|
||||||
.video-main {
|
.video-main {
|
||||||
margin: 1rem 0;
|
margin: 1rem 0;
|
||||||
|
position: relative; /* needed for modal */
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-modal {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 1;
|
||||||
|
top: 20%;
|
||||||
|
width: 100%;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-modal-text {
|
||||||
|
background: rgba(0,0,0,.5);
|
||||||
|
color: #eeeeee;
|
||||||
|
font-size: 1.3em;
|
||||||
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.video-main video {
|
.video-main video {
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
|
|
||||||
function sortChange(sortValue) {
|
function sortChange(sortValue) {
|
||||||
var payload = JSON.stringify({'sort_order': sortValue});
|
var payload = JSON.stringify({'sort_order': sortValue});
|
||||||
sendPost(payload);
|
sendPost(payload);
|
||||||
|
@ -411,7 +410,7 @@ function createPlayer(button) {
|
||||||
} else {
|
} else {
|
||||||
var watchStatusIndicator = createWatchStatusIndicator(videoId, "unwatched");
|
var watchStatusIndicator = createWatchStatusIndicator(videoId, "unwatched");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
var playerStats = `<div class="thumb-icon player-stats"><img src="/static/img/icon-eye.svg" alt="views icon"><span>${videoViews}</span>`;
|
var playerStats = `<div class="thumb-icon player-stats"><img src="/static/img/icon-eye.svg" alt="views icon"><span>${videoViews}</span>`;
|
||||||
if (videoData.data.stats.like_count) {
|
if (videoData.data.stats.like_count) {
|
||||||
|
@ -426,6 +425,7 @@ function createPlayer(button) {
|
||||||
|
|
||||||
const markup = `
|
const markup = `
|
||||||
<div class="video-player" data-id="${videoId}">
|
<div class="video-player" data-id="${videoId}">
|
||||||
|
<div class="video-modal"><span class="video-modal-text"></span></div>
|
||||||
${videoTag}
|
${videoTag}
|
||||||
<div class="notifications" id="notifications"></div>
|
<div class="notifications" id="notifications"></div>
|
||||||
${sponsorBlockElements}
|
${sponsorBlockElements}
|
||||||
|
@ -444,13 +444,14 @@ function createPlayer(button) {
|
||||||
`;
|
`;
|
||||||
const divPlayer = document.getElementById("player");
|
const divPlayer = document.getElementById("player");
|
||||||
divPlayer.innerHTML = markup;
|
divPlayer.innerHTML = markup;
|
||||||
|
recordTextTrackChanges();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add video tag to video page when passed a video id, function loaded on page load `video.html (115-117)`
|
// Add video tag to video page when passed a video id, function loaded on page load `video.html (115-117)`
|
||||||
function insertVideoTag(videoData, videoProgress) {
|
function insertVideoTag(videoData, videoProgress) {
|
||||||
var videoTag = createVideoTag(videoData, videoProgress);
|
var videoTag = createVideoTag(videoData, videoProgress);
|
||||||
var videoMain = document.getElementsByClassName("video-main");
|
var videoMain = document.querySelector(".video-main");
|
||||||
videoMain[0].innerHTML = videoTag;
|
videoMain.innerHTML += videoTag;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generates a video tag with subtitles when passed videoData and videoProgress.
|
// Generates a video tag with subtitles when passed videoData and videoProgress.
|
||||||
|
@ -793,8 +794,8 @@ function setProgressBar(videoId, currentTime, duration) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// progressBar = document.getElementById("progress-" + videoId);
|
// progressBar = document.getElementById("progress-" + videoId);
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// multi search form
|
// multi search form
|
||||||
|
@ -1046,7 +1047,7 @@ function createFulltext(fullText) {
|
||||||
</a>
|
</a>
|
||||||
<div class="video-desc list">
|
<div class="video-desc list">
|
||||||
<p>${subtitle_start} - ${subtitle_end}</p>
|
<p>${subtitle_start} - ${subtitle_end}</p>
|
||||||
<p>${subtitleLine}</p>
|
<p>${subtitleLine}</p>
|
||||||
<div>
|
<div>
|
||||||
<a href="/channel/${channelId}/"><h3>${channelName}</h3></a>
|
<a href="/channel/${channelId}/"><h3>${channelName}</h3></a>
|
||||||
<a class="video-more" href="/video/${videoId}/?t=${subtitle_start}"><h2>${videoTitle}</h2></a>
|
<a class="video-more" href="/video/${videoId}/?t=${subtitle_start}"><h2>${videoTitle}</h2></a>
|
||||||
|
@ -1173,3 +1174,133 @@ function animate(elementId, animationClass) {
|
||||||
toAnimate.classList.remove(animationClass);
|
toAnimate.classList.remove(animationClass);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// keep track of changes to the subtitles list made with the native UI
|
||||||
|
// needed so that when toggling subtitles with the shortcut we go to the last selected one, not the first one
|
||||||
|
addEventListener('DOMContentLoaded', recordTextTrackChanges);
|
||||||
|
|
||||||
|
let lastSeenTextTrack = 0;
|
||||||
|
function recordTextTrackChanges() {
|
||||||
|
let player = getVideoPlayer();
|
||||||
|
if (player == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
player.textTracks.addEventListener('change', () => {
|
||||||
|
let active = [...player.textTracks].findIndex(x => x.mode === 'showing');
|
||||||
|
if (active !== -1) {
|
||||||
|
lastSeenTextTrack = active;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// keyboard shortcuts for the video player
|
||||||
|
document.addEventListener('keydown', doShortcut);
|
||||||
|
|
||||||
|
let modalHideTimeout = -1;
|
||||||
|
function showModal(html, duration) {
|
||||||
|
let player = getVideoPlayer();
|
||||||
|
let modal = document.querySelector('.video-modal-text');
|
||||||
|
modal.innerHTML = html;
|
||||||
|
modal.style.display = 'initial';
|
||||||
|
clearTimeout(modalHideTimeout);
|
||||||
|
modalHideTimeout = setTimeout(() => { modal.style.display = 'none'; }, duration);
|
||||||
|
}
|
||||||
|
|
||||||
|
let videoSpeeds = [.25, .5, .75, 1, 1.25, 1.5, 1.75, 2, 2.25, 2.5, 2.75, 3];
|
||||||
|
function doShortcut(e) {
|
||||||
|
if (!(e.target instanceof HTMLElement)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let target = e.target;
|
||||||
|
let targetName = target.nodeName.toLowerCase();
|
||||||
|
if (targetName === 'textarea' || targetName === 'input' || targetName === 'select' || target.isContentEditable) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (e.altKey || e.ctrlKey || e.metaKey) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let player = getVideoPlayer();
|
||||||
|
if (player == null) {
|
||||||
|
// not on the video page
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
switch (e.key) {
|
||||||
|
case 'c': {
|
||||||
|
// toggle captions
|
||||||
|
let tracks = [...player.textTracks];
|
||||||
|
if (tracks.length === 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
let active = tracks.find(x => x.mode === 'showing');
|
||||||
|
if (active != null) {
|
||||||
|
active.mode = 'disabled';
|
||||||
|
} else {
|
||||||
|
tracks[lastSeenTextTrack].mode = 'showing';
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'm': {
|
||||||
|
player.muted = !player.muted;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'ArrowLeft': {
|
||||||
|
if (targetName === 'video') {
|
||||||
|
// hitting arrows while the video is focused will use the built-in skip
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
showModal('- 5 seconds', 500);
|
||||||
|
player.currentTime -= 5;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'ArrowRight': {
|
||||||
|
if (targetName === 'video') {
|
||||||
|
// hitting space while the video is focused will use the built-in skip
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
showModal('+ 5 seconds', 500);
|
||||||
|
player.currentTime += 5;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case '<':
|
||||||
|
case '>': {
|
||||||
|
// change speed
|
||||||
|
let currentSpeedIdx = videoSpeeds.findIndex(s => s >= player.playbackRate);
|
||||||
|
if (currentSpeedIdx === -1) {
|
||||||
|
// handle the case where the user manually set the speed above our max speed
|
||||||
|
currentSpeedIdx = videoSpeeds.length - 1;
|
||||||
|
}
|
||||||
|
let newSpeedIdx = e.key === '<' ? Math.max(0, currentSpeedIdx - 1) : Math.min(videoSpeeds.length - 1, currentSpeedIdx + 1);
|
||||||
|
let newSpeed = videoSpeeds[newSpeedIdx];
|
||||||
|
player.playbackRate = newSpeed;
|
||||||
|
showModal(newSpeed + 'x', 500);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case ' ': {
|
||||||
|
if (targetName === 'video') {
|
||||||
|
// hitting space while the video is focused will toggle it anyway
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
e.preventDefault();
|
||||||
|
if (player.paused) {
|
||||||
|
player.play();
|
||||||
|
} else {
|
||||||
|
player.pause();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case '?': {
|
||||||
|
showModal(`
|
||||||
|
<table style="margin: auto; background: rgba(0,0,0,.5)"><tbody>
|
||||||
|
<tr><td>Show help</td><td>?</td>
|
||||||
|
<tr><td>Toggle mute</td><td>m</td>
|
||||||
|
<tr><td>Toggle subtitles (if available)</td><td>c</td>
|
||||||
|
<tr><td>Increase speed</td><td>></td>
|
||||||
|
<tr><td>Decrease speed</td><td><</td>
|
||||||
|
<tr><td>Back 5 seconds</td><td>←</td>
|
||||||
|
<tr><td>Forward 5 seconds</td><td>→</td>
|
||||||
|
`, 3000);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue