Player URL fixes, #build

Changed:
- Fix player url query persistence
- Fix various search params handling
- Bumped yt-dlp
This commit is contained in:
Simon 2025-04-04 06:03:01 +02:00
commit 253571b5ac
No known key found for this signature in database
GPG Key ID: 2C15AA5E89985DD4
25 changed files with 494 additions and 424 deletions

View File

@ -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 = {

View File

@ -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

View File

@ -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

File diff suppressed because it is too large Load Diff

View File

@ -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"
}
}

View File

@ -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() },
});
};

View File

@ -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 });

View File

@ -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 });

View File

@ -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' });

View File

@ -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

View File

@ -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

View File

@ -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>}

View File

@ -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;
});
}}
/>

View File

@ -40,7 +40,7 @@ const Notifications = ({
}
setNotificationResponse(notifications);
}, 500);
}, 1000);
return () => {
clearInterval(intervalId);

View File

@ -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(() => {

View File

@ -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

View File

@ -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" />

View File

@ -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

View 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;

View File

@ -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;
});

View File

@ -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>

View File

@ -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);

View File

@ -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>
</>
)}

View File

@ -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>

View File

@ -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>
</>
)}