mirror of
https://git.vectorsigma.ru/public/tubearchivist.git
synced 2026-08-04 21:39:49 +00:00
DRAFT: Add Tubearchivist Frontend React dev docker setup (#768)
* Add development docker-compose file * Add /new/ path in nginx conf * Add frontend production setup * Fix lint * Refac move prod docker compose into non suffixed file * Fix run.sh fileendings on windows * Fix docker file naming consistancies * Add frontend dev setup * Add docker compose dev command * Refac remove docker network * Fix potential error causes * Chore update react-router-dom * Add redirect to login after logout * Refac allow basic auth for session login in api * Fix loginresponsetype optional property * Refac move isAdmin check into page Base * Refac use node lts for dev container * Refac remove old setup in readme * Refac move getisAdmin into loader and rename * Refac remove manual csrf cookie handing from actions and loader * Fix post requiring csrf header & cookie * Fix remove empty files * Refac revert dockerfile changes * Refac revert gitatrributes changes * Refac revert docker changes * Refac revert nginx changes * Refac revert docker change * Refac move frontend into frontend folder * Add production steps to dockerfile * Refac implement endpoint renaming * Refac remove frontend dockerfile * Add credentials include for dev env * Fix allow cors with credentials for dev environment * Fix images in dev mode * Add credentials for dev mode to all loader and actions, except signin * Revert cors config * Revert cors config * Fix nginx not serving /youtube/ * Fix video url missing api * Fix media url missing api * Add application settings page * Add continue vids * Add csrf to delete requests * Refac use api/video endpoint with filter to home, channel, playlist pages * Add channel nav request * Add channel playlists * Fix filterbar for playlist in channel * Add playlist_nav to video page * Add downloads aggs * Refac remove basic auth * Fix credentials include in signin * Refac user config to user me config * Add ApiToken get
This commit is contained in:
254
frontend/src/components/VideoPlayer.tsx
Normal file
254
frontend/src/components/VideoPlayer.tsx
Normal file
@@ -0,0 +1,254 @@
|
||||
import updateVideoProgressById from '../api/actions/updateVideoProgressById';
|
||||
import updateWatchedState from '../api/actions/updateWatchedState';
|
||||
import { SponsorBlockSegmentType, SponsorBlockType, VideoResponseType } from '../pages/Video';
|
||||
import watchedThreshold from '../functions/watchedThreshold';
|
||||
import Notifications from './Notifications';
|
||||
import { Dispatch, SetStateAction, SyntheticEvent, useState } from 'react';
|
||||
import formatTime from '../functions/formatTime';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
import getApiUrl from '../configuration/getApiUrl';
|
||||
|
||||
type VideoTag = SyntheticEvent<HTMLVideoElement, Event>;
|
||||
|
||||
export type SkippedSegmentType = {
|
||||
from: number;
|
||||
to: number;
|
||||
};
|
||||
|
||||
export type SponsorSegmentsSkippedType = Record<string, SkippedSegmentType>;
|
||||
|
||||
type Subtitle = {
|
||||
name: string;
|
||||
source: string;
|
||||
lang: string;
|
||||
media_url: string;
|
||||
};
|
||||
|
||||
type SubtitlesProp = {
|
||||
subtitles: Subtitle[];
|
||||
};
|
||||
|
||||
const Subtitles = ({ subtitles }: SubtitlesProp) => {
|
||||
return subtitles.map((subtitle: Subtitle) => {
|
||||
let label = subtitle.name;
|
||||
|
||||
if (subtitle.source === 'auto') {
|
||||
label += ' - auto';
|
||||
}
|
||||
|
||||
return (
|
||||
<track
|
||||
key={subtitle.name}
|
||||
label={label}
|
||||
kind="subtitles"
|
||||
srcLang={subtitle.lang}
|
||||
src={`${getApiUrl()}${subtitle.media_url}`}
|
||||
/>
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
const handleTimeUpdate =
|
||||
(
|
||||
youtubeId: string,
|
||||
duration: number,
|
||||
watched: boolean,
|
||||
sponsorBlock?: SponsorBlockType,
|
||||
setSponsorSegmentSkipped?: Dispatch<SetStateAction<SponsorSegmentsSkippedType>>,
|
||||
) =>
|
||||
async (videoTag: VideoTag) => {
|
||||
const currentTime = Number(videoTag.currentTarget.currentTime);
|
||||
|
||||
if (sponsorBlock && sponsorBlock.segments) {
|
||||
sponsorBlock.segments.forEach((segment: SponsorBlockSegmentType) => {
|
||||
const [from, to] = segment.segment;
|
||||
|
||||
if (currentTime >= from && currentTime <= from + 0.3) {
|
||||
videoTag.currentTarget.currentTime = to;
|
||||
|
||||
setSponsorSegmentSkipped?.((segments: SponsorSegmentsSkippedType) => {
|
||||
return { ...segments, [segment.UUID]: { from, to } };
|
||||
});
|
||||
}
|
||||
|
||||
if (currentTime > to + 10) {
|
||||
setSponsorSegmentSkipped?.((segments: SponsorSegmentsSkippedType) => {
|
||||
return { ...segments, [segment.UUID]: { from: 0, to: 0 } };
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (currentTime < 10) return;
|
||||
if (Number((currentTime % 10).toFixed(1)) <= 0.2) {
|
||||
// Check progress every 10 seconds or else progress is checked a few times a second
|
||||
await updateVideoProgressById({
|
||||
youtubeId,
|
||||
currentProgress: currentTime,
|
||||
});
|
||||
|
||||
if (!watched) {
|
||||
// Check if video is already marked as watched
|
||||
if (watchedThreshold(currentTime, duration)) {
|
||||
await updateWatchedState({
|
||||
id: youtubeId,
|
||||
is_watched: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleVideoEnd =
|
||||
(
|
||||
youtubeId: string,
|
||||
watched: boolean,
|
||||
setSponsorSegmentSkipped?: Dispatch<SetStateAction<SponsorSegmentsSkippedType>>,
|
||||
) =>
|
||||
async () => {
|
||||
if (!watched) {
|
||||
// Check if video is already marked as watched
|
||||
await updateWatchedState({ id: youtubeId, is_watched: true });
|
||||
}
|
||||
|
||||
setSponsorSegmentSkipped?.((segments: SponsorSegmentsSkippedType) => {
|
||||
const keys = Object.keys(segments);
|
||||
|
||||
keys.forEach(uuid => {
|
||||
segments[uuid] = { from: 0, to: 0 };
|
||||
});
|
||||
|
||||
return segments;
|
||||
});
|
||||
};
|
||||
|
||||
export type VideoProgressType = {
|
||||
youtube_id: string;
|
||||
user_id: number;
|
||||
position: number;
|
||||
};
|
||||
|
||||
type VideoPlayerProps = {
|
||||
video: VideoResponseType;
|
||||
videoProgress?: VideoProgressType;
|
||||
sponsorBlock?: SponsorBlockType;
|
||||
embed?: boolean;
|
||||
};
|
||||
|
||||
const VideoPlayer = ({ video, videoProgress, sponsorBlock, embed }: VideoPlayerProps) => {
|
||||
const [searchParams] = useSearchParams();
|
||||
const searchParamVideoProgress = searchParams.get('t');
|
||||
|
||||
const [skippedSegments, setSkippedSegments] = useState<SponsorSegmentsSkippedType>({});
|
||||
|
||||
const videoId = video.data.youtube_id;
|
||||
const videoUrl = video.data.media_url;
|
||||
const videoThumbUrl = video.data.vid_thumb_url;
|
||||
const watched = video.data.player.watched;
|
||||
const duration = video.data.player.duration;
|
||||
const videoSubtitles = video.data.subtitles;
|
||||
|
||||
let videoSrcProgress = Number(videoProgress?.position) > 0 ? Number(videoProgress?.position) : '';
|
||||
|
||||
if (searchParamVideoProgress !== null) {
|
||||
videoSrcProgress = searchParamVideoProgress;
|
||||
}
|
||||
|
||||
const autoplay = false;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div id="player" className={embed ? '' : 'player-wrapper'}>
|
||||
<div className={embed ? '' : 'video-main'}>
|
||||
<video
|
||||
poster={`${getApiUrl()}${videoThumbUrl}`}
|
||||
onVolumeChange={(videoTag: VideoTag) => {
|
||||
localStorage.setItem('playerVolume', videoTag.currentTarget.volume.toString());
|
||||
}}
|
||||
onLoadStart={(videoTag: VideoTag) => {
|
||||
videoTag.currentTarget.volume = Number(localStorage.getItem('playerVolume')) ?? 1;
|
||||
}}
|
||||
onTimeUpdate={handleTimeUpdate(
|
||||
videoId,
|
||||
duration,
|
||||
watched,
|
||||
sponsorBlock,
|
||||
setSkippedSegments,
|
||||
)}
|
||||
onPause={async (videoTag: VideoTag) => {
|
||||
const currentTime = Number(videoTag.currentTarget.currentTime);
|
||||
|
||||
if (currentTime < 10) return;
|
||||
|
||||
await updateVideoProgressById({
|
||||
youtubeId: videoId,
|
||||
currentProgress: currentTime,
|
||||
});
|
||||
}}
|
||||
onEnded={handleVideoEnd(videoId, watched)}
|
||||
autoPlay={autoplay}
|
||||
controls
|
||||
width="100%"
|
||||
playsInline
|
||||
id="video-item"
|
||||
>
|
||||
<source
|
||||
src={`${getApiUrl()}${videoUrl}#t=${videoSrcProgress}`}
|
||||
type="video/mp4"
|
||||
id="video-source"
|
||||
/>
|
||||
{videoSubtitles && <Subtitles subtitles={videoSubtitles} />}
|
||||
</video>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Notifications pageName="all" />
|
||||
<div className="sponsorblock" id="sponsorblock">
|
||||
{sponsorBlock?.is_enabled && (
|
||||
<>
|
||||
{sponsorBlock.segments.length == 0 && (
|
||||
<h4>
|
||||
This video doesn't have any sponsor segments added. To add a segment go to{' '}
|
||||
<u>
|
||||
<a href={`https://www.youtube.com/watch?v=${videoId}`}>this video on YouTube</a>
|
||||
</u>{' '}
|
||||
and add a segment using the{' '}
|
||||
<u>
|
||||
<a href="https://sponsor.ajay.app/">SponsorBlock</a>
|
||||
</u>{' '}
|
||||
extension.
|
||||
</h4>
|
||||
)}
|
||||
{sponsorBlock.has_unlocked && (
|
||||
<h4>
|
||||
This video has unlocked sponsor segments. Go to{' '}
|
||||
<u>
|
||||
<a href={`https://www.youtube.com/watch?v=${videoId}`}>this video on YouTube</a>
|
||||
</u>{' '}
|
||||
and vote on the segments using the{' '}
|
||||
<u>
|
||||
<a href="https://sponsor.ajay.app/">SponsorBlock</a>
|
||||
</u>{' '}
|
||||
extension.
|
||||
</h4>
|
||||
)}
|
||||
|
||||
{Object.values(skippedSegments).map(({ from, to }) => {
|
||||
return (
|
||||
<>
|
||||
{from !== 0 && to !== 0 && (
|
||||
<h3>
|
||||
Skipped sponsor segment from {formatTime(from)} to {formatTime(to)}.
|
||||
</h3>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default VideoPlayer;
|
||||
Reference in New Issue
Block a user