dependencies: bump (major)

This commit is contained in:
Jesse Chan
2021-10-23 20:52:11 -07:00
parent cb148f7e09
commit 9c9675df34
30 changed files with 2169 additions and 2398 deletions

View File

@@ -20,7 +20,7 @@
{
"files": ["*.js", "*.jsx"],
"extends": ["prettier"],
"parser": "babel-eslint",
"parser": "@babel/eslint-parser",
"rules": {
"@typescript-eslint/no-var-requires": 0
}

View File

@@ -1,6 +1,5 @@
{
"bracketSpacing": false,
"jsxBracketSameLine": true,
"printWidth": 120,
"singleQuote": true,
"trailingComma": "all"

View File

@@ -1,11 +1,13 @@
import axios, {AxiosError, AxiosResponse} from 'axios';
import axios, {AxiosError} from 'axios';
import AuthStore from '@client/stores/AuthStore';
import ConfigStore from '@client/stores/ConfigStore';
import type {
AuthAuthenticationOptions,
AuthAuthenticationResponse,
AuthRegistrationOptions,
AuthRegistrationResponse,
AuthUpdateUserOptions,
AuthVerificationResponse,
} from '@shared/schema/api/auth';
@@ -20,10 +22,9 @@ const {baseURI} = ConfigStore;
const AuthActions = {
authenticate: (options: AuthAuthenticationOptions) =>
axios
.post(`${baseURI}api/auth/authenticate`, options)
.then((json) => json.data)
.post<AuthAuthenticationResponse>(`${baseURI}api/auth/authenticate`, options)
.then(
(data) => {
({data}) => {
AuthStore.handleLoginSuccess(data);
},
(error) => {
@@ -53,36 +54,27 @@ const AuthActions = {
),
createUser: (options: AuthRegistrationOptions) =>
axios
.post(`${baseURI}api/auth/register?cookie=false`, options)
.then((json) => json.data)
.then((data) => {
AuthStore.handleCreateUserSuccess(data);
}),
axios.post<AuthRegistrationResponse>(`${baseURI}api/auth/register?cookie=false`, options).then(({data}) => {
AuthStore.handleCreateUserSuccess(data);
}),
updateUser: (username: Credentials['username'], options: AuthUpdateUserOptions) =>
axios.patch(`${baseURI}api/auth/users/${encodeURIComponent(username)}`, options).then((json) => json.data),
axios.patch(`${baseURI}api/auth/users/${encodeURIComponent(username)}`, options).then((res) => res.data),
deleteUser: (username: Credentials['username']) =>
axios
.delete(`${baseURI}api/auth/users/${encodeURIComponent(username)}`)
.then((json) => json.data)
.then(
() => {
// do nothing.
},
() => {
// do nothing.
},
),
axios.delete(`${baseURI}api/auth/users/${encodeURIComponent(username)}`).then(
() => {
// do nothing.
},
() => {
// do nothing.
},
),
fetchUsers: () =>
axios
.get(`${baseURI}api/auth/users`)
.then((json) => json.data)
.then((data) => {
AuthStore.handleListUsersSuccess(data);
}),
axios.get<Array<Credentials>>(`${baseURI}api/auth/users`).then(({data}) => {
AuthStore.handleListUsersSuccess(data);
}),
logout: () =>
axios.get(`${baseURI}api/auth/logout`).then(
@@ -95,47 +87,42 @@ const AuthActions = {
),
register: (options: AuthRegistrationOptions) =>
axios
.post(`${baseURI}api/auth/register`, options)
.then((json) => json.data)
.then(
(data) => {
AuthStore.handleRegisterSuccess(data);
},
(error: AxiosError) => {
throw error;
},
),
axios.post<AuthRegistrationResponse>(`${baseURI}api/auth/register`, options).then(
({data}) => {
AuthStore.handleRegisterSuccess(data);
},
(error: AxiosError) => {
throw error;
},
),
verify: () =>
axios
.get(`${baseURI}api/auth/verify?${Date.now()}`)
.get<AuthVerificationResponse>(`${baseURI}api/auth/verify?${Date.now()}`)
.then(
(res: AxiosResponse) => {
if (res.data.configs != null) {
ConfigStore.handlePreloadConfigs(res.data.configs);
({data}) => {
if (data.configs != null) {
ConfigStore.handlePreloadConfigs(data.configs);
}
return res.data;
return data;
},
(error: AxiosError) => {
(error: AxiosError<AuthVerificationResponse>) => {
if (error.response?.data?.configs != null) {
ConfigStore.handlePreloadConfigs(error.response.data.configs);
}
throw error;
},
)
.then(
(data: AuthVerificationResponse) => {
AuthStore.handleAuthVerificationSuccess(data);
.then((data) => {
AuthStore.handleAuthVerificationSuccess(data);
return Promise.all([ClientActions.fetchSettings(), SettingActions.fetchSettings()]).then(() => data);
},
(error) => {
AuthStore.handleAuthVerificationError();
return Promise.all([ClientActions.fetchSettings(), SettingActions.fetchSettings()]).then(() => data);
})
.catch((error) => {
AuthStore.handleAuthVerificationError();
throw error;
},
),
throw error;
}),
} as const;
export default AuthActions;

View File

@@ -11,46 +11,35 @@ const {baseURI} = ConfigStore;
const ClientActions = {
fetchSettings: async (): Promise<void> =>
axios
.get(`${baseURI}api/client/settings`)
.then((json) => json.data)
.then(
(data) => {
SettingStore.handleClientSettingsFetchSuccess(data);
},
() => {
// do nothing.
},
),
axios.get<ClientSettings>(`${baseURI}api/client/settings`).then(
({data}) => {
SettingStore.handleClientSettingsFetchSuccess(data);
},
() => {
// do nothing.
},
),
saveSettings: async (settings: SetClientSettingsOptions, options?: {alert?: boolean}): Promise<void> => {
if (Object.keys(settings).length > 0) {
SettingStore.saveClientSettings(settings);
let err = false;
await axios
.patch(`${baseURI}api/client/settings`, settings)
.then((json) => json.data)
.then(
() => {
// do nothing.
},
() => {
err = true;
},
);
const success = await axios.patch(`${baseURI}api/client/settings`, settings).then(
() => true,
() => false,
);
if (options?.alert) {
// TODO: More precise error message.
AlertStore.add(
err
success
? {
id: 'general.error.unknown',
type: 'error',
}
: {
id: 'alert.settings.saved',
type: 'success',
}
: {
id: 'general.error.unknown',
type: 'error',
},
);
}
@@ -61,12 +50,9 @@ const ClientActions = {
ClientActions.saveSettings({[property]: data}),
testConnection: async (): Promise<void> =>
axios
.get(`${baseURI}api/client/connection-test`)
.then((json) => json.data)
.then(() => {
// do nothing.
}),
axios.get(`${baseURI}api/client/connection-test`).then(() => {
// do nothing.
}),
} as const;
export default ClientActions;

View File

@@ -3,58 +3,40 @@ import axios from 'axios';
import ConfigStore from '@client/stores/ConfigStore';
import FeedStore from '@client/stores/FeedStore';
import type {Feed, Item, Rule} from '@shared/types/Feed';
import type {AddFeedOptions, AddRuleOptions, ModifyFeedOptions} from '@shared/types/api/feed-monitor';
const {baseURI} = ConfigStore;
const FeedActions = {
addFeed: (options: AddFeedOptions) =>
axios
.put(`${baseURI}api/feed-monitor/feeds`, options)
.then((json) => json.data)
.then(() => {
FeedActions.fetchFeedMonitors();
}),
axios.put(`${baseURI}api/feed-monitor/feeds`, options).then(() => FeedActions.fetchFeedMonitors()),
modifyFeed: (id: string, options: ModifyFeedOptions) =>
axios
.patch(`${baseURI}api/feed-monitor/feeds/${id}`, options)
.then((json) => json.data)
.then(() => {
FeedActions.fetchFeedMonitors();
}),
axios.patch(`${baseURI}api/feed-monitor/feeds/${id}`, options).then(() => FeedActions.fetchFeedMonitors()),
addRule: (options: AddRuleOptions) =>
axios
.put(`${baseURI}api/feed-monitor/rules`, options)
.then((json) => json.data)
.then(() => {
FeedActions.fetchFeedMonitors();
}),
axios.put(`${baseURI}api/feed-monitor/rules`, options).then(() => FeedActions.fetchFeedMonitors()),
fetchFeedMonitors: () =>
axios
.get(`${baseURI}api/feed-monitor`)
.then((json) => json.data)
.then(
(data) => {
FeedStore.handleFeedMonitorsFetchSuccess(data);
},
() => {
// do nothing.
},
),
axios.get<{feeds: Array<Feed>; rules: Array<Rule>}>(`${baseURI}api/feed-monitor`).then(
({data}) => {
FeedStore.handleFeedMonitorsFetchSuccess(data);
},
() => {
// do nothing.
},
),
fetchItems: ({id, search}: {id: string; search: string}) =>
axios
.get(`${baseURI}api/feed-monitor/feeds/${id}/items`, {
.get<Item[]>(`${baseURI}api/feed-monitor/feeds/${id}/items`, {
params: {
search,
},
})
.then((json) => json.data)
.then(
(data) => {
({data}) => {
FeedStore.handleItemsFetchSuccess(data);
},
() => {
@@ -63,17 +45,12 @@ const FeedActions = {
),
removeFeedMonitor: (id: string) =>
axios
.delete(`${baseURI}api/feed-monitor/${id}`)
.then((json) => json.data)
.then(
() => {
FeedActions.fetchFeedMonitors();
},
() => {
// do nothing.
},
),
axios.delete(`${baseURI}api/feed-monitor/${id}`).then(
() => FeedActions.fetchFeedMonitors(),
() => {
// do nothing.
},
),
} as const;
export default FeedActions;

View File

@@ -9,8 +9,9 @@ import TorrentStore from '@client/stores/TorrentStore';
import TransferDataStore from '@client/stores/TransferDataStore';
import UIStore from '@client/stores/UIStore';
import type {DirectoryListResponse} from '@shared/types/api';
import type {HistorySnapshot} from '@shared/constants/historySnapshotTypes';
import type {NotificationFetchOptions} from '@shared/types/Notification';
import type {NotificationFetchOptions, NotificationState} from '@shared/types/Notification';
import type {ServerEvents} from '@shared/types/ServerEvents';
interface ActivityStreamOptions {
@@ -70,17 +71,14 @@ const ServerEventHandlers: Record<keyof ServerEvents, (event: unknown) => void>
const FloodActions = {
clearNotifications: () => {
NotificationStore.clearAll();
return axios
.delete(`${baseURI}api/notifications`)
.then((json) => json.data)
.then(
() => {
// do nothing.
},
() => {
// do nothing.
},
);
return axios.delete(`${baseURI}api/notifications`).then(
() => {
// do nothing.
},
() => {
// do nothing.
},
);
},
closeActivityStream() {
@@ -101,19 +99,18 @@ const FloodActions = {
fetchDirectoryList: (path: string) =>
axios
.get(`${baseURI}api/directory-list`, {
.get<DirectoryListResponse>(`${baseURI}api/directory-list`, {
params: {path},
})
.then((json) => json.data),
.then((res) => res.data),
fetchNotifications: (options: NotificationFetchOptions) =>
axios
.get(`${baseURI}api/notifications`, {
.get<NotificationState>(`${baseURI}api/notifications`, {
params: options,
})
.then((json) => json.data)
.then(
(data) => {
({data}) => {
NotificationStore.handleNotificationsFetchSuccess(data);
},
() => {

View File

@@ -11,46 +11,35 @@ const {baseURI} = ConfigStore;
const SettingActions = {
fetchSettings: async (): Promise<void> =>
axios
.get(`${baseURI}api/settings`)
.then((json) => json.data)
.then(
(data) => {
SettingStore.handleSettingsFetchSuccess(data);
},
() => {
// do nothing.
},
),
axios.get<FloodSettings>(`${baseURI}api/settings`).then(
({data}) => {
SettingStore.handleSettingsFetchSuccess(data);
},
() => {
// do nothing.
},
),
saveSettings: async (settings: SetFloodSettingsOptions, options?: {alert?: boolean}): Promise<void> => {
if (Object.keys(settings).length > 0) {
SettingStore.saveFloodSettings(settings);
let err = false;
await axios
.patch(`${baseURI}api/settings`, settings)
.then((json) => json.data)
.then(
() => {
// do nothing.
},
() => {
err = true;
},
);
const success = await axios.patch(`${baseURI}api/settings`, settings).then(
() => true,
() => false,
);
if (options?.alert) {
// TODO: More precise error message.
AlertStore.add(
err
success
? {
id: 'general.error.unknown',
type: 'error',
}
: {
id: 'alert.settings.saved',
type: 'success',
}
: {
id: 'general.error.unknown',
type: 'error',
},
);
}

View File

@@ -11,7 +11,7 @@ import type {
ReannounceTorrentsOptions,
SetTorrentsTagsOptions,
} from '@shared/schema/api/torrents';
import type {
import {
CheckTorrentsOptions,
CreateTorrentOptions,
DeleteTorrentsOptions,
@@ -23,6 +23,8 @@ import type {
SetTorrentsTrackersOptions,
StartTorrentsOptions,
StopTorrentsOptions,
TorrentAddResponse,
TorrentMediainfoResponse,
} from '@shared/types/api/torrents';
import type {TorrentContent} from '@shared/types/TorrentContent';
import type {TorrentPeer} from '@shared/types/TorrentPeer';
@@ -56,43 +58,37 @@ const emitFailedToAddTorrentAlert = (count: number) => {
const TorrentActions = {
addTorrentsByUrls: (options: AddTorrentByURLOptions): Promise<void> =>
axios
.post(`${baseURI}api/torrents/add-urls`, options)
.then((json) => json.data)
.then(
(response) => {
if (response.length) {
emitTorrentAddedAlert(response.length);
} else {
emitRequestSentAlert(options.urls.length);
}
},
() => {
emitFailedToAddTorrentAlert(options.urls.length);
},
),
axios.post<TorrentAddResponse>(`${baseURI}api/torrents/add-urls`, options).then(
({data}) => {
if (data.length) {
emitTorrentAddedAlert(data.length);
} else {
emitRequestSentAlert(options.urls.length);
}
},
() => {
emitFailedToAddTorrentAlert(options.urls.length);
},
),
addTorrentsByFiles: (options: AddTorrentByFileOptions): Promise<void> =>
axios
.post(`${baseURI}api/torrents/add-files`, options)
.then((json) => json.data)
.then(
(response) => {
if (response.length) {
emitTorrentAddedAlert(response.length);
} else {
emitRequestSentAlert(options.files.length);
}
},
() => {
emitFailedToAddTorrentAlert(options.files.length);
},
),
axios.post<TorrentAddResponse>(`${baseURI}api/torrents/add-files`, options).then(
({data}) => {
if (data.length) {
emitTorrentAddedAlert(data.length);
} else {
emitRequestSentAlert(options.files.length);
}
},
() => {
emitFailedToAddTorrentAlert(options.files.length);
},
),
createTorrent: (options: CreateTorrentOptions): Promise<void> =>
axios.post(`${baseURI}api/torrents/create`, options, {responseType: 'blob'}).then(
(response) => {
download(response.data, (options.name || `${Date.now()}`).concat('.torrent'));
axios.post<Blob>(`${baseURI}api/torrents/create`, options, {responseType: 'blob'}).then(
({data}) => {
download(data, (options.name || `${Date.now()}`).concat('.torrent'));
emitTorrentAddedAlert(1);
},
() => {
@@ -101,68 +97,53 @@ const TorrentActions = {
),
deleteTorrents: (options: DeleteTorrentsOptions): Promise<void> =>
axios
.post(`${baseURI}api/torrents/delete`, options)
.then((json) => json.data)
.then(
() => {
AlertStore.add({
id: 'alert.torrent.remove',
type: 'success',
count: options.hashes.length,
});
},
() => {
AlertStore.add({
id: 'alert.torrent.remove.failed',
type: 'error',
count: options.hashes.length,
});
},
),
axios.post(`${baseURI}api/torrents/delete`, options).then(
() =>
AlertStore.add({
id: 'alert.torrent.remove',
type: 'success',
count: options.hashes.length,
}),
() =>
AlertStore.add({
id: 'alert.torrent.remove.failed',
type: 'error',
count: options.hashes.length,
}),
),
checkHash: (options: CheckTorrentsOptions): Promise<void> =>
axios
.post(`${baseURI}api/torrents/check-hash`, options)
.then((json) => json.data)
.then(
() => {
// do nothing.
},
() => {
// do nothing.
},
),
axios.post(`${baseURI}api/torrents/check-hash`, options).then(
() => {
// do nothing.
},
() => {
// do nothing.
},
),
fetchMediainfo: (hash: TorrentProperties['hash'], cancelToken?: CancelToken): Promise<{output: string}> =>
axios.get(`${baseURI}api/torrents/${hash}/mediainfo`, {cancelToken}).then<{output: string}>((json) => json.data),
axios
.get<TorrentMediainfoResponse>(`${baseURI}api/torrents/${hash}/mediainfo`, {cancelToken})
.then<{output: string}>((res) => res.data),
fetchTorrentContents: (hash: TorrentProperties['hash']): Promise<Array<TorrentContent> | null> =>
axios
.get(`${baseURI}api/torrents/${hash}/contents`)
.then<Array<TorrentContent>>((json) => json.data)
.then(
(contents) => contents,
() => null,
),
axios.get<Array<TorrentContent>>(`${baseURI}api/torrents/${hash}/contents`).then(
(res) => res.data,
() => null,
),
fetchTorrentPeers: (hash: TorrentProperties['hash']): Promise<Array<TorrentPeer> | null> =>
axios
.get(`${baseURI}api/torrents/${hash}/peers`)
.then<Array<TorrentPeer>>((json) => json.data)
.then(
(peers) => peers,
() => null,
),
axios.get<Array<TorrentPeer>>(`${baseURI}api/torrents/${hash}/peers`).then(
(res) => res.data,
() => null,
),
fetchTorrentTrackers: (hash: TorrentProperties['hash']): Promise<Array<TorrentTracker> | null> =>
axios
.get(`${baseURI}api/torrents/${hash}/trackers`)
.then<Array<TorrentTracker>>((json) => json.data)
.then(
(trackers) => trackers,
() => null,
),
axios.get<Array<TorrentTracker>>(`${baseURI}api/torrents/${hash}/trackers`).then(
(res) => res.data,
() => null,
),
getTorrentContentsDataPermalink: (hash: TorrentProperties['hash'], indices: number[]): Promise<string> =>
axios
@@ -175,150 +156,116 @@ const TorrentActions = {
),
moveTorrents: (options: MoveTorrentsOptions): Promise<void> =>
axios
.post(`${baseURI}api/torrents/move`, options)
.then((json) => json.data)
.then(
() => {
AlertStore.add({
id: 'alert.torrent.move',
type: 'success',
count: options.hashes.length,
});
},
() => {
AlertStore.add({
id: 'alert.torrent.move.failed',
type: 'error',
count: options.hashes.length,
});
},
),
axios.post(`${baseURI}api/torrents/move`, options).then(
() =>
AlertStore.add({
id: 'alert.torrent.move',
type: 'success',
count: options.hashes.length,
}),
() =>
AlertStore.add({
id: 'alert.torrent.move.failed',
type: 'error',
count: options.hashes.length,
}),
),
reannounce: (options: ReannounceTorrentsOptions): Promise<void> =>
axios
.post(`${baseURI}api/torrents/reannounce`, options)
.then((json) => json.data)
.then(
() => {
// do nothing.
},
() => {
// do nothing.
},
),
axios.post(`${baseURI}api/torrents/reannounce`, options).then(
() => {
// do nothing.
},
() => {
// do nothing.
},
),
startTorrents: async (options: StartTorrentsOptions): Promise<void> => {
if (options.hashes.length > 0) {
return axios
.post(`${baseURI}api/torrents/start`, options)
.then((json) => json.data)
.then(
() => {
// do nothing.
},
() => {
// do nothing.
},
);
return axios.post(`${baseURI}api/torrents/start`, options).then(
() => {
// do nothing.
},
() => {
// do nothing.
},
);
}
return undefined;
},
stopTorrents: async (options: StopTorrentsOptions): Promise<void> => {
if (options.hashes.length > 0) {
return axios
.post(`${baseURI}api/torrents/stop`, options)
.then((json) => json.data)
.then(
() => {
// do nothing.
},
() => {
// do nothing.
},
);
return axios.post(`${baseURI}api/torrents/stop`, options).then(
() => {
// do nothing.
},
() => {
// do nothing.
},
);
}
return undefined;
},
setInitialSeeding: (options: SetTorrentsInitialSeedingOptions): Promise<void> =>
axios
.patch(`${baseURI}api/torrents/initial-seeding`, options)
.then((json) => json.data)
.then(
() => {
// do nothing.
},
() => {
// do nothing.
},
),
axios.patch(`${baseURI}api/torrents/initial-seeding`, options).then(
() => {
// do nothing.
},
() => {
// do nothing.
},
),
setPriority: (options: SetTorrentsPriorityOptions): Promise<void> =>
axios
.patch(`${baseURI}api/torrents/priority`, options)
.then((json) => json.data)
.then(
() => {
// do nothing.
},
() => {
// do nothing.
},
),
axios.patch(`${baseURI}api/torrents/priority`, options).then(
() => {
// do nothing.
},
() => {
// do nothing.
},
),
setSequential: (options: SetTorrentsSequentialOptions): Promise<void> =>
axios
.patch(`${baseURI}api/torrents/sequential`, options)
.then((json) => json.data)
.then(
() => {
// do nothing.
},
() => {
// do nothing.
},
),
axios.patch(`${baseURI}api/torrents/sequential`, options).then(
() => {
// do nothing.
},
() => {
// do nothing.
},
),
setFilePriority: (hash: TorrentProperties['hash'], options: SetTorrentContentsPropertiesOptions): Promise<void> =>
axios
.patch(`${baseURI}api/torrents/${hash}/contents`, options)
.then((json) => json.data)
.then(
() => {
// do nothing.
},
() => {
// do nothing.
},
),
axios.patch(`${baseURI}api/torrents/${hash}/contents`, options).then(
() => {
// do nothing.
},
() => {
// do nothing.
},
),
setTags: (options: SetTorrentsTagsOptions): Promise<void> =>
axios
.patch(`${baseURI}api/torrents/tags`, options)
.then((json) => json.data)
.then(
() => {
UIStore.handleSetTaxonomySuccess();
},
() => {
// do nothing.
},
),
axios.patch(`${baseURI}api/torrents/tags`, options).then(
() => UIStore.handleSetTaxonomySuccess(),
() => {
// do nothing.
},
),
setTrackers: (options: SetTorrentsTrackersOptions): Promise<void> =>
axios
.patch(`${baseURI}api/torrents/trackers`, options)
.then((json) => json.data)
.then(
() => {
// do nothing.
},
() => {
// do nothing.
},
),
axios.patch(`${baseURI}api/torrents/trackers`, options).then(
() => {
// do nothing.
},
() => {
// do nothing.
},
),
};
export default TorrentActions;

View File

@@ -1,5 +1,5 @@
import debounce from 'lodash/debounce';
import {forwardRef, MutableRefObject, ReactNode, useEffect, useRef, useState} from 'react';
import {forwardRef, MutableRefObject, ReactElement, ReactNode, useEffect, useRef, useState} from 'react';
import {Trans, useLingui} from '@lingui/react';
import {useEnsuredForwardedRef} from 'react-use';
@@ -67,7 +67,7 @@ const FilesystemBrowserTextbox = forwardRef<HTMLInputElement, FilesystemBrowserT
};
}, []);
const toggles: React.ReactNodeArray = [];
const toggles: Array<ReactElement> = [];
if (showBasePathToggle) {
toggles.push(
<Checkbox grow={false} id="isBasePath" key="isBasePath">

View File

@@ -1,5 +1,5 @@
import classnames from 'classnames';
import {FC, ReactNode, ReactNodeArray, useEffect, useRef, useState} from 'react';
import {FC, ReactElement, ReactNode, useEffect, useRef, useState} from 'react';
import {sort} from 'fast-sort';
import {Trans} from '@lingui/react';
import {useKeyPressEvent} from 'react-use';
@@ -114,7 +114,7 @@ const TagSelect: FC<TagSelectProps> = ({defaultValue, placeholder, id, label, on
...sort(Object.keys(TorrentFilterStore.taxonomy.tagCounts)).asc(),
...selectedTags,
]),
].reduce((accumulator: ReactNodeArray, tag) => {
].reduce((accumulator: Array<ReactElement>, tag) => {
if (tag === '') {
return accumulator;
}

View File

@@ -1,5 +1,5 @@
import classnames from 'classnames';
import {FC, ReactNode, ReactNodeArray} from 'react';
import {FC, ReactNode} from 'react';
export interface Tab {
id?: string;
@@ -18,41 +18,43 @@ interface ModalTabsProps {
const ModalTabs: FC<ModalTabsProps> = (props: ModalTabsProps) => {
const {activeTabId, tabs = {}, onTabChange} = props;
const tabNodes: ReactNodeArray = Object.keys(tabs).map((tabId) => {
const currentTab = tabs[tabId];
return (
<ul className="modal__tabs">
{Object.keys(tabs).map((tabId) => {
const currentTab = tabs[tabId];
currentTab.id = tabId;
currentTab.id = tabId;
const classes = classnames('modal__tab', {
'is-active': tabId === activeTabId,
});
const classes = classnames('modal__tab', {
'is-active': tabId === activeTabId,
});
return (
<li className={classes} key={tabId}>
<button
css={{
':focus': {
outline: 'none',
WebkitTapHighlightColor: 'transparent',
},
':focus-visible': {
outline: 'dashed',
},
}}
type="button"
onClick={() => {
if (onTabChange) {
onTabChange(currentTab);
}
}}
>
{currentTab.label}
</button>
</li>
);
});
return <ul className="modal__tabs">{tabNodes}</ul>;
return (
<li className={classes} key={tabId}>
<button
css={{
':focus': {
outline: 'none',
WebkitTapHighlightColor: 'transparent',
},
':focus-visible': {
outline: 'dashed',
},
}}
type="button"
onClick={() => {
if (onTabChange) {
onTabChange(currentTab);
}
}}
>
{currentTab.label}
</button>
</li>
);
})}
</ul>
);
};
ModalTabs.defaultProps = {

View File

@@ -1,4 +1,4 @@
import {FC, ReactNodeArray, useRef, useState} from 'react';
import {FC, ReactElement, useRef, useState} from 'react';
import {Trans, useLingui} from '@lingui/react';
import {Button, Form, FormError, FormRow, FormRowItem} from '@client/ui';
@@ -164,7 +164,7 @@ const DownloadRulesTab: FC = () => {
<ModalFormSectionHeader>
<Trans id="feeds.existing.rules" />
</ModalFormSectionHeader>
{Object.keys(errors).reduce((memo: ReactNodeArray, key) => {
{Object.keys(errors).reduce((memo: Array<ReactElement>, key) => {
if (errors[key as ValidatedField] != null) {
memo.push(
<FormRow key={`error-${key}`}>

View File

@@ -1,4 +1,4 @@
import {FC, ReactNodeArray} from 'react';
import {FC, ReactElement} from 'react';
import {observer} from 'mobx-react';
import {Trans} from '@lingui/react';
@@ -13,7 +13,7 @@ interface FeedItemsProps {
const FeedItems: FC<FeedItemsProps> = observer(({selectedFeedID}: FeedItemsProps) => {
const {items} = FeedStore;
const itemElements: ReactNodeArray = [];
const itemElements: Array<ReactElement> = [];
if (selectedFeedID) {
const titleOccurrences: Record<string, number> = {};
items.forEach((item, index) => {

View File

@@ -1,4 +1,4 @@
import {FC, ReactNodeArray, useRef, useState} from 'react';
import {FC, ReactElement, useRef, useState} from 'react';
import {Trans, useLingui} from '@lingui/react';
import {Button, Form, FormError, FormRow, FormRowItem} from '@client/ui';
@@ -137,7 +137,7 @@ const FeedsTab: FC = () => {
<ModalFormSectionHeader>
<Trans id="feeds.existing.feeds" />
</ModalFormSectionHeader>
{Object.keys(errors).reduce((memo: ReactNodeArray, key) => {
{Object.keys(errors).reduce((memo: Array<ReactElement>, key) => {
if (errors[key as ValidatedField] != null) {
memo.push(
<FormRow key={`error-${key}`}>

View File

@@ -1,5 +1,5 @@
import classnames from 'classnames';
import {forwardRef, MutableRefObject, ReactNodeArray, useRef, useState} from 'react';
import {forwardRef, MutableRefObject, ReactElement, useRef, useState} from 'react';
import {observer} from 'mobx-react';
import {Trans, useLingui} from '@lingui/react';
import {useEnsuredForwardedRef} from 'react-use';
@@ -76,7 +76,7 @@ const TableHeading = observer(
return (
<div className="table__row table__row--heading" role="row" ref={tableHeading}>
{SettingStore.floodSettings.torrentListColumns.reduce((accumulator: ReactNodeArray, {id, visible}) => {
{SettingStore.floodSettings.torrentListColumns.reduce((accumulator: Array<ReactElement>, {id, visible}) => {
if (!visible) {
return accumulator;
}

View File

@@ -1,4 +1,4 @@
import {CSSProperties, forwardRef, KeyboardEvent, MouseEvent, ReactNodeArray, TouchEvent} from 'react';
import {CSSProperties, forwardRef, KeyboardEvent, MouseEvent, ReactElement, TouchEvent} from 'react';
import {observer} from 'mobx-react';
import SettingStore from '../../stores/SettingStore';
@@ -34,7 +34,7 @@ const TorrentListRowCondensed = observer(
ref,
) => {
const torrentListColumns = SettingStore.floodSettings.torrentListColumns.reduce(
(accumulator: ReactNodeArray, {id, visible}) => {
(accumulator: Array<ReactElement>, {id, visible}) => {
if (TorrentListColumns[id] == null) {
return accumulator;
}

View File

@@ -1,4 +1,4 @@
import {CSSProperties, FC, forwardRef, KeyboardEvent, MouseEvent, ReactNodeArray, TouchEvent} from 'react';
import {CSSProperties, FC, forwardRef, KeyboardEvent, MouseEvent, ReactElement, TouchEvent} from 'react';
import {observer} from 'mobx-react';
import {useLingui} from '@lingui/react';
@@ -54,7 +54,7 @@ const TorrentListRowExpanded = observer(
) => {
const columns = SettingStore.floodSettings.torrentListColumns;
const primarySection: ReactNodeArray = [
const primarySection: Array<ReactElement> = [
<TorrentListCell
key="name"
hash={hash}
@@ -62,12 +62,12 @@ const TorrentListRowExpanded = observer(
className="torrent__details__section torrent__details__section--primary"
/>,
];
const secondarySection: ReactNodeArray = [
const secondarySection: Array<ReactElement> = [
<TorrentListCell key="eta" hash={hash} column="eta" showIcon />,
<TorrentListCell key="downRate" hash={hash} column="downRate" showIcon />,
<TorrentListCell key="upRate" hash={hash} column="upRate" showIcon />,
];
const tertiarySection: ReactNodeArray = [
const tertiarySection: Array<ReactElement> = [
<TorrentListCell
key="percentComplete"
hash={hash}
@@ -76,7 +76,7 @@ const TorrentListRowExpanded = observer(
showIcon
/>,
];
const quaternarySection: ReactNodeArray = [
const quaternarySection: Array<ReactElement> = [
<TorrentListCell
key="percentBar"
hash={hash}

View File

@@ -4,7 +4,11 @@ import FloodActions from '@client/actions/FloodActions';
import {AccessLevel} from '@shared/schema/constants/Auth';
import type {AuthAuthenticationResponse, AuthVerificationResponse} from '@shared/schema/api/auth';
import type {
AuthAuthenticationResponse,
AuthRegistrationResponse,
AuthVerificationResponse,
} from '@shared/schema/api/auth';
import type {Credentials} from '@shared/schema/Auth';
class AuthStore {
@@ -26,7 +30,7 @@ class AuthStore {
makeAutoObservable(this);
}
handleCreateUserSuccess({username}: {username: Credentials['username']}): void {
handleCreateUserSuccess({username}: AuthRegistrationResponse): void {
this.optimisticUsers.push({username});
}
@@ -37,9 +41,9 @@ class AuthStore {
this.users = nextUserList;
}
handleLoginSuccess(response: AuthAuthenticationResponse): void {
this.currentUser.username = response.username;
this.currentUser.isAdmin = response.level === AccessLevel.ADMINISTRATOR;
handleLoginSuccess({username, level}: AuthAuthenticationResponse): void {
this.currentUser.username = username;
this.currentUser.isAdmin = level === AccessLevel.ADMINISTRATOR;
this.currentUser.isInitialUser = false;
this.isAuthenticating = true;
this.isAuthenticated = true;
@@ -50,21 +54,23 @@ class AuthStore {
this.isAuthenticating = true;
}
handleRegisterSuccess(response: AuthAuthenticationResponse): void {
this.currentUser.username = response.username;
this.currentUser.isAdmin = response.level === AccessLevel.ADMINISTRATOR;
handleRegisterSuccess({username, level}: AuthRegistrationResponse): void {
this.currentUser.username = username;
this.currentUser.isAdmin = level === AccessLevel.ADMINISTRATOR;
this.currentUser.isInitialUser = false;
FloodActions.restartActivityStream();
}
handleAuthVerificationSuccess(response: AuthVerificationResponse): void {
if (response.initialUser === true) {
this.currentUser.isInitialUser = response.initialUser;
this.currentUser.isInitialUser = true;
} else {
const {username, level} = response;
this.currentUser = {
username: response.username,
isAdmin: response.level === AccessLevel.ADMINISTRATOR,
isInitialUser: response.initialUser,
username: username,
isAdmin: level === AccessLevel.ADMINISTRATOR,
isInitialUser: false,
};
}

View File

@@ -1,4 +1,4 @@
import {ButtonHTMLAttributes, Children, cloneElement, FC, ReactElement, ReactNode, ReactNodeArray, Ref} from 'react';
import {ButtonHTMLAttributes, Children, cloneElement, FC, ReactElement, ReactNode, Ref} from 'react';
import classnames from 'classnames';
import {LoadingRing} from '@client/ui/icons';
@@ -42,8 +42,8 @@ const Button: FC<ButtonProps> = ({
grow,
onClick,
}: ButtonProps) => {
const addonNodes: ReactNodeArray = [];
const childNodes: ReactNodeArray = [];
const addonNodes: Array<ReactElement> = [];
const childNodes: Array<ReactElement> = [];
Children.toArray(children).forEach((child) => {
const childAsElement = child as ReactElement;
@@ -56,7 +56,7 @@ const Button: FC<ButtonProps> = ({
}),
);
} else {
childNodes.push(child);
childNodes.push(childAsElement);
}
});

3423
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -66,7 +66,7 @@
"test:client": "FLOOD_OPTION_port=4200 start-server-and-test start 4200 'cypress run'"
},
"dependencies": {
"geoip-country": "^4.0.89"
"geoip-country": "^4.0.91"
},
"devDependencies": {
"@babel/core": "^7.15.8",
@@ -78,8 +78,8 @@
"@babel/preset-react": "^7.14.5",
"@babel/preset-typescript": "^7.15.0",
"@emotion/babel-plugin": "^11.3.0",
"@emotion/css": "^11.1.3",
"@emotion/react": "^11.4.1",
"@emotion/css": "^11.5.0",
"@emotion/react": "^11.5.0",
"@lingui/loader": "^3.12.1",
"@lingui/react": "^3.12.1",
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.1",
@@ -89,7 +89,7 @@
"@types/content-disposition": "^0.5.4",
"@types/cookie-parser": "^1.4.2",
"@types/create-torrent": "^5.0.0",
"@types/d3": "^7.0.0",
"@types/d3": "^7.1.0",
"@types/debug": "^4.1.7",
"@types/express": "^4.17.13",
"@types/express-rate-limit": "^5.1.3",
@@ -98,29 +98,28 @@
"@types/http-errors": "^1.8.1",
"@types/jest": "^27.0.2",
"@types/jsonwebtoken": "^8.5.5",
"@types/lodash": "^4.14.175",
"@types/lodash": "^4.14.176",
"@types/morgan": "^1.9.3",
"@types/node": "^12.20.28",
"@types/node": "^12.20.34",
"@types/overlayscrollbars": "^1.12.1",
"@types/parse-torrent": "^5.8.4",
"@types/passport": "^1.0.7",
"@types/passport-jwt": "^3.0.6",
"@types/react": "^17.0.27",
"@types/react-dom": "^17.0.9",
"@types/react-measure": "^2.0.7",
"@types/react": "^17.0.31",
"@types/react-dom": "^17.0.10",
"@types/react-measure": "^2.0.8",
"@types/react-router-dom": "^5.3.1",
"@types/react-transition-group": "^4.4.3",
"@types/react-transition-group": "^4.4.4",
"@types/react-window": "^1.8.5",
"@types/spdy": "^3.4.5",
"@types/supertest": "^2.0.11",
"@types/tar-fs": "^2.0.1",
"@types/tar-stream": "^2.2.1",
"@typescript-eslint/eslint-plugin": "^4.33.0",
"@typescript-eslint/parser": "^4.33.0",
"@vercel/ncc": "^0.31.1",
"autoprefixer": "^10.3.7",
"axios": "^0.21.4",
"babel-loader": "^8.2.2",
"axios": "^0.23.0",
"babel-loader": "^8.2.3",
"bencode": "^2.0.2",
"body-parser": "^1.19.0",
"case-sensitive-paths-webpack-plugin": "2.4.0",
@@ -130,7 +129,7 @@
"content-disposition": "^0.5.3",
"cookie-parser": "^1.4.5",
"create-torrent": "^5.0.1",
"css-loader": "^6.3.0",
"css-loader": "^6.4.0",
"css-minimizer-webpack-plugin": "^3.1.1",
"d3-array": "^3.1.1",
"d3-scale": "^4.0.2",
@@ -141,16 +140,11 @@
"eslint-config-airbnb": "^18.2.1",
"eslint-config-airbnb-typescript": "^14.0.1",
"eslint-config-prettier": "^8.3.0",
"eslint-config-react-app": "^6.0.0",
"eslint-import-resolver-webpack": "^0.13.1",
"eslint-plugin-import": "^2.24.2",
"eslint-plugin-jest": "^24.5.2",
"eslint-plugin-jsx-a11y": "^6.4.1",
"eslint-plugin-react": "^7.26.1",
"eslint-plugin-react-hooks": "^4.2.0",
"eslint-config-react-app": "^7.0.0-next.91",
"eslint-import-resolver-webpack": "^0.13.2",
"eslint-webpack-plugin": "^3.0.1",
"express": "^4.17.1",
"express-rate-limit": "^5.4.1",
"express-rate-limit": "^5.5.0",
"fast-json-patch": "^3.1.0",
"fast-sort": "^3.0.3",
"feedsub": "^0.7.7",
@@ -160,15 +154,15 @@
"fs-extra": "^10.0.0",
"get-user-locale": "^1.4.0",
"hash-wasm": "^4.9.0",
"html-webpack-plugin": "^5.3.2",
"html-webpack-plugin": "^5.4.0",
"http-errors": "^1.8.0",
"jest": "^27.2.5",
"jest": "^27.3.1",
"js-file-download": "^0.4.12",
"jsonwebtoken": "^8.5.1",
"lodash": "^4.17.21",
"mini-css-extract-plugin": "^2.4.2",
"mobx": "^6.3.3",
"mobx-react": "^7.2.0",
"mini-css-extract-plugin": "^2.4.3",
"mobx": "^6.3.5",
"mobx-react": "^7.2.1",
"morgan": "^1.10.0",
"nedb-promises": "^5.0.1",
"overlayscrollbars": "^1.13.1",
@@ -177,8 +171,8 @@
"passport": "^0.5.0",
"passport-jwt": "^4.0.0",
"polished": "^4.1.3",
"postcss": "^8.3.9",
"postcss-loader": "^6.1.1",
"postcss": "^8.3.11",
"postcss-loader": "^6.2.0",
"prettier": "^2.4.1",
"promise": "^8.1.0",
"query-string": "^6.14.1",
@@ -200,29 +194,27 @@
"react-window": "^1.8.6",
"ress": "^4.0.0",
"sanitize-filename": "^1.6.3",
"sass": "^1.42.1",
"sass-loader": "^12.1.0",
"sass": "^1.43.3",
"sass-loader": "^12.2.0",
"saxen": "^8.1.2",
"source-map-loader": "^3.0.0",
"spdy": "^4.0.2",
"style-loader": "^3.3.0",
"style-loader": "^3.3.1",
"supertest": "^6.1.6",
"tar-fs": "^2.1.1",
"tar-stream": "^2.2.0",
"terser-webpack-plugin": "^5.2.4",
"tldts": "^5.7.47",
"ts-jest": "^27.0.5",
"tldts": "^5.7.49",
"ts-jest": "^27.0.7",
"ts-node-dev": "^1.1.8",
"tsconfig-paths": "^3.11.0",
"typed-emitter": "^1.3.1",
"typescript": "~4.4.3",
"url-loader": "^4.1.1",
"typed-emitter": "^1.4.0",
"typescript": "~4.4.4",
"use-query-params": "^1.2.3",
"webpack": "^5.58.1",
"webpack": "^5.59.1",
"webpack-dev-server": "^4.3.1",
"webpackbar": "^5.0.0-3",
"yargs": "^17.2.1",
"zod": "^3.9.8"
"zod": "^3.11.2"
},
"engines": {
"node": ">=12.0.0",

View File

@@ -9,7 +9,7 @@ import {contentTokenSchema} from '@shared/schema/api/torrents';
import type {FloodSettings} from '@shared/types/FloodSettings';
import type {HistorySnapshot} from '@shared/constants/historySnapshotTypes';
import type {NotificationFetchOptions, NotificationState} from '@shared/types/Notification';
import type {SetFloodSettingsOptions} from '@shared/types/api/index';
import type {DirectoryListQuery, DirectoryListResponse, SetFloodSettingsOptions} from '@shared/types/api/index';
import {accessDeniedError, isAllowedPath, sanitizePath} from '../../util/fileUtil';
import appendUserServices from '../../middleware/appendUserServices';
@@ -100,9 +100,9 @@ router.get('/activity-stream', eventStream, clientActivityStream);
* @return {Error} 422 - invalid argument - application/json
* @return {Error} 500 - other errors - application/json
*/
router.get<unknown, unknown, unknown, {path: string}>(
router.get<unknown, unknown, unknown, DirectoryListQuery>(
'/directory-list',
async (req, res): Promise<Response<unknown>> => {
async (req, res): Promise<Response<DirectoryListResponse>> => {
const {path: inputPath} = req.query;
if (typeof inputPath !== 'string' || !inputPath) {

View File

@@ -907,13 +907,9 @@ router.get<{hash: string}>(
'mediainfo',
torrentContentPaths,
{maxBuffer: 1024 * 2000, timeout: 1000 * 10},
(error, stdout, stderr) => {
(error, stdout) => {
if (error) {
return res.status(500).json({message: error.message});
}
if (stderr) {
return res.status(500).json({message: stderr});
return res.status(500).json({code: error.code, message: error.message});
}
return res.status(200).json({output: stdout});

View File

@@ -18,49 +18,50 @@ import {
TransmissionTorrentsSetLocationArguments,
} from './types/TransmissionTorrentsMethods';
type TransmissionRPCResponse<T = undefined> = {
result: 'success';
arguments: T;
} & {
result: string;
};
class ClientRequestManager {
private rpcURL: string;
private authHeader: string;
private sessionID?: Promise<string | undefined>;
async fetchSessionID(url = this.rpcURL, authHeader = this.authHeader): Promise<string | undefined> {
return axios
let id: string | undefined = undefined;
await axios
.get(url, {
headers: {
Authorization: authHeader,
},
})
.then<string | undefined>(
() => {
return undefined;
},
(err: AxiosError) => {
if (err.response?.status === 409) {
return err.response?.headers['x-transmission-session-id'];
}
.catch((err: AxiosError) => {
if (err.response?.status !== 409) {
throw err;
},
);
}
id = err.response?.headers['x-transmission-session-id'];
});
return id;
}
async updateSessionID(url = this.rpcURL, authHeader = this.authHeader): Promise<void> {
let authFailed = false;
this.sessionID = new Promise((resolve) => {
this.fetchSessionID(url, authHeader).then(
(sessionID) => {
resolve(sessionID);
},
() => {
authFailed = true;
resolve(undefined);
},
);
this.sessionID = this.fetchSessionID(url, authHeader).catch(() => {
authFailed = true;
return undefined;
});
await this.sessionID;
return authFailed ? Promise.reject(new Error()) : Promise.resolve();
if (authFailed) {
throw new Error();
}
}
async getRequestHeaders(): Promise<Record<string, string>> {
@@ -74,18 +75,18 @@ class ClientRequestManager {
async getSessionStats(): Promise<TransmissionSessionStats> {
return axios
.post(
.post<TransmissionRPCResponse<TransmissionSessionStats>>(
this.rpcURL,
{method: 'session-stats'},
{
headers: await this.getRequestHeaders(),
},
)
.then((res) => {
if (res.data.result !== 'success') {
.then(({data}) => {
if (data.result !== 'success') {
throw new Error();
}
return res.data.arguments;
return data.arguments;
});
}
@@ -95,32 +96,32 @@ class ClientRequestManager {
const sessionGetArguments: TransmissionSessionGetArguments = {fields};
return axios
.post(
.post<TransmissionRPCResponse<Pick<TransmissionSessionProperties, T[number]>>>(
this.rpcURL,
{method: 'session-get', arguments: sessionGetArguments},
{
headers: await this.getRequestHeaders(),
},
)
.then((res) => {
if (res.data.result !== 'success') {
.then(({data}) => {
if (data.result !== 'success') {
throw new Error();
}
return res.data.arguments;
return data.arguments;
});
}
async setSessionProperties(properties: TransmissionSessionSetArguments): Promise<void> {
return axios
.post(
.post<TransmissionRPCResponse>(
this.rpcURL,
{method: 'session-set', arguments: properties},
{
headers: await this.getRequestHeaders(),
},
)
.then((res) => {
if (res.data.result !== 'success') {
.then(({data}) => {
if (data.result !== 'success') {
throw new Error();
}
});
@@ -137,18 +138,18 @@ class ClientRequestManager {
};
return axios
.post(
.post<TransmissionRPCResponse<{torrents: Array<Pick<TransmissionTorrentProperties, T[number]>>}>>(
this.rpcURL,
{method: 'torrent-get', arguments: torrentsGetArguments},
{
headers: await this.getRequestHeaders(),
},
)
.then((res) => {
if (res.data.result !== 'success' || res.data.arguments.torrents == null) {
.then(({data}) => {
if (data.result !== 'success' || data.arguments.torrents == null) {
throw new Error();
}
return res.data.arguments.torrents;
return data.arguments.torrents;
});
}
@@ -156,32 +157,34 @@ class ClientRequestManager {
args: TransmissionTorrentAddArguments,
): Promise<Pick<TransmissionTorrentProperties, 'id' | 'name' | 'hashString'>> {
return axios
.post(
.post<
TransmissionRPCResponse<{'torrent-added'?: Pick<TransmissionTorrentProperties, 'id' | 'name' | 'hashString'>}>
>(
this.rpcURL,
{method: 'torrent-add', arguments: args},
{
headers: await this.getRequestHeaders(),
},
)
.then((res) => {
if (res.data.result !== 'success') {
.then(({data}) => {
if (data.result !== 'success' || !data.arguments['torrent-added']) {
throw new Error();
}
return res.data.arguments['torrent-added'];
return data.arguments['torrent-added'];
});
}
async reannounceTorrents(ids: TransmissionTorrentIDs): Promise<void> {
return axios
.post(
.post<TransmissionRPCResponse>(
this.rpcURL,
{method: 'torrent-reannounce', arguments: {ids}},
{
headers: await this.getRequestHeaders(),
},
)
.then((res) => {
if (res.data.result !== 'success') {
.then(({data}) => {
if (data.result !== 'success') {
throw new Error();
}
});
@@ -189,15 +192,15 @@ class ClientRequestManager {
async setTorrentsProperties(args: TransmissionTorrentsSetArguments): Promise<void> {
return axios
.post(
.post<TransmissionRPCResponse>(
this.rpcURL,
{method: 'torrent-set', arguments: args},
{
headers: await this.getRequestHeaders(),
},
)
.then((res) => {
if (res.data.result !== 'success') {
.then(({data}) => {
if (data.result !== 'success') {
throw new Error();
}
});
@@ -205,15 +208,15 @@ class ClientRequestManager {
async startTorrents(ids: TransmissionTorrentIDs): Promise<void> {
return axios
.post(
.post<TransmissionRPCResponse>(
this.rpcURL,
{method: 'torrent-start-now', arguments: {ids}},
{
headers: await this.getRequestHeaders(),
},
)
.then((res) => {
if (res.data.result !== 'success') {
.then(({data}) => {
if (data.result !== 'success') {
throw new Error();
}
});
@@ -221,15 +224,15 @@ class ClientRequestManager {
async stopTorrents(ids: TransmissionTorrentIDs): Promise<void> {
return axios
.post(
.post<TransmissionRPCResponse>(
this.rpcURL,
{method: 'torrent-stop', arguments: {ids}},
{
headers: await this.getRequestHeaders(),
},
)
.then((res) => {
if (res.data.result !== 'success') {
.then(({data}) => {
if (data.result !== 'success') {
throw new Error();
}
});
@@ -237,15 +240,15 @@ class ClientRequestManager {
async verifyTorrents(ids: TransmissionTorrentIDs): Promise<void> {
return axios
.post(
.post<TransmissionRPCResponse>(
this.rpcURL,
{method: 'torrent-verify', arguments: {ids}},
{
headers: await this.getRequestHeaders(),
},
)
.then((res) => {
if (res.data.result !== 'success') {
.then(({data}) => {
if (data.result !== 'success') {
throw new Error();
}
});
@@ -258,15 +261,15 @@ class ClientRequestManager {
};
return axios
.post(
.post<TransmissionRPCResponse>(
this.rpcURL,
{method: 'torrent-remove', arguments: removeTorrentsArguments},
{
headers: await this.getRequestHeaders(),
},
)
.then((res) => {
if (res.data.result !== 'success') {
.then(({data}) => {
if (data.result !== 'success') {
throw new Error();
}
});
@@ -280,7 +283,7 @@ class ClientRequestManager {
};
return axios
.post(
.post<TransmissionRPCResponse>(
this.rpcURL,
{
method: 'torrent-set-location',
@@ -290,8 +293,8 @@ class ClientRequestManager {
headers: await this.getRequestHeaders(),
},
)
.then((res) => {
if (res.data.result !== 'success') {
.then(({data}) => {
if (data.result !== 'success') {
throw new Error();
}
});

View File

@@ -34,7 +34,7 @@ const EMPTY_SERVER_STATE = {
class ClientRequestManager {
private connectionSettings: QBittorrentConnectionSettings;
private apiBase: string;
private authCookie?: Promise<string | undefined>;
private authCookie: Promise<string | undefined> = Promise.resolve(undefined);
private isMainDataPending = false;
private syncRids: {
@@ -66,7 +66,7 @@ class ClientRequestManager {
},
})
.then((res) => {
const cookies: Array<string> = res.headers['set-cookie'];
const cookies = res.headers['set-cookie'];
if (Array.isArray(cookies)) {
return cookies.filter((cookie) => cookie.includes('SID='))[0];
@@ -79,35 +79,37 @@ class ClientRequestManager {
async updateAuthCookie(connectionSettings?: QBittorrentConnectionSettings): Promise<void> {
let authFailed = false;
this.authCookie = new Promise((resolve) => {
this.authenticate(connectionSettings).then(
(authCookie) => {
resolve(authCookie);
},
() => {
authFailed = true;
resolve(undefined);
},
);
this.authCookie = this.authenticate(connectionSettings).catch(() => {
authFailed = true;
return undefined;
});
await this.authCookie;
return authFailed ? Promise.reject(new Error()) : Promise.resolve();
if (authFailed) {
throw new Error();
}
}
async getRequestHeaders(): Promise<Record<string, string>> {
const Cookie = await this.authCookie;
return {
...(Cookie == null ? {} : {Cookie}),
};
}
async getAppPreferences(): Promise<QBittorrentAppPreferences> {
return axios
.get(`${this.apiBase}/app/preferences`, {
headers: {Cookie: await this.authCookie},
.get<QBittorrentAppPreferences>(`${this.apiBase}/app/preferences`, {
headers: await this.getRequestHeaders(),
})
.then((json) => json.data);
.then((res) => res.data);
}
async setAppPreferences(preferences: Partial<QBittorrentAppPreferences>): Promise<void> {
return axios
.post(`${this.apiBase}/app/setPreferences`, `json=${JSON.stringify(preferences)}`, {
headers: {Cookie: await this.authCookie},
headers: await this.getRequestHeaders(),
})
.then(() => {
// returns nothing
@@ -116,55 +118,55 @@ class ClientRequestManager {
async getTorrentInfos(): Promise<QBittorrentTorrentInfos> {
return axios
.get(`${this.apiBase}/torrents/info`, {
headers: {Cookie: await this.authCookie},
.get<QBittorrentTorrentInfos>(`${this.apiBase}/torrents/info`, {
headers: await this.getRequestHeaders(),
})
.then((json) => json.data);
.then((res) => res.data);
}
async getTorrentContents(hash: string): Promise<QBittorrentTorrentContents> {
return axios
.get(`${this.apiBase}/torrents/files`, {
.get<QBittorrentTorrentContents>(`${this.apiBase}/torrents/files`, {
params: {
hash: hash.toLowerCase(),
},
headers: {Cookie: await this.authCookie},
headers: await this.getRequestHeaders(),
})
.then((json) => json.data);
.then((res) => res.data);
}
async getTorrentProperties(hash: string): Promise<QBittorrentTorrentProperties> {
return axios
.get(`${this.apiBase}/torrents/properties`, {
.get<QBittorrentTorrentProperties>(`${this.apiBase}/torrents/properties`, {
params: {
hash: hash.toLowerCase(),
},
headers: {Cookie: await this.authCookie},
headers: await this.getRequestHeaders(),
})
.then((json) => json.data);
.then((res) => res.data);
}
async getTorrentTrackers(hash: string): Promise<QBittorrentTorrentTrackers> {
return axios
.get(`${this.apiBase}/torrents/trackers`, {
.get<QBittorrentTorrentTrackers>(`${this.apiBase}/torrents/trackers`, {
params: {
hash: hash.toLowerCase(),
},
headers: {Cookie: await this.authCookie},
headers: await this.getRequestHeaders(),
})
.then((json) => json.data);
.then((res) => res.data);
}
async getTransferInfo(): Promise<QBittorrentTransferInfo> {
return axios
.get(`${this.apiBase}/transfer/info`, {
headers: {Cookie: await this.authCookie},
.get<QBittorrentTransferInfo>(`${this.apiBase}/transfer/info`, {
headers: await this.getRequestHeaders(),
})
.then((json) => json.data);
.then((res) => res.data);
}
async syncMainData(): Promise<QBittorrentMainData> {
const Cookie = await this.authCookie;
const headers = await this.getRequestHeaders();
if (this.isMainDataPending == false) {
this.isMainDataPending = true;
@@ -174,7 +176,7 @@ class ClientRequestManager {
params: {
rid,
},
headers: {Cookie},
headers,
})
.then(({data}) => {
const {
@@ -268,9 +270,9 @@ class ClientRequestManager {
hash: hash.toLowerCase(),
rid: 0,
},
headers: {Cookie: await this.authCookie},
headers: await this.getRequestHeaders(),
})
.then(({data}) => data.peers as QBittorrentTorrentPeers);
.then(({data}) => data.peers);
}
async torrentsPause(hashes: Array<string>): Promise<void> {
@@ -279,7 +281,7 @@ class ClientRequestManager {
params: {
hashes: hashes.join('|').toLowerCase(),
},
headers: {Cookie: await this.authCookie},
headers: await this.getRequestHeaders(),
})
.then(() => {
// returns nothing
@@ -292,7 +294,7 @@ class ClientRequestManager {
params: {
hashes: hashes.join('|').toLowerCase(),
},
headers: {Cookie: await this.authCookie},
headers: await this.getRequestHeaders(),
})
.then(() => {
// returns nothing
@@ -306,7 +308,7 @@ class ClientRequestManager {
hashes: hashes.join('|').toLowerCase(),
deleteFiles: deleteFiles ? 'true' : 'false',
},
headers: {Cookie: await this.authCookie},
headers: await this.getRequestHeaders(),
})
.then(() => {
// returns nothing
@@ -319,7 +321,7 @@ class ClientRequestManager {
params: {
hashes: hashes.join('|').toLowerCase(),
},
headers: {Cookie: await this.authCookie},
headers: await this.getRequestHeaders(),
})
.then(() => {
// returns nothing
@@ -333,7 +335,7 @@ class ClientRequestManager {
hashes: hashes.join('|').toLowerCase(),
location,
},
headers: {Cookie: await this.authCookie},
headers: await this.getRequestHeaders(),
})
.then(() => {
// returns nothing
@@ -346,7 +348,7 @@ class ClientRequestManager {
params: {
hashes: hashes.join('|').toLowerCase(),
},
headers: {Cookie: await this.authCookie},
headers: await this.getRequestHeaders(),
})
.then(() => {
// returns nothing
@@ -359,7 +361,7 @@ class ClientRequestManager {
params: {
hashes: hashes.join('|').toLowerCase(),
},
headers: {Cookie: await this.authCookie},
headers: await this.getRequestHeaders(),
})
.then(() => {
// returns nothing
@@ -426,7 +428,7 @@ class ClientRequestManager {
hashes: hashes.join('|').toLowerCase(),
tags: tags.join(','),
},
headers: {Cookie: await this.authCookie},
headers: await this.getRequestHeaders(),
})
.then(() => {
// returns nothing
@@ -440,7 +442,7 @@ class ClientRequestManager {
hashes: hashes.join('|').toLowerCase(),
tags: tags?.join(','),
},
headers: {Cookie: await this.authCookie},
headers: await this.getRequestHeaders(),
})
.then(() => {
// returns nothing
@@ -455,7 +457,7 @@ class ClientRequestManager {
hash: hash.toLowerCase(),
urls: urls.join('\n'),
},
headers: {Cookie: await this.authCookie},
headers: await this.getRequestHeaders(),
})
.then(() => {
// returns nothing
@@ -470,7 +472,7 @@ class ClientRequestManager {
params: {
hashes: hashes.join('|').toLowerCase(),
},
headers: {Cookie: await this.authCookie},
headers: await this.getRequestHeaders(),
})
.then(() => {
// returns nothing
@@ -486,7 +488,7 @@ class ClientRequestManager {
hash: hash.toLowerCase(),
urls: urls.join('|'),
},
headers: {Cookie: await this.authCookie},
headers: await this.getRequestHeaders(),
})
.then(() => {
// returns nothing
@@ -502,7 +504,7 @@ class ClientRequestManager {
hashes: hashes.join('|').toLowerCase(),
value: value ? 'true' : 'false',
},
headers: {Cookie: await this.authCookie},
headers: await this.getRequestHeaders(),
})
.then(() => {
// returns nothing
@@ -517,7 +519,7 @@ class ClientRequestManager {
params: {
hashes: hashes.join('|').toLowerCase(),
},
headers: {Cookie: await this.authCookie},
headers: await this.getRequestHeaders(),
})
.then(() => {
// returns nothing
@@ -533,7 +535,7 @@ class ClientRequestManager {
id: ids.join('|'),
priority,
},
headers: {Cookie: await this.authCookie},
headers: await this.getRequestHeaders(),
})
.then(() => {
// returns nothing

View File

@@ -50,7 +50,7 @@ export type QBittorrentMainData = Required<
export interface QBittorrentSyncTorrentPeers {
rid: number;
peers?: {
peers: {
[ip_and_port: string]: QBittorrentTorrentPeer;
};
peers_removed?: string[];

View File

@@ -1,4 +1,4 @@
import axios, {AxiosError, AxiosResponse} from 'axios';
import axios, {AxiosError} from 'axios';
import fs from 'fs';
import path from 'path';
@@ -26,7 +26,7 @@ export const fetchUrls = async (
}
: undefined,
}).then(
(res: AxiosResponse) => res.data,
(res) => res.data,
(e: AxiosError) => console.error(e),
);

View File

@@ -25,6 +25,12 @@ export interface AuthAuthenticationResponse {
export const authRegistrationSchema = credentialsSchema;
export type AuthRegistrationOptions = Required<zodInfer<typeof authRegistrationSchema>>;
// POST /api/auth/register - success response
export interface AuthRegistrationResponse {
username: string;
level: AccessLevel;
}
// PATCH /api/auth/users/{username}
export const authUpdateUserSchema = credentialsSchema.partial();
export type AuthUpdateUserOptions = zodInfer<typeof authUpdateUserSchema>;

View File

@@ -2,3 +2,16 @@ import type {FloodSettings} from '../FloodSettings';
// PATCH /api/settings
export type SetFloodSettingsOptions = Partial<FloodSettings>;
// GET /api/directory-list
export interface DirectoryListQuery {
path: string;
}
// GET /api/directory-list - success response
export interface DirectoryListResponse {
path: string;
separator: string;
directories: string[];
files: string[];
}

View File

@@ -104,3 +104,13 @@ export interface SetTorrentContentsPropertiesOptions {
// Number representing priority
priority: TorrentContentPriority;
}
// GET /api/torrents/{hash}/mediainfo - success response
export interface TorrentMediainfoResponse {
output: string;
}
// POST /api/torrents/add-urls - success response
// POST /api/torrents/add-files - success response
// hashes of torrents added
export type TorrentAddResponse = string[];