run pre-commit on all

This commit is contained in:
Simon 2025-01-06 21:08:51 +07:00
parent cf54f6d7fc
commit bc74bf80f4
No known key found for this signature in database
GPG Key ID: 2C15AA5E89985DD4
58 changed files with 8962 additions and 8942 deletions

View File

@ -18,4 +18,4 @@ venv/
assets/*
# for local testing only
testing.sh
testing.sh

2
.gitattributes vendored
View File

@ -1 +1 @@
docker_assets\run.sh eol=lf
docker_assets\run.sh eol=lf

2
.github/FUNDING.yml vendored
View File

@ -1,3 +1,3 @@
github: bbilly1
ko_fi: bbilly1
custom: https://paypal.me/bbilly1
custom: https://paypal.me/bbilly1

View File

@ -6,7 +6,7 @@ body:
- type: checkboxes
id: block
attributes:
label: "This project doesn't accept any new feature requests for the forseeable future. There is no shortage of ideas and the next development steps are clear for years to come."
label: "This project doesn't accept any new feature requests for the foreseeable future. There is no shortage of ideas and the next development steps are clear for years to come."
options:
- label: I understand that this issue will be closed without comment.
required: true

48
.pre-commit-config.yaml Normal file
View File

@ -0,0 +1,48 @@
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v5.0.0
hooks:
- id: end-of-file-fixer
- repo: https://github.com/psf/black
rev: 24.10.0
hooks:
- id: black
alias: python
files: ^backend/
args: ["--line-length=79"]
- repo: https://github.com/pycqa/isort
rev: 5.13.2
hooks:
- id: isort
name: isort (python)
alias: python
files: ^backend/
args: ["--profile", "black", "-l 79"]
- repo: https://github.com/pycqa/flake8
rev: 7.1.1
hooks:
- id: flake8
alias: python
files: ^backend/
args: [ "--max-complexity=10", "--max-line-length=79" ]
- repo: https://github.com/codespell-project/codespell
rev: v2.3.0
hooks:
- id: codespell
exclude: ^frontend/package-lock.json
# - repo: https://github.com/pre-commit/mirrors-eslint
# rev: v9.17.0
# hooks:
# - id: eslint
# name: eslint
# entry: npm run --prefix ./frontend lint
# pass_filenames: false
- repo: https://github.com/pre-commit/mirrors-prettier
rev: v4.0.0-alpha.8
hooks:
- id: prettier
entry: npm run --prefix ./frontend prettier
args: ["--write", "."]
pass_filenames: false
exclude: '.*(\.svg|/migrations/).*'

View File

@ -684,4 +684,4 @@
}
}
]
}
}

View File

@ -1,9 +1,6 @@
-r requirements.txt
black==24.10.0
codespell==2.3.0
flake8==7.1.1
ipython==8.31.0
isort==5.13.2
pre-commit==4.0.1
pylint-django==2.6.1
pylint==3.3.3
pytest-django==4.9.0

View File

@ -25,7 +25,10 @@ class Migration(migrations.Migration):
verbose_name="ID",
),
),
("password", models.CharField(max_length=128, verbose_name="password")),
(
"password",
models.CharField(max_length=128, verbose_name="password"),
),
(
"last_login",
models.DateTimeField(

View File

@ -53,4 +53,4 @@ server {
location / {
try_files $uri $uri/ /index.html =404;
}
}
}

View File

@ -2,4 +2,4 @@
build
dist
coverage
node_modules
node_modules

View File

@ -1,26 +1,26 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="apple-touch-icon" sizes="180x180" href="/favicon/apple-touch-icon.png" />
<link rel="icon" type="image/png" sizes="32x32" href="/favicon/favicon-32x32.png" />
<link rel="icon" type="image/png" sizes="16x16" href="/favicon/favicon-16x16.png" />
<link rel="manifest" href="/favicon/site.webmanifest" />
<link rel="mask-icon" href="/favicon/safari-pinned-tab.svg" color="#01202e" />
<link rel="shortcut icon" href="/favicon/favicon.ico" />
<meta name="apple-mobile-web-app-title" content="TubeArchivist" />
<meta name="application-name" content="TubeArchivist" />
<meta name="msapplication-TileColor" content="#01202e" />
<meta name="msapplication-config" content="/favicon/browserconfig.xml" />
<meta name="theme-color" content="#01202e" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>TubeArchivist</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="apple-touch-icon" sizes="180x180" href="/favicon/apple-touch-icon.png" />
<link rel="icon" type="image/png" sizes="32x32" href="/favicon/favicon-32x32.png" />
<link rel="icon" type="image/png" sizes="16x16" href="/favicon/favicon-16x16.png" />
<link rel="manifest" href="/favicon/site.webmanifest" />
<link rel="mask-icon" href="/favicon/safari-pinned-tab.svg" color="#01202e" />
<link rel="shortcut icon" href="/favicon/favicon.ico" />
<meta name="apple-mobile-web-app-title" content="TubeArchivist" />
<meta name="application-name" content="TubeArchivist" />
<meta name="msapplication-TileColor" content="#01202e" />
<meta name="msapplication-config" content="/favicon/browserconfig.xml" />
<meta name="theme-color" content="#01202e" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>TubeArchivist</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

File diff suppressed because it is too large Load Diff

View File

@ -1,33 +1,34 @@
{
"name": "tubearchivist-frontend",
"version": "0.1.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"build:deploy": "vite build",
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
"preview": "vite preview"
},
"dependencies": {
"dompurify": "^3.2.3",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-router-dom": "^7.0.2",
"zustand": "^5.0.2"
},
"devDependencies": {
"@types/react": "^19.0.1",
"@types/react-dom": "^19.0.2",
"@typescript-eslint/eslint-plugin": "^8.18.0",
"@typescript-eslint/parser": "^8.18.0",
"@vitejs/plugin-react-swc": "^3.7.2",
"eslint": "^9.16.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-react-hooks": "^5.1.0",
"eslint-plugin-react-refresh": "^0.4.16",
"prettier": "3.4.2",
"typescript": "^5.7.2",
"vite": "^6.0.3"
}
}
{
"name": "tubearchivist-frontend",
"version": "0.1.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"build:deploy": "vite build",
"lint": "eslint . --report-unused-disable-directives --max-warnings 0",
"prettier": "prettier --write .",
"preview": "vite preview"
},
"dependencies": {
"dompurify": "^3.2.3",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-router-dom": "^7.0.2",
"zustand": "^5.0.2"
},
"devDependencies": {
"@types/react": "^19.0.1",
"@types/react-dom": "^19.0.2",
"@typescript-eslint/eslint-plugin": "^8.18.0",
"@typescript-eslint/parser": "^8.18.0",
"@vitejs/plugin-react-swc": "^3.7.2",
"eslint": "^9.16.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-react-hooks": "^5.1.0",
"eslint-plugin-react-refresh": "^0.4.16",
"prettier": "3.4.2",
"typescript": "^5.7.2",
"vite": "^6.0.3"
}
}

View File

@ -1,42 +1,42 @@
import defaultHeaders from '../../configuration/defaultHeaders';
import getApiUrl from '../../configuration/getApiUrl';
import getFetchCredentials from '../../configuration/getFetchCredentials';
import getCookie from '../../functions/getCookie';
const updateChannelSubscription = async (channelIds: string, status: boolean) => {
const apiUrl = getApiUrl();
const csrfCookie = getCookie('csrftoken');
const channels = [];
const containsMultiple = channelIds.includes('\n');
if (containsMultiple) {
const youtubeChannelIds = channelIds.split('\n');
youtubeChannelIds.forEach(channelId => {
channels.push({ channel_id: channelId, channel_subscribed: status });
});
} else {
channels.push({ channel_id: channelIds, channel_subscribed: status });
}
const response = await fetch(`${apiUrl}/api/channel/`, {
method: 'POST',
headers: {
...defaultHeaders,
'X-CSRFToken': csrfCookie || '',
},
credentials: getFetchCredentials(),
body: JSON.stringify({
data: [...channels],
}),
});
const channelSubscription = await response.json();
console.log('updateChannelSubscription', channelSubscription);
return channelSubscription;
};
export default updateChannelSubscription;
import defaultHeaders from '../../configuration/defaultHeaders';
import getApiUrl from '../../configuration/getApiUrl';
import getFetchCredentials from '../../configuration/getFetchCredentials';
import getCookie from '../../functions/getCookie';
const updateChannelSubscription = async (channelIds: string, status: boolean) => {
const apiUrl = getApiUrl();
const csrfCookie = getCookie('csrftoken');
const channels = [];
const containsMultiple = channelIds.includes('\n');
if (containsMultiple) {
const youtubeChannelIds = channelIds.split('\n');
youtubeChannelIds.forEach(channelId => {
channels.push({ channel_id: channelId, channel_subscribed: status });
});
} else {
channels.push({ channel_id: channelIds, channel_subscribed: status });
}
const response = await fetch(`${apiUrl}/api/channel/`, {
method: 'POST',
headers: {
...defaultHeaders,
'X-CSRFToken': csrfCookie || '',
},
credentials: getFetchCredentials(),
body: JSON.stringify({
data: [...channels],
}),
});
const channelSubscription = await response.json();
console.log('updateChannelSubscription', channelSubscription);
return channelSubscription;
};
export default updateChannelSubscription;

View File

@ -1,33 +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;
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;
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,47 +1,47 @@
import defaultHeaders from '../../configuration/defaultHeaders';
import getApiUrl from '../../configuration/getApiUrl';
import getFetchCredentials from '../../configuration/getFetchCredentials';
import getCookie from '../../functions/getCookie';
const updateDownloadQueue = async (youtubeIdStrings: string, autostart: boolean) => {
const apiUrl = getApiUrl();
const csrfCookie = getCookie('csrftoken');
const urls = [];
const containsMultiple = youtubeIdStrings.includes('\n');
if (containsMultiple) {
const youtubeIds = youtubeIdStrings.split('\n');
youtubeIds.forEach(youtubeId => {
urls.push({ youtube_id: youtubeId, status: 'pending' });
});
} else {
urls.push({ youtube_id: youtubeIdStrings, status: 'pending' });
}
let params = '';
if (autostart) {
params = '?autostart=true';
}
const response = await fetch(`${apiUrl}/api/download/${params}`, {
method: 'POST',
headers: {
...defaultHeaders,
'X-CSRFToken': csrfCookie || '',
},
credentials: getFetchCredentials(),
body: JSON.stringify({
data: [...urls],
}),
});
const downloadState = await response.json();
console.log('updateDownloadQueue', downloadState);
return downloadState;
};
export default updateDownloadQueue;
import defaultHeaders from '../../configuration/defaultHeaders';
import getApiUrl from '../../configuration/getApiUrl';
import getFetchCredentials from '../../configuration/getFetchCredentials';
import getCookie from '../../functions/getCookie';
const updateDownloadQueue = async (youtubeIdStrings: string, autostart: boolean) => {
const apiUrl = getApiUrl();
const csrfCookie = getCookie('csrftoken');
const urls = [];
const containsMultiple = youtubeIdStrings.includes('\n');
if (containsMultiple) {
const youtubeIds = youtubeIdStrings.split('\n');
youtubeIds.forEach(youtubeId => {
urls.push({ youtube_id: youtubeId, status: 'pending' });
});
} else {
urls.push({ youtube_id: youtubeIdStrings, status: 'pending' });
}
let params = '';
if (autostart) {
params = '?autostart=true';
}
const response = await fetch(`${apiUrl}/api/download/${params}`, {
method: 'POST',
headers: {
...defaultHeaders,
'X-CSRFToken': csrfCookie || '',
},
credentials: getFetchCredentials(),
body: JSON.stringify({
data: [...urls],
}),
});
const downloadState = await response.json();
console.log('updateDownloadQueue', downloadState);
return downloadState;
};
export default updateDownloadQueue;

View File

@ -1,42 +1,42 @@
import defaultHeaders from '../../configuration/defaultHeaders';
import getApiUrl from '../../configuration/getApiUrl';
import getFetchCredentials from '../../configuration/getFetchCredentials';
import getCookie from '../../functions/getCookie';
const updatePlaylistSubscription = async (playlistIds: string, status: boolean) => {
const apiUrl = getApiUrl();
const csrfCookie = getCookie('csrftoken');
const playlists = [];
const containsMultiple = playlistIds.includes('\n');
if (containsMultiple) {
const youtubePlaylistIds = playlistIds.split('\n');
youtubePlaylistIds.forEach(playlistId => {
playlists.push({ playlist_id: playlistId, playlist_subscribed: status });
});
} else {
playlists.push({ playlist_id: playlistIds, playlist_subscribed: status });
}
const response = await fetch(`${apiUrl}/api/playlist/`, {
method: 'POST',
headers: {
...defaultHeaders,
'X-CSRFToken': csrfCookie || '',
},
credentials: getFetchCredentials(),
body: JSON.stringify({
data: [...playlists],
}),
});
const playlistSubscription = await response.json();
console.log('updatePlaylistSubscription', playlistSubscription);
return playlistSubscription;
};
export default updatePlaylistSubscription;
import defaultHeaders from '../../configuration/defaultHeaders';
import getApiUrl from '../../configuration/getApiUrl';
import getFetchCredentials from '../../configuration/getFetchCredentials';
import getCookie from '../../functions/getCookie';
const updatePlaylistSubscription = async (playlistIds: string, status: boolean) => {
const apiUrl = getApiUrl();
const csrfCookie = getCookie('csrftoken');
const playlists = [];
const containsMultiple = playlistIds.includes('\n');
if (containsMultiple) {
const youtubePlaylistIds = playlistIds.split('\n');
youtubePlaylistIds.forEach(playlistId => {
playlists.push({ playlist_id: playlistId, playlist_subscribed: status });
});
} else {
playlists.push({ playlist_id: playlistIds, playlist_subscribed: status });
}
const response = await fetch(`${apiUrl}/api/playlist/`, {
method: 'POST',
headers: {
...defaultHeaders,
'X-CSRFToken': csrfCookie || '',
},
credentials: getFetchCredentials(),
body: JSON.stringify({
data: [...playlists],
}),
});
const playlistSubscription = await response.json();
console.log('updatePlaylistSubscription', playlistSubscription);
return playlistSubscription;
};
export default updatePlaylistSubscription;

View File

@ -1,36 +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');
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;
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

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

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

View File

@ -1,42 +1,42 @@
import { ReactNode } from 'react';
export interface ButtonProps {
id?: string;
name?: string;
className?: string;
type?: 'submit' | 'reset' | 'button' | undefined;
label?: string | ReactNode | ReactNode[];
children?: string | ReactNode | ReactNode[];
value?: string;
title?: string;
onClick?: () => void;
}
const Button = ({
id,
name,
className,
type,
label,
children,
value,
title,
onClick,
}: ButtonProps) => {
return (
<button
id={id}
name={name}
className={className}
type={type}
value={value}
title={title}
onClick={onClick}
>
{label}
{children}
</button>
);
};
export default Button;
import { ReactNode } from 'react';
export interface ButtonProps {
id?: string;
name?: string;
className?: string;
type?: 'submit' | 'reset' | 'button' | undefined;
label?: string | ReactNode | ReactNode[];
children?: string | ReactNode | ReactNode[];
value?: string;
title?: string;
onClick?: () => void;
}
const Button = ({
id,
name,
className,
type,
label,
children,
value,
title,
onClick,
}: ButtonProps) => {
return (
<button
id={id}
name={name}
className={className}
type={type}
value={value}
title={title}
onClick={onClick}
>
{label}
{children}
</button>
);
};
export default Button;

View File

@ -1,22 +1,22 @@
import getApiUrl from '../configuration/getApiUrl';
import defaultChannelImage from '/img/default-channel-banner.jpg';
type ChannelIconProps = {
channelId: string;
channelBannerUrl: string | undefined;
};
const ChannelBanner = ({ channelId, channelBannerUrl }: ChannelIconProps) => {
return (
<img
src={`${getApiUrl()}${channelBannerUrl}`}
alt={`${channelId}-banner`}
onError={({ currentTarget }) => {
currentTarget.onerror = null; // prevents looping
currentTarget.src = defaultChannelImage;
}}
/>
);
};
export default ChannelBanner;
import getApiUrl from '../configuration/getApiUrl';
import defaultChannelImage from '/img/default-channel-banner.jpg';
type ChannelIconProps = {
channelId: string;
channelBannerUrl: string | undefined;
};
const ChannelBanner = ({ channelId, channelBannerUrl }: ChannelIconProps) => {
return (
<img
src={`${getApiUrl()}${channelBannerUrl}`}
alt={`${channelId}-banner`}
onError={({ currentTarget }) => {
currentTarget.onerror = null; // prevents looping
currentTarget.src = defaultChannelImage;
}}
/>
);
};
export default ChannelBanner;

View File

@ -1,22 +1,22 @@
import getApiUrl from '../configuration/getApiUrl';
import defaultChannelIcon from '/img/default-channel-icon.jpg';
type ChannelIconProps = {
channelId: string;
channelThumbUrl: string | undefined;
};
const ChannelIcon = ({ channelId, channelThumbUrl }: ChannelIconProps) => {
return (
<img
src={`${getApiUrl()}${channelThumbUrl}`}
alt={`${channelId}-thumb`}
onError={({ currentTarget }) => {
currentTarget.onerror = null; // prevents looping
currentTarget.src = defaultChannelIcon;
}}
/>
);
};
export default ChannelIcon;
import getApiUrl from '../configuration/getApiUrl';
import defaultChannelIcon from '/img/default-channel-icon.jpg';
type ChannelIconProps = {
channelId: string;
channelThumbUrl: string | undefined;
};
const ChannelIcon = ({ channelId, channelThumbUrl }: ChannelIconProps) => {
return (
<img
src={`${getApiUrl()}${channelThumbUrl}`}
alt={`${channelId}-thumb`}
onError={({ currentTarget }) => {
currentTarget.onerror = null; // prevents looping
currentTarget.src = defaultChannelIcon;
}}
/>
);
};
export default ChannelIcon;

View File

@ -1,98 +1,97 @@
import { Link } from 'react-router-dom';
import { ChannelType } from '../pages/Channels';
import Routes from '../configuration/routes/RouteList';
import updateChannelSubscription from '../api/actions/updateChannelSubscription';
import formatDate from '../functions/formatDates';
import FormattedNumber from './FormattedNumber';
import Button from './Button';
import ChannelIcon from './ChannelIcon';
import ChannelBanner from './ChannelBanner';
import { useUserConfigStore } from '../stores/UserConfigStore';
type ChannelListProps = {
channelList: ChannelType[] | undefined;
refreshChannelList: (refresh: boolean) => void;
};
const ChannelList = ({ channelList, refreshChannelList }: ChannelListProps) => {
const { userConfig } = useUserConfigStore();
const viewLayout = userConfig.config.view_style_channel;
if (!channelList || channelList.length === 0) {
return <p>No channels found.</p>;
}
return (
<>
{channelList.map(channel => {
return (
<div key={channel.channel_id} className={`channel-item ${viewLayout}`}>
<div className={`channel-banner ${viewLayout}`}>
<Link to={Routes.Channel(channel.channel_id)}>
<ChannelBanner
channelId={channel.channel_id}
channelBannerUrl={channel.channel_banner_url}
/>
</Link>
</div>
<div className={`info-box info-box-2 ${viewLayout}`}>
<div className="info-box-item">
<div className="round-img">
<Link to={Routes.Channel(channel.channel_id)}>
<ChannelIcon
channelId={channel.channel_id}
channelThumbUrl={channel.channel_thumb_url}
/>
</Link>
</div>
<div>
<h3>
<Link to={Routes.Channel(channel.channel_id)}>{channel.channel_name}</Link>
</h3>
<FormattedNumber text="Subscribers:" number={channel.channel_subs} />
</div>
</div>
<div className="info-box-item">
<div>
<p>Last refreshed: {formatDate(channel.channel_last_refresh)}</p>
{channel.channel_subscribed && (
<Button
label="Unsubscribe"
className="unsubscribe"
type="button"
title={`Unsubscribe from ${channel.channel_name}`}
onClick={async () => {
await updateChannelSubscription(channel.channel_id, false);
setTimeout(() => {
refreshChannelList(true);
}, 1000);
}}
/>
)}
{!channel.channel_subscribed && (
<Button
label="Subscribe"
type="button"
title={`Subscribe to ${channel.channel_name}`}
onClick={async () => {
await updateChannelSubscription(channel.channel_id, true);
setTimeout(() => {
refreshChannelList(true);
}, 500);
}}
/>
)}
</div>
</div>
</div>
</div>
);
})}
</>
);
};
export default ChannelList;
import { Link } from 'react-router-dom';
import { ChannelType } from '../pages/Channels';
import Routes from '../configuration/routes/RouteList';
import updateChannelSubscription from '../api/actions/updateChannelSubscription';
import formatDate from '../functions/formatDates';
import FormattedNumber from './FormattedNumber';
import Button from './Button';
import ChannelIcon from './ChannelIcon';
import ChannelBanner from './ChannelBanner';
import { useUserConfigStore } from '../stores/UserConfigStore';
type ChannelListProps = {
channelList: ChannelType[] | undefined;
refreshChannelList: (refresh: boolean) => void;
};
const ChannelList = ({ channelList, refreshChannelList }: ChannelListProps) => {
const { userConfig } = useUserConfigStore();
const viewLayout = userConfig.config.view_style_channel;
if (!channelList || channelList.length === 0) {
return <p>No channels found.</p>;
}
return (
<>
{channelList.map(channel => {
return (
<div key={channel.channel_id} className={`channel-item ${viewLayout}`}>
<div className={`channel-banner ${viewLayout}`}>
<Link to={Routes.Channel(channel.channel_id)}>
<ChannelBanner
channelId={channel.channel_id}
channelBannerUrl={channel.channel_banner_url}
/>
</Link>
</div>
<div className={`info-box info-box-2 ${viewLayout}`}>
<div className="info-box-item">
<div className="round-img">
<Link to={Routes.Channel(channel.channel_id)}>
<ChannelIcon
channelId={channel.channel_id}
channelThumbUrl={channel.channel_thumb_url}
/>
</Link>
</div>
<div>
<h3>
<Link to={Routes.Channel(channel.channel_id)}>{channel.channel_name}</Link>
</h3>
<FormattedNumber text="Subscribers:" number={channel.channel_subs} />
</div>
</div>
<div className="info-box-item">
<div>
<p>Last refreshed: {formatDate(channel.channel_last_refresh)}</p>
{channel.channel_subscribed && (
<Button
label="Unsubscribe"
className="unsubscribe"
type="button"
title={`Unsubscribe from ${channel.channel_name}`}
onClick={async () => {
await updateChannelSubscription(channel.channel_id, false);
setTimeout(() => {
refreshChannelList(true);
}, 1000);
}}
/>
)}
{!channel.channel_subscribed && (
<Button
label="Subscribe"
type="button"
title={`Subscribe to ${channel.channel_name}`}
onClick={async () => {
await updateChannelSubscription(channel.channel_id, true);
setTimeout(() => {
refreshChannelList(true);
}, 500);
}}
/>
)}
</div>
</div>
</div>
</div>
);
})}
</>
);
};
export default ChannelList;

View File

@ -1,78 +1,78 @@
import { Link } from 'react-router-dom';
import Routes from '../configuration/routes/RouteList';
import updateChannelSubscription from '../api/actions/updateChannelSubscription';
import FormattedNumber from './FormattedNumber';
import Button from './Button';
import ChannelIcon from './ChannelIcon';
type ChannelOverviewProps = {
channelId: string;
channelname: string;
channelSubs: number;
channelSubscribed: boolean;
channelThumbUrl: string;
showSubscribeButton?: boolean;
isUserAdmin?: boolean;
setRefresh: (status: boolean) => void;
};
const ChannelOverview = ({
channelId,
channelSubs,
channelSubscribed,
channelname,
channelThumbUrl,
showSubscribeButton = false,
isUserAdmin,
setRefresh,
}: ChannelOverviewProps) => {
return (
<>
<div className="info-box-item">
<div className="round-img">
<Link to={Routes.Channel(channelId)}>
<ChannelIcon channelId={channelId} channelThumbUrl={channelThumbUrl} />
</Link>
</div>
<div>
<h3>
<Link to={Routes.ChannelVideo(channelId)}>{channelname}</Link>
</h3>
<FormattedNumber text="Subscribers:" number={channelSubs} />
{showSubscribeButton && (
<>
{channelSubscribed && isUserAdmin && (
<Button
label="Unsubscribe"
className="unsubscribe"
type="button"
title={`Unsubscribe from ${channelname}`}
onClick={async () => {
await updateChannelSubscription(channelId, false);
setRefresh(true);
}}
/>
)}
{!channelSubscribed && (
<Button
label="Subscribe"
type="button"
title={`Subscribe to ${channelname}`}
onClick={async () => {
await updateChannelSubscription(channelId, true);
setRefresh(true);
}}
/>
)}
</>
)}
</div>
</div>
</>
);
};
export default ChannelOverview;
import { Link } from 'react-router-dom';
import Routes from '../configuration/routes/RouteList';
import updateChannelSubscription from '../api/actions/updateChannelSubscription';
import FormattedNumber from './FormattedNumber';
import Button from './Button';
import ChannelIcon from './ChannelIcon';
type ChannelOverviewProps = {
channelId: string;
channelname: string;
channelSubs: number;
channelSubscribed: boolean;
channelThumbUrl: string;
showSubscribeButton?: boolean;
isUserAdmin?: boolean;
setRefresh: (status: boolean) => void;
};
const ChannelOverview = ({
channelId,
channelSubs,
channelSubscribed,
channelname,
channelThumbUrl,
showSubscribeButton = false,
isUserAdmin,
setRefresh,
}: ChannelOverviewProps) => {
return (
<>
<div className="info-box-item">
<div className="round-img">
<Link to={Routes.Channel(channelId)}>
<ChannelIcon channelId={channelId} channelThumbUrl={channelThumbUrl} />
</Link>
</div>
<div>
<h3>
<Link to={Routes.ChannelVideo(channelId)}>{channelname}</Link>
</h3>
<FormattedNumber text="Subscribers:" number={channelSubs} />
{showSubscribeButton && (
<>
{channelSubscribed && isUserAdmin && (
<Button
label="Unsubscribe"
className="unsubscribe"
type="button"
title={`Unsubscribe from ${channelname}`}
onClick={async () => {
await updateChannelSubscription(channelId, false);
setRefresh(true);