Refac react frontend (#790)

* Add channel config endpoint

* Add channel aggs

* Add playlist show subscribed only toggle

* Fix refresh always on filterbar toggle

* Add loadingindicator for watchstate change

* Fix missing space in scheduling

* Add schedule request and apprisenotifcation

* Refac upgrade TypeScript target to include 2024 for Object.groupBy

* WIP: Schedule page

* WIP: Schedule page

* Add schedule management ( - notification )

* Fix missing space

* Refac show current selection in input

* Add apprise notifictation url

* Add Stream & Shorts channel pages

* Refac autotarget input on search page

* Fix input requiring 1 instead of 0

* Fix remove unused function

* Chore: npm audit fix

* Refac get channel_overwrites from channelById

* Refac remove defaultvalues form select

* Fix delay content refresh to allow the backend to update subscribed state

* Fix styling selection

* Fix lint

* Fix spelling

* Fix remove unused import

* Chore: update all dependencies - React 19 & vite 6

* Add missing property to ValidatedCookieType

* Refac fix complaints about JSX.Element, used ReactNode instead

* Refac remove unused dependency

* Refac replace react-helmet with react 19 implementation

* Fix Application Settings page

* Chore update dependencies

* Add simple playlist autoplay feature

* Refac use server provided channel images path

* Refac use server provided playlistthumbnail images path

* Add save and restore video playback speed
This commit is contained in:
Merlin
2024-12-22 15:59:30 +01:00
committed by GitHub
parent 75339e479e
commit 5a5d47da9b
54 changed files with 10047 additions and 9526 deletions

View File

@@ -0,0 +1,36 @@
import defaultHeaders from '../../configuration/defaultHeaders';
import getApiUrl from '../../configuration/getApiUrl';
import getFetchCredentials from '../../configuration/getFetchCredentials';
import getCookie from '../../functions/getCookie';
import isDevEnvironment from '../../functions/isDevEnvironment';
export type AppriseTaskNameType =
| 'update_subscribed'
| 'extract_download'
| 'download_pending'
| 'check_reindex';
const createAppriseNotificationUrl = async (taskName: AppriseTaskNameType, url: string) => {
const apiUrl = getApiUrl();
const csrfCookie = getCookie('csrftoken');
const response = await fetch(`${apiUrl}/api/task/notification/`, {
method: 'POST',
headers: {
...defaultHeaders,
'X-CSRFToken': csrfCookie || '',
},
credentials: getFetchCredentials(),
body: JSON.stringify({ task_name: taskName, url }),
});
const appriseNotificationUrl = await response.json();
if (isDevEnvironment()) {
console.log('createAppriseNotificationUrl', appriseNotificationUrl);
}
return appriseNotificationUrl;
};
export default createAppriseNotificationUrl;

View File

@@ -0,0 +1,53 @@
import defaultHeaders from '../../configuration/defaultHeaders';
import getApiUrl from '../../configuration/getApiUrl';
import getFetchCredentials from '../../configuration/getFetchCredentials';
import getCookie from '../../functions/getCookie';
import isDevEnvironment from '../../functions/isDevEnvironment';
export type TaskScheduleNameType =
| 'update_subscribed'
| 'download_pending'
| 'extract_download'
| 'check_reindex'
| 'manual_import'
| 'run_backup'
| 'restore_backup'
| 'rescan_filesystem'
| 'thumbnail_check'
| 'resync_thumbs'
| 'index_playlists'
| 'subscribe_to'
| 'version_check';
type ScheduleConfigType = {
schedule?: string;
config?: {
days?: number;
rotate?: number;
};
};
const createTaskSchedule = async (taskName: TaskScheduleNameType, schedule: ScheduleConfigType) => {
const apiUrl = getApiUrl();
const csrfCookie = getCookie('csrftoken');
const response = await fetch(`${apiUrl}/api/task/schedule/${taskName}/`, {
method: 'POST',
headers: {
...defaultHeaders,
'X-CSRFToken': csrfCookie || '',
},
credentials: getFetchCredentials(),
body: JSON.stringify(schedule),
});
const scheduledTask = await response.json();
if (isDevEnvironment()) {
console.log('createTaskSchedule', scheduledTask);
}
return scheduledTask;
};
export default createTaskSchedule;

View File

@@ -0,0 +1,36 @@
import defaultHeaders from '../../configuration/defaultHeaders';
import getApiUrl from '../../configuration/getApiUrl';
import getFetchCredentials from '../../configuration/getFetchCredentials';
import getCookie from '../../functions/getCookie';
import isDevEnvironment from '../../functions/isDevEnvironment';
type AppriseTaskNameType =
| 'update_subscribed'
| 'extract_download'
| 'download_pending'
| 'check_reindex';
const deleteAppriseNotificationUrl = async (taskName: AppriseTaskNameType) => {
const apiUrl = getApiUrl();
const csrfCookie = getCookie('csrftoken');
const response = await fetch(`${apiUrl}/api/task/notification/`, {
method: 'DELETE',
headers: {
...defaultHeaders,
'X-CSRFToken': csrfCookie || '',
},
credentials: getFetchCredentials(),
body: JSON.stringify({ task_name: taskName }),
});
const appriseNotification = await response.json();
if (isDevEnvironment()) {
console.log('deleteAppriseNotificationUrl', appriseNotification);
}
return appriseNotification;
};
export default deleteAppriseNotificationUrl;

View File

@@ -0,0 +1,30 @@
import defaultHeaders from '../../configuration/defaultHeaders';
import getApiUrl from '../../configuration/getApiUrl';
import getFetchCredentials from '../../configuration/getFetchCredentials';
import getCookie from '../../functions/getCookie';
import isDevEnvironment from '../../functions/isDevEnvironment';
import { TaskScheduleNameType } from './createTaskSchedule';
const deleteTaskSchedule = async (taskName: TaskScheduleNameType) => {
const apiUrl = getApiUrl();
const csrfCookie = getCookie('csrftoken');
const response = await fetch(`${apiUrl}/api/task/schedule/${taskName}/`, {
method: 'DELETE',
headers: {
...defaultHeaders,
'X-CSRFToken': csrfCookie || '',
},
credentials: getFetchCredentials(),
});
const scheduledTask = await response.json();
if (isDevEnvironment()) {
console.log('deleteTaskSchedule', scheduledTask);
}
return scheduledTask;
};
export default deleteTaskSchedule;

View File

@@ -0,0 +1,39 @@
import defaultHeaders from '../../configuration/defaultHeaders';
import getApiUrl from '../../configuration/getApiUrl';
import getFetchCredentials from '../../configuration/getFetchCredentials';
import getCookie from '../../functions/getCookie';
export type ChannelAboutConfigType = {
index_playlists?: boolean;
download_format?: boolean | string;
autodelete_days?: boolean | number;
integrate_sponsorblock?: boolean | null;
subscriptions_channel_size?: number;
subscriptions_live_channel_size?: number;
subscriptions_shorts_channel_size?: number;
};
const updateChannelSettings = async (channelId: string, config: ChannelAboutConfigType) => {
const apiUrl = getApiUrl();
const csrfCookie = getCookie('csrftoken');
const response = await fetch(`${apiUrl}/api/channel/${channelId}/`, {
method: 'POST',
headers: {
...defaultHeaders,
'X-CSRFToken': csrfCookie || '',
},
credentials: getFetchCredentials(),
body: JSON.stringify({
channel_overwrites: config,
}),
});
const channelSubscription = await response.json();
console.log('updateChannelSettings', channelSubscription);
return channelSubscription;
};
export default updateChannelSettings;

View File

@@ -1,32 +1,33 @@
import defaultHeaders from '../../configuration/defaultHeaders';
import getApiUrl from '../../configuration/getApiUrl';
import getFetchCredentials from '../../configuration/getFetchCredentials';
import getCookie from '../../functions/getCookie';
export type ValidatedCookieType = {
cookie_enabled: boolean;
status: boolean;
validated: number;
validated_str: string;
};
const updateCookie = async (): Promise<ValidatedCookieType> => {
const apiUrl = getApiUrl();
const csrfCookie = getCookie('csrftoken');
const response = await fetch(`${apiUrl}/api/appsettings/cookie/`, {
method: 'POST',
headers: {
...defaultHeaders,
'X-CSRFToken': csrfCookie || '',
},
credentials: getFetchCredentials(),
});
const validatedCookie = await response.json();
console.log('updateCookie', validatedCookie);
return validatedCookie;
};
export default updateCookie;
import defaultHeaders from '../../configuration/defaultHeaders';
import getApiUrl from '../../configuration/getApiUrl';
import getFetchCredentials from '../../configuration/getFetchCredentials';
import getCookie from '../../functions/getCookie';
export type ValidatedCookieType = {
cookie_enabled: boolean;
status: boolean;
validated: number;
validated_str: string;
cookie_validated?: boolean;
};
const updateCookie = async (): Promise<ValidatedCookieType> => {
const apiUrl = getApiUrl();
const csrfCookie = getCookie('csrftoken');
const response = await fetch(`${apiUrl}/api/appsettings/cookie/`, {
method: 'POST',
headers: {
...defaultHeaders,
'X-CSRFToken': csrfCookie || '',
},
credentials: getFetchCredentials(),
});
const validatedCookie = await response.json();
console.log('updateCookie', validatedCookie);
return validatedCookie;
};
export default updateCookie;

View File

@@ -1,32 +1,36 @@
import defaultHeaders from '../../configuration/defaultHeaders';
import getApiUrl from '../../configuration/getApiUrl';
import getFetchCredentials from '../../configuration/getFetchCredentials';
import getCookie from '../../functions/getCookie';
import isDevEnvironment from '../../functions/isDevEnvironment';
type ApiTokenResponse = {
token: string;
};
const loadApiToken = async (): Promise<ApiTokenResponse> => {
const apiUrl = getApiUrl();
const csrfCookie = getCookie('csrftoken');
const response = await fetch(`${apiUrl}/api/appsettings/token/`, {
headers: {
...defaultHeaders,
'X-CSRFToken': csrfCookie || '',
},
credentials: getFetchCredentials(),
});
const apiToken = await response.json();
if (isDevEnvironment()) {
console.log('loadApiToken', apiToken);
}
return apiToken;
};
export default loadApiToken;
import defaultHeaders from '../../configuration/defaultHeaders';
import getApiUrl from '../../configuration/getApiUrl';
import getFetchCredentials from '../../configuration/getFetchCredentials';
import getCookie from '../../functions/getCookie';
import isDevEnvironment from '../../functions/isDevEnvironment';
type ApiTokenResponse = {
token: string;
};
const loadApiToken = async (): Promise<ApiTokenResponse> => {
const apiUrl = getApiUrl();
const csrfCookie = getCookie('csrftoken');
try {
const response = await fetch(`${apiUrl}/api/appsettings/token/`, {
headers: {
...defaultHeaders,
'X-CSRFToken': csrfCookie || '',
},
credentials: getFetchCredentials(),
});
const apiToken = await response.json();
if (isDevEnvironment()) {
console.log('loadApiToken', apiToken);
}
return apiToken;
} catch (e) {
return { token: '' };
}
};
export default loadApiToken;

View File

@@ -0,0 +1,42 @@
import defaultHeaders from '../../configuration/defaultHeaders';
import getApiUrl from '../../configuration/getApiUrl';
import getFetchCredentials from '../../configuration/getFetchCredentials';
import isDevEnvironment from '../../functions/isDevEnvironment';
export type AppriseNotificationType = {
check_reindex?: {
urls: string[];
title: string;
};
download_pending?: {
urls: string[];
title: string;
};
extract_download?: {
urls: string[];
title: string;
};
update_subscribed?: {
urls: string[];
title: string;
};
};
const loadAppriseNotification = async (): Promise<AppriseNotificationType> => {
const apiUrl = getApiUrl();
const response = await fetch(`${apiUrl}/api/task/notification/`, {
headers: defaultHeaders,
credentials: getFetchCredentials(),
});
const notification = await response.json();
if (isDevEnvironment()) {
console.log('loadAppriseNotification', notification);
}
return notification;
};
export default loadAppriseNotification;

View File

@@ -1,54 +1,54 @@
import defaultHeaders from '../../configuration/defaultHeaders';
import getApiUrl from '../../configuration/getApiUrl';
import getFetchCredentials from '../../configuration/getFetchCredentials';
import isDevEnvironment from '../../functions/isDevEnvironment';
export type AppSettingsConfigType = {
subscriptions: {
channel_size: number;
live_channel_size: number;
shorts_channel_size: number;
auto_start: boolean;
};
downloads: {
limit_speed: boolean | number;
sleep_interval: number;
autodelete_days: boolean | number;
format: boolean | string;
format_sort: boolean | string;
add_metadata: boolean;
add_thumbnail: boolean;
subtitle: boolean | string;
subtitle_source: boolean | string;
subtitle_index: boolean;
comment_max: boolean | number;
comment_sort: string;
cookie_import: boolean;
throttledratelimit: boolean | number;
extractor_lang: boolean | string;
integrate_ryd: boolean;
integrate_sponsorblock: boolean;
};
application: {
enable_snapshot: boolean;
};
};
const loadAppsettingsConfig = async (): Promise<AppSettingsConfigType> => {
const apiUrl = getApiUrl();
const response = await fetch(`${apiUrl}/api/appsettings/config/`, {
headers: defaultHeaders,
credentials: getFetchCredentials(),
});
const appSettingsConfig = await response.json();
if (isDevEnvironment()) {
console.log('loadApplicationConfig', appSettingsConfig);
}
return appSettingsConfig;
};
export default loadAppsettingsConfig;
import defaultHeaders from '../../configuration/defaultHeaders';
import getApiUrl from '../../configuration/getApiUrl';
import getFetchCredentials from '../../configuration/getFetchCredentials';
import isDevEnvironment from '../../functions/isDevEnvironment';
export type AppSettingsConfigType = {
subscriptions: {
channel_size: number;
live_channel_size: number;
shorts_channel_size: number;
auto_start: boolean;
};
downloads: {
limit_speed: false | number;
sleep_interval: number;
autodelete_days: number;
format: number | string;
format_sort: boolean | string;
add_metadata: boolean;
add_thumbnail: boolean;
subtitle: boolean | string;
subtitle_source: boolean | string;
subtitle_index: boolean;
comment_max: string | number;
comment_sort: string;
cookie_import: boolean;
throttledratelimit: false | number;
extractor_lang: boolean | string;
integrate_ryd: boolean;
integrate_sponsorblock: boolean;
};
application: {
enable_snapshot: boolean;
};
};
const loadAppsettingsConfig = async (): Promise<AppSettingsConfigType> => {
const apiUrl = getApiUrl();
const response = await fetch(`${apiUrl}/api/appsettings/config/`, {
headers: defaultHeaders,
credentials: getFetchCredentials(),
});
const appSettingsConfig = await response.json();
if (isDevEnvironment()) {
console.log('loadApplicationConfig', appSettingsConfig);
}
return appSettingsConfig;
};
export default loadAppsettingsConfig;

View File

@@ -0,0 +1,36 @@
import defaultHeaders from '../../configuration/defaultHeaders';
import getApiUrl from '../../configuration/getApiUrl';
import getFetchCredentials from '../../configuration/getFetchCredentials';
import isDevEnvironment from '../../functions/isDevEnvironment';
export type ChannelAggsType = {
total_items: {
value: number;
};
total_size: {
value: number;
};
total_duration: {
value: number;
value_str: string;
};
};
const loadChannelAggs = async (channelId: string): Promise<ChannelAggsType> => {
const apiUrl = getApiUrl();
const response = await fetch(`${apiUrl}/api/channel/${channelId}/aggs/`, {
headers: defaultHeaders,
credentials: getFetchCredentials(),
});
const channel = await response.json();
if (isDevEnvironment()) {
console.log('loadChannelAggs', channel);
}
return channel;
};
export default loadChannelAggs;

View File

@@ -0,0 +1,36 @@
import defaultHeaders from '../../configuration/defaultHeaders';
import getApiUrl from '../../configuration/getApiUrl';
import getFetchCredentials from '../../configuration/getFetchCredentials';
import isDevEnvironment from '../../functions/isDevEnvironment';
type ScheduleType = {
name: string;
schedule: string;
schedule_human: string;
last_run_at: string;
config: {
days?: number;
rotate?: number;
};
};
export type ScheduleResponseType = ScheduleType[];
const loadSchedule = async (): Promise<ScheduleResponseType> => {
const apiUrl = getApiUrl();
const response = await fetch(`${apiUrl}/api/task/schedule/`, {
headers: defaultHeaders,
credentials: getFetchCredentials(),
});
const schedule = await response.json();
if (isDevEnvironment()) {
console.log('loadSchedule', schedule);
}
return schedule;
};
export default loadSchedule;

View File

@@ -1,74 +1,74 @@
import defaultHeaders from '../../configuration/defaultHeaders';
import getApiUrl from '../../configuration/getApiUrl';
import getFetchCredentials from '../../configuration/getFetchCredentials';
import isDevEnvironment from '../../functions/isDevEnvironment';
import { ConfigType, SortByType, SortOrderType, VideoType } from '../../pages/Home';
import { PaginationType } from '../../components/Pagination';
export type VideoListByFilterResponseType = {
data?: VideoType[];
config?: ConfigType;
paginate?: PaginationType;
};
type WatchTypes = 'watched' | 'unwatched' | 'continue';
type VideoTypes = 'videos' | 'streams' | 'shorts';
type FilterType = {
page?: number;
playlist?: string;
channel?: string;
watch?: WatchTypes;
sort?: SortByType;
order?: SortOrderType;
type?: VideoTypes;
};
const loadVideoListByFilter = async (
filter: FilterType,
): Promise<VideoListByFilterResponseType> => {
const apiUrl = getApiUrl();
const searchParams = new URLSearchParams();
if (filter.page) {
searchParams.append('page', filter.page.toString());
}
if (filter.playlist) {
searchParams.append('playlist', filter.playlist);
} else if (filter.channel) {
searchParams.append('channel', filter.channel);
}
if (filter.watch) {
searchParams.append('watch', filter.watch);
}
if (filter.sort) {
searchParams.append('sort', filter.sort);
}
if (filter.order) {
searchParams.append('order', filter.order);
}
if (filter.type) {
searchParams.append('type', filter.type);
}
const response = await fetch(`${apiUrl}/api/video/?${searchParams.toString()}`, {
headers: defaultHeaders,
credentials: getFetchCredentials(),
});
const videos = await response.json();
if (isDevEnvironment()) {
console.log('loadVideoListByFilter', filter, videos);
}
return videos;
};
export default loadVideoListByFilter;
import defaultHeaders from '../../configuration/defaultHeaders';
import getApiUrl from '../../configuration/getApiUrl';
import getFetchCredentials from '../../configuration/getFetchCredentials';
import isDevEnvironment from '../../functions/isDevEnvironment';
import { ConfigType, SortByType, SortOrderType, VideoType } from '../../pages/Home';
import { PaginationType } from '../../components/Pagination';
export type VideoListByFilterResponseType = {
data?: VideoType[];
config?: ConfigType;
paginate?: PaginationType;
};
type WatchTypes = 'watched' | 'unwatched' | 'continue';
export type VideoTypes = 'videos' | 'streams' | 'shorts';
type FilterType = {
page?: number;
playlist?: string;
channel?: string;
watch?: WatchTypes;
sort?: SortByType;
order?: SortOrderType;
type?: VideoTypes;
};
const loadVideoListByFilter = async (
filter: FilterType,
): Promise<VideoListByFilterResponseType> => {
const apiUrl = getApiUrl();
const searchParams = new URLSearchParams();
if (filter.page) {
searchParams.append('page', filter.page.toString());
}
if (filter.playlist) {
searchParams.append('playlist', filter.playlist);
} else if (filter.channel) {
searchParams.append('channel', filter.channel);
}
if (filter.watch) {
searchParams.append('watch', filter.watch);
}
if (filter.sort) {
searchParams.append('sort', filter.sort);
}
if (filter.order) {
searchParams.append('order', filter.order);
}
if (filter.type) {
searchParams.append('type', filter.type);
}
const response = await fetch(`${apiUrl}/api/video/?${searchParams.toString()}`, {
headers: defaultHeaders,
credentials: getFetchCredentials(),
});
const videos = await response.json();
if (isDevEnvironment()) {
console.log('loadVideoListByFilter', filter, videos);
}
return videos;
};
export default loadVideoListByFilter;