mirror of
https://github.com/tubearchivist/tubearchivist.git
synced 2025-04-20 03:40:12 +00:00
Player URL fixes, #build
Changed: - Fix player url query persistence - Fix various search params handling - Bumped yt-dlp
This commit is contained in:
commit
253571b5ac
@ -294,7 +294,7 @@ CORS_ALLOW_HEADERS = list(default_headers) + [
|
||||
|
||||
# TA application settings
|
||||
TA_UPSTREAM = "https://github.com/tubearchivist/tubearchivist"
|
||||
TA_VERSION = "v0.5.1"
|
||||
TA_VERSION = "v0.5.2-unstable"
|
||||
|
||||
# API
|
||||
REST_FRAMEWORK = {
|
||||
|
@ -3,8 +3,8 @@ ipython==9.0.2
|
||||
pre-commit==4.2.0
|
||||
pylint-django==2.6.1
|
||||
pylint==3.3.6
|
||||
pytest-django==4.10.0
|
||||
pytest-django==4.11.1
|
||||
pytest==8.3.5
|
||||
python-dotenv==1.1.0
|
||||
requirementscheck==0.0.6
|
||||
types-requests==2.32.0.20250306
|
||||
types-requests==2.32.0.20250328
|
||||
|
@ -1,10 +1,10 @@
|
||||
apprise==1.9.2
|
||||
celery==5.4.0
|
||||
apprise==1.9.3
|
||||
celery==5.5.0
|
||||
django-auth-ldap==5.1.0
|
||||
django-celery-beat==2.7.0
|
||||
django-cors-headers==4.7.0
|
||||
Django==5.1.7
|
||||
djangorestframework==3.15.2
|
||||
Django==5.1.8
|
||||
djangorestframework==3.16.0
|
||||
drf-spectacular==0.28.0
|
||||
Pillow==11.1.0
|
||||
redis==5.2.1
|
||||
@ -12,4 +12,4 @@ requests==2.32.3
|
||||
ryd-client==0.0.6
|
||||
uvicorn==0.34.0
|
||||
whitenoise==6.9.0
|
||||
yt-dlp[default]==2025.3.26
|
||||
yt-dlp[default]==2025.3.31
|
||||
|
660
frontend/package-lock.json
generated
660
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -31,7 +31,7 @@
|
||||
"prettier": "3.5.1",
|
||||
"typescript": "^5.7.3",
|
||||
"typescript-eslint": "^8.24.0",
|
||||
"vite": "^6.1.0",
|
||||
"vite": ">=6.2.4",
|
||||
"vite-plugin-checker": "^0.8.0"
|
||||
}
|
||||
}
|
||||
|
@ -3,7 +3,7 @@ import APIClient from '../../functions/APIClient';
|
||||
const createCustomPlaylist = async (playlistId: string) => {
|
||||
return APIClient('/api/playlist/custom/', {
|
||||
method: 'POST',
|
||||
body: { playlist_name: playlistId },
|
||||
body: { playlist_name: playlistId.trim() },
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -8,7 +8,9 @@ const updateBulkChannelSubscriptions = async (channelIds: string, status: boolea
|
||||
const youtubeChannelIds = channelIds.split('\n');
|
||||
|
||||
youtubeChannelIds.forEach(channelId => {
|
||||
channels.push({ channel_id: channelId, channel_subscribed: status });
|
||||
if (channelId.trim()) {
|
||||
channels.push({ channel_id: channelId, channel_subscribed: status });
|
||||
}
|
||||
});
|
||||
} else {
|
||||
channels.push({ channel_id: channelIds, channel_subscribed: status });
|
||||
|
@ -8,7 +8,9 @@ const updateBulkPlaylistSubscriptions = async (playlistIds: string, status: bool
|
||||
const youtubePlaylistIds = playlistIds.split('\n');
|
||||
|
||||
youtubePlaylistIds.forEach(playlistId => {
|
||||
playlists.push({ playlist_id: playlistId, playlist_subscribed: status });
|
||||
if (playlistId.trim()) {
|
||||
playlists.push({ playlist_id: playlistId, playlist_subscribed: status });
|
||||
}
|
||||
});
|
||||
} else {
|
||||
playlists.push({ playlist_id: playlistIds, playlist_subscribed: status });
|
||||
|
@ -8,7 +8,9 @@ const updateDownloadQueue = async (youtubeIdStrings: string, autostart: boolean)
|
||||
const youtubeIds = youtubeIdStrings.split('\n');
|
||||
|
||||
youtubeIds.forEach(youtubeId => {
|
||||
urls.push({ youtube_id: youtubeId, status: 'pending' });
|
||||
if (youtubeId.trim()) {
|
||||
urls.push({ youtube_id: youtubeId, status: 'pending' });
|
||||
}
|
||||
});
|
||||
} else {
|
||||
urls.push({ youtube_id: youtubeIdStrings, status: 'pending' });
|
||||
|
@ -7,9 +7,15 @@ type ChannelIconProps = {
|
||||
};
|
||||
|
||||
const ChannelBanner = ({ channelId, channelBannerUrl }: ChannelIconProps) => {
|
||||
let src = `${getApiUrl()}${channelBannerUrl}`;
|
||||
|
||||
if (channelBannerUrl === undefined) {
|
||||
src = defaultChannelImage;
|
||||
}
|
||||
|
||||
return (
|
||||
<img
|
||||
src={`${getApiUrl()}${channelBannerUrl}`}
|
||||
src={src}
|
||||
alt={`${channelId}-banner`}
|
||||
onError={({ currentTarget }) => {
|
||||
currentTarget.onerror = null; // prevents looping
|
||||
|
@ -7,9 +7,15 @@ type ChannelIconProps = {
|
||||
};
|
||||
|
||||
const ChannelIcon = ({ channelId, channelThumbUrl }: ChannelIconProps) => {
|
||||
let src = `${getApiUrl()}${channelThumbUrl}`;
|
||||
|
||||
if (channelThumbUrl === undefined) {
|
||||
src = defaultChannelIcon;
|
||||
}
|
||||
|
||||
return (
|
||||
<img
|
||||
src={`${getApiUrl()}${channelThumbUrl}`}
|
||||
src={src}
|
||||
alt={`${channelId}-thumb`}
|
||||
onError={({ currentTarget }) => {
|
||||
currentTarget.onerror = null; // prevents looping
|
||||
|
@ -6,9 +6,8 @@ import formatDate from '../functions/formatDates';
|
||||
import Button from './Button';
|
||||
import deleteDownloadById from '../api/actions/deleteDownloadById';
|
||||
import updateDownloadQueueStatusById from '../api/actions/updateDownloadQueueStatusById';
|
||||
import getApiUrl from '../configuration/getApiUrl';
|
||||
import { useUserConfigStore } from '../stores/UserConfigStore';
|
||||
import defaultVideoThumb from '/img/default-video-thumb.jpg';
|
||||
import VideoThumbnail from './VideoThumbail';
|
||||
|
||||
type DownloadListItemProps = {
|
||||
download: Download;
|
||||
@ -26,14 +25,7 @@ const DownloadListItem = ({ download, setRefresh }: DownloadListItemProps) => {
|
||||
<div className={`video-item ${view}`} id={`dl-${download.youtube_id}`}>
|
||||
<div className={`video-thumb-wrap ${view}`}>
|
||||
<div className="video-thumb">
|
||||
<img
|
||||
src={`${getApiUrl()}${download.vid_thumb_url}`}
|
||||
alt="video_thumb"
|
||||
onError={({ currentTarget }) => {
|
||||
currentTarget.onerror = null; // prevents looping
|
||||
currentTarget.src = defaultVideoThumb;
|
||||
}}
|
||||
/>
|
||||
<VideoThumbnail videoThumbUrl={download.vid_thumb_url} />
|
||||
|
||||
<div className="video-tags">
|
||||
{showIgnored && <span>ignored</span>}
|
||||
|
@ -121,7 +121,11 @@ const EmbeddableVideoPlayer = ({ videoId }: EmbeddableVideoPlayerProps) => {
|
||||
alt="close-icon"
|
||||
title="Close player"
|
||||
onClick={() => {
|
||||
setSearchParams({});
|
||||
setSearchParams(params => {
|
||||
const newParams = new URLSearchParams(params);
|
||||
newParams.delete('videoId');
|
||||
return newParams;
|
||||
});
|
||||
}}
|
||||
/>
|
||||
|
||||
|
@ -40,7 +40,7 @@ const Notifications = ({
|
||||
}
|
||||
|
||||
setNotificationResponse(notifications);
|
||||
}, 500);
|
||||
}, 1000);
|
||||
|
||||
return () => {
|
||||
clearInterval(intervalId);
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Link, useSearchParams } from 'react-router-dom';
|
||||
import { Fragment } from 'react/jsx-runtime';
|
||||
import Routes from '../configuration/routes/RouteList';
|
||||
import { useCallback, useEffect } from 'react';
|
||||
@ -23,6 +23,9 @@ interface Props {
|
||||
const Pagination = ({ pagination, setPage }: Props) => {
|
||||
const { total_hits, params, prev_pages, current_page, next_pages, last_page, max_hits } =
|
||||
pagination;
|
||||
const [searchParams] = useSearchParams();
|
||||
|
||||
const videoId = searchParams.get('videoId');
|
||||
|
||||
const totalHits = Number(total_hits);
|
||||
const currentPage = Number(current_page);
|
||||
@ -39,7 +42,7 @@ const Pagination = ({ pagination, setPage }: Props) => {
|
||||
(event: KeyboardEvent) => {
|
||||
const { code } = event;
|
||||
|
||||
if (code === 'ArrowRight') {
|
||||
if (code === 'ArrowRight' && videoId === null) {
|
||||
if (currentPage === 0 && totalHits > 1) {
|
||||
setPage(2);
|
||||
return;
|
||||
@ -52,7 +55,7 @@ const Pagination = ({ pagination, setPage }: Props) => {
|
||||
setPage(currentPage + 1);
|
||||
}
|
||||
|
||||
if (code === 'ArrowLeft') {
|
||||
if (code === 'ArrowLeft' && videoId === null) {
|
||||
if (currentPage === 0) {
|
||||
return;
|
||||
}
|
||||
@ -65,7 +68,7 @@ const Pagination = ({ pagination, setPage }: Props) => {
|
||||
setPage(currentPage - 1);
|
||||
}
|
||||
},
|
||||
[currentPage, lastPage, setPage, totalHits],
|
||||
[currentPage, lastPage, setPage, totalHits, videoId],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -7,9 +7,15 @@ type PlaylistThumbnailProps = {
|
||||
};
|
||||
|
||||
const PlaylistThumbnail = ({ playlistId, playlistThumbnail }: PlaylistThumbnailProps) => {
|
||||
let src = `${getApiUrl()}${playlistThumbnail}`;
|
||||
|
||||
if (playlistThumbnail === undefined) {
|
||||
src = defaultPlaylistThumbnail;
|
||||
}
|
||||
|
||||
return (
|
||||
<img
|
||||
src={`${getApiUrl()}${playlistThumbnail}`}
|
||||
src={src}
|
||||
alt={`${playlistId}-thumbnail`}
|
||||
onError={({ currentTarget }) => {
|
||||
currentTarget.onerror = null; // prevents looping
|
||||
|
@ -2,7 +2,7 @@ import { Link, useSearchParams } from 'react-router-dom';
|
||||
import Routes from '../configuration/routes/RouteList';
|
||||
import iconPlay from '/img/icon-play.svg';
|
||||
import Linkify from './Linkify';
|
||||
import getApiUrl from '../configuration/getApiUrl';
|
||||
import VideoThumbnail from './VideoThumbail';
|
||||
|
||||
type SubtitleListType = {
|
||||
subtitle_index: number;
|
||||
@ -44,15 +44,17 @@ const SubtitleList = ({ subtitleList }: SubtitleListProps) => {
|
||||
<div className="video-item list">
|
||||
<a
|
||||
onClick={() => {
|
||||
setSearchParams({
|
||||
videoId: subtitle.youtube_id,
|
||||
t: stripNanoSecs(subtitle.subtitle_start) || '00:00:00',
|
||||
setSearchParams(params => {
|
||||
params.set('videoId', subtitle.youtube_id);
|
||||
params.set('t', stripNanoSecs(subtitle.subtitle_start) || '00:00:00');
|
||||
|
||||
return params;
|
||||
});
|
||||
}}
|
||||
>
|
||||
<div className="video-thumb-wrap list">
|
||||
<div className="video-thumb">
|
||||
<img src={`${getApiUrl()}${subtitle.vid_thumb_url}`} alt="video-thumb" />
|
||||
<VideoThumbnail videoThumbUrl={subtitle.vid_thumb_url} />
|
||||
</div>
|
||||
<div className="video-play">
|
||||
<img src={iconPlay} alt="play-icon" />
|
||||
|
@ -4,14 +4,13 @@ import { VideoType, ViewLayoutType } from '../pages/Home';
|
||||
import iconPlay from '/img/icon-play.svg';
|
||||
import iconDotMenu from '/img/icon-dot-menu.svg';
|
||||
import iconClose from '/img/icon-close.svg';
|
||||
import defaultVideoThumb from '/img/default-video-thumb.jpg';
|
||||
import updateWatchedState from '../api/actions/updateWatchedState';
|
||||
import formatDate from '../functions/formatDates';
|
||||
import WatchedCheckBox from './WatchedCheckBox';
|
||||
import MoveVideoMenu from './MoveVideoMenu';
|
||||
import { useState } from 'react';
|
||||
import getApiUrl from '../configuration/getApiUrl';
|
||||
import deleteVideoProgressById from '../api/actions/deleteVideoProgressById';
|
||||
import VideoThumbnail from './VideoThumbail';
|
||||
|
||||
type VideoListItemProps = {
|
||||
video: VideoType;
|
||||
@ -40,19 +39,16 @@ const VideoListItem = ({
|
||||
<div className={`video-item ${viewLayout}`}>
|
||||
<a
|
||||
onClick={() => {
|
||||
setSearchParams({ videoId: video.youtube_id });
|
||||
setSearchParams(params => {
|
||||
const newParams = new URLSearchParams(params);
|
||||
newParams.set('videoId', video.youtube_id);
|
||||
return newParams;
|
||||
});
|
||||
}}
|
||||
>
|
||||
<div className={`video-thumb-wrap ${viewLayout}`}>
|
||||
<div className="video-thumb">
|
||||
<img
|
||||
src={`${getApiUrl()}${video.vid_thumb_url}`}
|
||||
alt="video-thumb"
|
||||
onError={({ currentTarget }) => {
|
||||
currentTarget.onerror = null; // prevents looping
|
||||
currentTarget.src = defaultVideoThumb;
|
||||
}}
|
||||
/>
|
||||
<VideoThumbnail videoThumbUrl={video.vid_thumb_url} />
|
||||
|
||||
{video.player.progress && (
|
||||
<div
|
||||
|
27
frontend/src/components/VideoThumbail.tsx
Normal file
27
frontend/src/components/VideoThumbail.tsx
Normal file
@ -0,0 +1,27 @@
|
||||
import getApiUrl from '../configuration/getApiUrl';
|
||||
import defaultVideoThumb from '/img/default-video-thumb.jpg';
|
||||
|
||||
type VideoThumbailProps = {
|
||||
videoThumbUrl: string | undefined;
|
||||
};
|
||||
|
||||
const VideoThumbnail = ({ videoThumbUrl }: VideoThumbailProps) => {
|
||||
let src = `${getApiUrl()}${videoThumbUrl}`;
|
||||
|
||||
if (videoThumbUrl === undefined) {
|
||||
src = defaultVideoThumb;
|
||||
}
|
||||
|
||||
return (
|
||||
<img
|
||||
src={src}
|
||||
alt="video_thumb"
|
||||
onError={({ currentTarget }) => {
|
||||
currentTarget.onerror = null; // prevents looping
|
||||
currentTarget.src = defaultVideoThumb;
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default VideoThumbnail;
|
@ -36,6 +36,7 @@ export type OutletContextType = {
|
||||
};
|
||||
|
||||
const Base = () => {
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
const { setAuth } = useAuthStore();
|
||||
const { setUserConfig } = useUserConfigStore();
|
||||
const { setUserAccount } = useUserAccountStore();
|
||||
@ -45,12 +46,9 @@ const Base = () => {
|
||||
|
||||
const location = useLocation();
|
||||
|
||||
const searchParams = new URLSearchParams(location.search);
|
||||
|
||||
const currentPageFromUrl = Number(searchParams.get('page'));
|
||||
|
||||
const [currentPage, setCurrentPage] = useState(currentPageFromUrl);
|
||||
const [, setSearchParams] = useSearchParams();
|
||||
|
||||
useEffect(() => {
|
||||
setAuth(auth);
|
||||
@ -82,7 +80,11 @@ const Base = () => {
|
||||
useEffect(() => {
|
||||
if (currentPageFromUrl !== currentPage) {
|
||||
setSearchParams(params => {
|
||||
params.set('page', currentPage.toString());
|
||||
if (currentPage == 0) {
|
||||
params.delete('page');
|
||||
} else {
|
||||
params.set('page', currentPage.toString());
|
||||
}
|
||||
|
||||
return params;
|
||||
});
|
||||
|
@ -119,10 +119,12 @@ const Channels = () => {
|
||||
label="Subscribe"
|
||||
type="submit"
|
||||
onClick={async () => {
|
||||
await updateBulkChannelSubscriptions(channelsToSubscribeTo, true);
|
||||
if (channelsToSubscribeTo.trim()) {
|
||||
await updateBulkChannelSubscriptions(channelsToSubscribeTo, true);
|
||||
|
||||
setShowNotification(true);
|
||||
setShowAddForm(false);
|
||||
setShowNotification(true);
|
||||
setShowAddForm(false);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
@ -98,24 +98,30 @@ const Download = () => {
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
const videosResponse = await loadDownloadQueue(
|
||||
currentPage,
|
||||
channelFilterFromUrl,
|
||||
showIgnored,
|
||||
);
|
||||
const { data: channelResponseData } = videosResponse ?? {};
|
||||
const videoCount = channelResponseData?.paginate?.total_hits;
|
||||
if (refresh) {
|
||||
const videosResponse = await loadDownloadQueue(
|
||||
currentPage,
|
||||
channelFilterFromUrl,
|
||||
showIgnored,
|
||||
);
|
||||
const { data: channelResponseData } = videosResponse ?? {};
|
||||
const videoCount = channelResponseData?.paginate?.total_hits;
|
||||
|
||||
if (videoCount && lastVideoCount !== videoCount) {
|
||||
setLastVideoCount(videoCount);
|
||||
if (videoCount && lastVideoCount !== videoCount) {
|
||||
setLastVideoCount(videoCount);
|
||||
}
|
||||
|
||||
setDownloadResponse(videosResponse);
|
||||
setRefresh(false);
|
||||
}
|
||||
|
||||
setDownloadResponse(videosResponse);
|
||||
setRefresh(false);
|
||||
})();
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [refresh, showIgnored, currentPage, downloadPending]);
|
||||
}, [refresh]);
|
||||
|
||||
useEffect(() => {
|
||||
setRefresh(true);
|
||||
}, [channelFilterFromUrl, currentPage, showIgnored]);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
@ -125,10 +131,6 @@ const Download = () => {
|
||||
})();
|
||||
}, [lastVideoCount, showIgnored]);
|
||||
|
||||
useEffect(() => {
|
||||
setRefresh(true);
|
||||
}, [channelFilterFromUrl]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<title>TA | Downloads</title>
|
||||
@ -200,19 +202,23 @@ const Download = () => {
|
||||
<Button
|
||||
label="Add to queue"
|
||||
onClick={async () => {
|
||||
await updateDownloadQueue(downloadQueueText, false);
|
||||
setDownloadQueueText('');
|
||||
setRefresh(true);
|
||||
setShowHiddenForm(false);
|
||||
if (downloadQueueText.trim()) {
|
||||
await updateDownloadQueue(downloadQueueText, false);
|
||||
setDownloadQueueText('');
|
||||
setRefresh(true);
|
||||
setShowHiddenForm(false);
|
||||
}
|
||||
}}
|
||||
/>{' '}
|
||||
<Button
|
||||
label="Download now"
|
||||
onClick={async () => {
|
||||
await updateDownloadQueue(downloadQueueText, true);
|
||||
setDownloadQueueText('');
|
||||
setRefresh(true);
|
||||
setShowHiddenForm(false);
|
||||
if (downloadQueueText.trim()) {
|
||||
await updateDownloadQueue(downloadQueueText, true);
|
||||
setDownloadQueueText('');
|
||||
setRefresh(true);
|
||||
setShowHiddenForm(false);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
@ -254,9 +260,11 @@ const Download = () => {
|
||||
onChange={async event => {
|
||||
const value = event.currentTarget.value;
|
||||
|
||||
const params = new URLSearchParams();
|
||||
const params = searchParams;
|
||||
if (value !== 'all') {
|
||||
params.append('channel', value);
|
||||
params.set('channel', value);
|
||||
} else {
|
||||
params.delete('channel');
|
||||
}
|
||||
|
||||
setSearchParams(params);
|
||||
|
@ -134,9 +134,12 @@ const Login = () => {
|
||||
|
||||
{waitingForBackend && (
|
||||
<>
|
||||
<div className="lds-ring" style={{ color: 'var(--accent-font-dark)' }}>
|
||||
<div />
|
||||
</div>
|
||||
<p>
|
||||
Waiting for backend{' '}
|
||||
<div className="lds-ring" style={{ color: 'var(--accent-font-dark)' }}>
|
||||
<div />
|
||||
</div>
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
|
||||
|
@ -102,9 +102,11 @@ const Playlists = () => {
|
||||
label="Subscribe"
|
||||
type="submit"
|
||||
onClick={async () => {
|
||||
await updateBulkPlaylistSubscriptions(playlistsToAddText, true);
|
||||
setShowNotification(true);
|
||||
setShowAddForm(false);
|
||||
if (playlistsToAddText.trim()) {
|
||||
await updateBulkPlaylistSubscriptions(playlistsToAddText, true);
|
||||
setShowNotification(true);
|
||||
setShowAddForm(false);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
@ -125,8 +127,10 @@ const Playlists = () => {
|
||||
label="Create"
|
||||
type="submit"
|
||||
onClick={async () => {
|
||||
await createCustomPlaylist(customPlaylistsToAddText);
|
||||
setRefresh(true);
|
||||
if (customPlaylistsToAddText.trim()) {
|
||||
await createCustomPlaylist(customPlaylistsToAddText);
|
||||
setRefresh(true);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
@ -43,6 +43,7 @@ import { FileSizeUnits } from '../api/actions/updateUserConfig';
|
||||
import { useUserConfigStore } from '../stores/UserConfigStore';
|
||||
import NotFound from './NotFound';
|
||||
import { ApiResponseType } from '../functions/APIClient';
|
||||
import VideoThumbnail from '../components/VideoThumbail';
|
||||
|
||||
const isInPlaylist = (videoId: string, playlist: PlaylistType) => {
|
||||
return playlist.playlist_entries.some(entry => {
|
||||
@ -534,9 +535,8 @@ const Video = () => {
|
||||
{playlistItem.playlist_previous && (
|
||||
<>
|
||||
<Link to={Routes.Video(playlistItem.playlist_previous.youtube_id)}>
|
||||
<img
|
||||
src={`${getApiUrl()}/${playlistItem.playlist_previous.vid_thumb}`}
|
||||
alt="previous thumbnail"
|
||||
<VideoThumbnail
|
||||
videoThumbUrl={playlistItem.playlist_previous.vid_thumb}
|
||||
/>
|
||||
</Link>
|
||||
<div className="playlist-desc">
|
||||
@ -564,10 +564,7 @@ const Video = () => {
|
||||
</Link>
|
||||
</div>
|
||||
<Link to={Routes.Video(playlistItem.playlist_next.youtube_id)}>
|
||||
<img
|
||||
src={`${getApiUrl()}/${playlistItem.playlist_next.vid_thumb}`}
|
||||
alt="previous thumbnail"
|
||||
/>
|
||||
<VideoThumbnail videoThumbUrl={playlistItem.playlist_next.vid_thumb} />
|
||||
</Link>
|
||||
</>
|
||||
)}
|
||||
|
Loading…
Reference in New Issue
Block a user