pull/1403/head^2
parent
09a31cf9a4
commit
72b6ab3c6a
@ -1,23 +1,19 @@
|
||||
# coding=utf-8
|
||||
|
||||
import json
|
||||
from app import socketio
|
||||
|
||||
|
||||
def event_stream(type=None, action=None, series=None, episode=None, movie=None, task=None):
|
||||
def event_stream(type, action="update", payload=None):
|
||||
"""
|
||||
:param type: The type of element.
|
||||
:type type: str
|
||||
:param action: The action type of element from insert, update, delete.
|
||||
:param action: The action type of element from update and delete.
|
||||
:type action: str
|
||||
:param series: The series id.
|
||||
:type series: str
|
||||
:param episode: The episode id.
|
||||
:type episode: str
|
||||
:param movie: The movie id.
|
||||
:type movie: str
|
||||
:param task: The task id.
|
||||
:type task: str
|
||||
:param payload: The payload to send, can be anything
|
||||
"""
|
||||
socketio.emit('event', json.dumps({"type": type, "action": action, "series": series, "episode": episode,
|
||||
"movie": movie, "task": task}))
|
||||
|
||||
try:
|
||||
payload = int(payload)
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
socketio.emit("data", {"type": type, "action": action, "payload": payload})
|
||||
|
@ -0,0 +1,116 @@
|
||||
# coding=utf-8
|
||||
|
||||
import os
|
||||
import requests
|
||||
import logging
|
||||
|
||||
from config import settings, url_sonarr, url_radarr
|
||||
from helper import path_mappings
|
||||
from database import database
|
||||
|
||||
headers = {"User-Agent": os.environ["SZ_USER_AGENT"]}
|
||||
|
||||
|
||||
def get_sonarr_rootfolder():
|
||||
apikey_sonarr = settings.sonarr.apikey
|
||||
sonarr_rootfolder = []
|
||||
|
||||
# Get root folder data from Sonarr
|
||||
url_sonarr_api_rootfolder = url_sonarr() + "/api/rootfolder?apikey=" + apikey_sonarr
|
||||
|
||||
try:
|
||||
rootfolder = requests.get(url_sonarr_api_rootfolder, timeout=60, verify=False, headers=headers)
|
||||
except requests.exceptions.ConnectionError:
|
||||
logging.exception("BAZARR Error trying to get rootfolder from Sonarr. Connection Error.")
|
||||
return []
|
||||
except requests.exceptions.Timeout:
|
||||
logging.exception("BAZARR Error trying to get rootfolder from Sonarr. Timeout Error.")
|
||||
return []
|
||||
except requests.exceptions.RequestException:
|
||||
logging.exception("BAZARR Error trying to get rootfolder from Sonarr.")
|
||||
return []
|
||||
else:
|
||||
for folder in rootfolder.json():
|
||||
sonarr_rootfolder.append({'id': folder['id'], 'path': folder['path']})
|
||||
db_rootfolder = database.execute('SELECT id, path FROM table_shows_rootfolder')
|
||||
rootfolder_to_remove = [x for x in db_rootfolder if not
|
||||
next((item for item in sonarr_rootfolder if item['id'] == x['id']), False)]
|
||||
rootfolder_to_update = [x for x in sonarr_rootfolder if
|
||||
next((item for item in db_rootfolder if item['id'] == x['id']), False)]
|
||||
rootfolder_to_insert = [x for x in sonarr_rootfolder if not
|
||||
next((item for item in db_rootfolder if item['id'] == x['id']), False)]
|
||||
|
||||
for item in rootfolder_to_remove:
|
||||
database.execute('DELETE FROM table_shows_rootfolder WHERE id = ?', (item['id'],))
|
||||
for item in rootfolder_to_update:
|
||||
database.execute('UPDATE table_shows_rootfolder SET path=? WHERE id = ?', (item['path'], item['id']))
|
||||
for item in rootfolder_to_insert:
|
||||
database.execute('INSERT INTO table_shows_rootfolder (id, path) VALUES (?, ?)', (item['id'], item['path']))
|
||||
|
||||
|
||||
def check_sonarr_rootfolder():
|
||||
get_sonarr_rootfolder()
|
||||
rootfolder = database.execute('SELECT id, path FROM table_shows_rootfolder')
|
||||
for item in rootfolder:
|
||||
if not os.path.isdir(path_mappings.path_replace(item['path'])):
|
||||
database.execute("UPDATE table_shows_rootfolder SET accessible = 0, error = 'This Sonarr root directory "
|
||||
"does not seems to be accessible by Bazarr. Please check path mapping.' WHERE id = ?",
|
||||
(item['id'],))
|
||||
elif not os.access(path_mappings.path_replace(item['path']), os.W_OK):
|
||||
database.execute("UPDATE table_shows_rootfolder SET accessible = 0, error = 'Bazarr cannot write to "
|
||||
"this directory' WHERE id = ?", (item['id'],))
|
||||
else:
|
||||
database.execute("UPDATE table_shows_rootfolder SET accessible = 1, error = '' WHERE id = ?", (item['id'],))
|
||||
|
||||
|
||||
def get_radarr_rootfolder():
|
||||
apikey_radarr = settings.radarr.apikey
|
||||
radarr_rootfolder = []
|
||||
|
||||
# Get root folder data from Radarr
|
||||
url_radarr_api_rootfolder = url_radarr() + "/api/rootfolder?apikey=" + apikey_radarr
|
||||
|
||||
try:
|
||||
rootfolder = requests.get(url_radarr_api_rootfolder, timeout=60, verify=False, headers=headers)
|
||||
except requests.exceptions.ConnectionError:
|
||||
logging.exception("BAZARR Error trying to get rootfolder from Radarr. Connection Error.")
|
||||
return []
|
||||
except requests.exceptions.Timeout:
|
||||
logging.exception("BAZARR Error trying to get rootfolder from Radarr. Timeout Error.")
|
||||
return []
|
||||
except requests.exceptions.RequestException:
|
||||
logging.exception("BAZARR Error trying to get rootfolder from Radarr.")
|
||||
return []
|
||||
else:
|
||||
for folder in rootfolder.json():
|
||||
radarr_rootfolder.append({'id': folder['id'], 'path': folder['path']})
|
||||
db_rootfolder = database.execute('SELECT id, path FROM table_movies_rootfolder')
|
||||
rootfolder_to_remove = [x for x in db_rootfolder if not
|
||||
next((item for item in radarr_rootfolder if item['id'] == x['id']), False)]
|
||||
rootfolder_to_update = [x for x in radarr_rootfolder if
|
||||
next((item for item in db_rootfolder if item['id'] == x['id']), False)]
|
||||
rootfolder_to_insert = [x for x in radarr_rootfolder if not
|
||||
next((item for item in db_rootfolder if item['id'] == x['id']), False)]
|
||||
|
||||
for item in rootfolder_to_remove:
|
||||
database.execute('DELETE FROM table_movies_rootfolder WHERE id = ?', (item['id'],))
|
||||
for item in rootfolder_to_update:
|
||||
database.execute('UPDATE table_movies_rootfolder SET path=? WHERE id = ?', (item['path'], item['id']))
|
||||
for item in rootfolder_to_insert:
|
||||
database.execute('INSERT INTO table_movies_rootfolder (id, path) VALUES (?, ?)', (item['id'], item['path']))
|
||||
|
||||
|
||||
def check_radarr_rootfolder():
|
||||
get_radarr_rootfolder()
|
||||
rootfolder = database.execute('SELECT id, path FROM table_movies_rootfolder')
|
||||
for item in rootfolder:
|
||||
if not os.path.isdir(path_mappings.path_replace_movie(item['path'])):
|
||||
database.execute("UPDATE table_movies_rootfolder SET accessible = 0, error = 'This Radarr root directory "
|
||||
"does not seems to be accessible by Bazarr. Please check path mapping.' WHERE id = ?",
|
||||
(item['id'],))
|
||||
elif not os.access(path_mappings.path_replace_movie(item['path']), os.W_OK):
|
||||
database.execute("UPDATE table_movies_rootfolder SET accessible = 0, error = 'Bazarr cannot write to "
|
||||
"this directory' WHERE id = ?", (item['id'],))
|
||||
else:
|
||||
database.execute("UPDATE table_movies_rootfolder SET accessible = 1, error = '' WHERE id = ?",
|
||||
(item['id'],))
|
@ -1,5 +1,4 @@
|
||||
export * from "./movie";
|
||||
export * from "./providers";
|
||||
export * from "./series";
|
||||
export * from "./site";
|
||||
export * from "./system";
|
||||
|
@ -1,13 +0,0 @@
|
||||
import { ProvidersApi } from "../../apis";
|
||||
import { PROVIDER_UPDATE_LIST } from "../constants";
|
||||
import { createAsyncAction, createCombineAction } from "./factory";
|
||||
import { badgeUpdateAll } from "./site";
|
||||
|
||||
const providerUpdateList = createAsyncAction(PROVIDER_UPDATE_LIST, () =>
|
||||
ProvidersApi.providers()
|
||||
);
|
||||
|
||||
export const providerUpdateAll = createCombineAction(() => [
|
||||
providerUpdateList(),
|
||||
badgeUpdateAll(),
|
||||
]);
|
@ -1,112 +0,0 @@
|
||||
import { mergeArray } from "../../utilites";
|
||||
import { AsyncAction } from "../types";
|
||||
|
||||
export function updateAsyncState<Payload>(
|
||||
action: AsyncAction<Payload>,
|
||||
defVal: Readonly<Payload>
|
||||
): AsyncState<Payload> {
|
||||
if (action.payload.loading) {
|
||||
return {
|
||||
updating: true,
|
||||
data: defVal,
|
||||
};
|
||||
} else if (action.error !== undefined) {
|
||||
return {
|
||||
updating: false,
|
||||
error: action.payload.item as Error,
|
||||
data: defVal,
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
updating: false,
|
||||
error: undefined,
|
||||
data: action.payload.item as Payload,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export function updateOrderIdState<T extends LooseObject>(
|
||||
action: AsyncAction<AsyncDataWrapper<T>>,
|
||||
state: AsyncState<OrderIdState<T>>,
|
||||
id: ItemIdType<T>
|
||||
): AsyncState<OrderIdState<T>> {
|
||||
if (action.payload.loading) {
|
||||
return {
|
||||
...state,
|
||||
updating: true,
|
||||
};
|
||||
} else if (action.error !== undefined) {
|
||||
return {
|
||||
...state,
|
||||
updating: false,
|
||||
error: action.payload.item as Error,
|
||||
};
|
||||
} else {
|
||||
const { data, total } = action.payload.item as AsyncDataWrapper<T>;
|
||||
const [start, length] = action.payload.parameters;
|
||||
|
||||
// Convert item list to object
|
||||
const idState: IdState<T> = data.reduce<IdState<T>>((prev, curr) => {
|
||||
const tid = curr[id];
|
||||
prev[tid] = curr;
|
||||
return prev;
|
||||
}, {});
|
||||
|
||||
const dataOrder: number[] = data.map((v) => v[id]);
|
||||
|
||||
let newItems = { ...state.data.items, ...idState };
|
||||
let newOrder = state.data.order;
|
||||
|
||||
const countDist = total - newOrder.length;
|
||||
if (countDist > 0) {
|
||||
newOrder.push(...Array(countDist).fill(null));
|
||||
} else if (countDist < 0) {
|
||||
// Completely drop old data if list has shrinked
|
||||
newOrder = Array(total).fill(null);
|
||||
newItems = { ...idState };
|
||||
}
|
||||
|
||||
if (typeof start === "number" && typeof length === "number") {
|
||||
newOrder.splice(start, length, ...dataOrder);
|
||||
} else if (start === undefined) {
|
||||
// Full Update
|
||||
newOrder = dataOrder;
|
||||
}
|
||||
|
||||
return {
|
||||
updating: false,
|
||||
data: {
|
||||
items: newItems,
|
||||
order: newOrder,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export function updateAsyncList<T, ID extends keyof T>(
|
||||
action: AsyncAction<T[]>,
|
||||
state: AsyncState<T[]>,
|
||||
match: ID
|
||||
): AsyncState<T[]> {
|
||||
if (action.payload.loading) {
|
||||
return {
|
||||
...state,
|
||||
updating: true,
|
||||
};
|
||||
} else if (action.error !== undefined) {
|
||||
return {
|
||||
...state,
|
||||
updating: false,
|
||||
error: action.payload.item as Error,
|
||||
};
|
||||
} else {
|
||||
const list = state.data as T[];
|
||||
const payload = action.payload.item as T[];
|
||||
const result = mergeArray(list, payload, (l, r) => l[match] === r[match]);
|
||||
|
||||
return {
|
||||
updating: false,
|
||||
data: result,
|
||||
};
|
||||
}
|
||||
}
|
@ -0,0 +1,10 @@
|
||||
export function defaultAOS(): AsyncOrderState<any> {
|
||||
return {
|
||||
updating: true,
|
||||
data: {
|
||||
items: [],
|
||||
order: [],
|
||||
fetched: false,
|
||||
},
|
||||
};
|
||||
}
|
@ -0,0 +1,181 @@
|
||||
import { difference, has, isArray, isNull, isNumber, uniqBy } from "lodash";
|
||||
import { Action } from "redux-actions";
|
||||
import { conditionalLog } from "../../utilites/logger";
|
||||
import { AsyncAction } from "../types";
|
||||
|
||||
export function updateAsyncState<Payload>(
|
||||
action: AsyncAction<Payload>,
|
||||
defVal: Readonly<Payload>
|
||||
): AsyncState<Payload> {
|
||||
if (action.payload.loading) {
|
||||
return {
|
||||
updating: true,
|
||||
data: defVal,
|
||||
};
|
||||
} else if (action.error !== undefined) {
|
||||
return {
|
||||
updating: false,
|
||||
error: action.payload.item as Error,
|
||||
data: defVal,
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
updating: false,
|
||||
error: undefined,
|
||||
data: action.payload.item as Payload,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export function updateOrderIdState<T extends LooseObject>(
|
||||
action: AsyncAction<AsyncDataWrapper<T>>,
|
||||
state: AsyncOrderState<T>,
|
||||
id: ItemIdType<T>
|
||||
): AsyncOrderState<T> {
|
||||
if (action.payload.loading) {
|
||||
return {
|
||||
data: {
|
||||
...state.data,
|
||||
fetched: true,
|
||||
},
|
||||
updating: true,
|
||||
};
|
||||
} else if (action.error !== undefined) {
|
||||
return {
|
||||
data: {
|
||||
...state.data,
|
||||
fetched: true,
|
||||
},
|
||||
updating: false,
|
||||
error: action.payload.item as Error,
|
||||
};
|
||||
} else {
|
||||
const { data, total } = action.payload.item as AsyncDataWrapper<T>;
|
||||
const { parameters } = action.payload;
|
||||
const [start, length] = parameters;
|
||||
|
||||
// Convert item list to object
|
||||
const newItems = data.reduce<IdState<T>>(
|
||||
(prev, curr) => {
|
||||
const tid = curr[id];
|
||||
prev[tid] = curr;
|
||||
return prev;
|
||||
},
|
||||
{ ...state.data.items }
|
||||
);
|
||||
|
||||
let newOrder = [...state.data.order];
|
||||
|
||||
const countDist = total - newOrder.length;
|
||||
if (countDist > 0) {
|
||||
newOrder = Array(countDist).fill(null).concat(newOrder);
|
||||
} else if (countDist < 0) {
|
||||
// Completely drop old data if list has shrinked
|
||||
newOrder = Array(total).fill(null);
|
||||
}
|
||||
|
||||
const idList = newOrder.filter(isNumber);
|
||||
|
||||
const dataOrder: number[] = data.map((v) => v[id]);
|
||||
|
||||
if (typeof start === "number" && typeof length === "number") {
|
||||
newOrder.splice(start, length, ...dataOrder);
|
||||
} else if (isArray(start)) {
|
||||
// Find the null values and delete them, insert new values to the front of array
|
||||
const addition = difference(dataOrder, idList);
|
||||
let addCount = addition.length;
|
||||
newOrder.unshift(...addition);
|
||||
|
||||
newOrder = newOrder.flatMap((v) => {
|
||||
if (isNull(v) && addCount > 0) {
|
||||
--addCount;
|
||||
return [];
|
||||
} else {
|
||||
return [v];
|
||||
}
|
||||
}, []);
|
||||
|
||||
conditionalLog(
|
||||
addCount !== 0,
|
||||
"Error when replacing item in OrderIdState"
|
||||
);
|
||||
} else if (parameters.length === 0) {
|
||||
// TODO: Delete me -> Full Update
|
||||
newOrder = dataOrder;
|
||||
}
|
||||
|
||||
return {
|
||||
updating: false,
|
||||
data: {
|
||||
fetched: true,
|
||||
items: newItems,
|
||||
order: newOrder,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export function deleteOrderListItemBy<T extends LooseObject>(
|
||||
action: Action<number[]>,
|
||||
state: AsyncOrderState<T>
|
||||
): AsyncOrderState<T> {
|
||||
const ids = action.payload;
|
||||
const { items, order } = state.data;
|
||||
const newItems = { ...items };
|
||||
ids.forEach((v) => {
|
||||
if (has(newItems, v)) {
|
||||
delete newItems[v];
|
||||
}
|
||||
});
|
||||
const newOrder = difference(order, ids);
|
||||
return {
|
||||
...state,
|
||||
data: {
|
||||
fetched: true,
|
||||
items: newItems,
|
||||
order: newOrder,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function deleteAsyncListItemBy<T extends LooseObject>(
|
||||
action: Action<number[]>,
|
||||
state: AsyncState<T[]>,
|
||||
match: ItemIdType<T>
|
||||
): AsyncState<T[]> {
|
||||
const ids = new Set(action.payload);
|
||||
const data = [...state.data].filter((v) => !ids.has(v[match]));
|
||||
return {
|
||||
...state,
|
||||
data,
|
||||
};
|
||||
}
|
||||
|
||||
export function updateAsyncList<T, ID extends keyof T>(
|
||||
action: AsyncAction<T[]>,
|
||||
state: AsyncState<T[]>,
|
||||
match: ID
|
||||
): AsyncState<T[]> {
|
||||
if (action.payload.loading) {
|
||||
return {
|
||||
...state,
|
||||
updating: true,
|
||||
};
|
||||
} else if (action.error !== undefined) {
|
||||
return {
|
||||
...state,
|
||||
updating: false,
|
||||
error: action.payload.item as Error,
|
||||
};
|
||||
} else {
|
||||
const olds = state.data as T[];
|
||||
const news = action.payload.item as T[];
|
||||
|
||||
const result = uniqBy([...news, ...olds], match);
|
||||
|
||||
return {
|
||||
updating: false,
|
||||
data: result,
|
||||
};
|
||||
}
|
||||
}
|
@ -0,0 +1,35 @@
|
||||
import { useCallback, useEffect, useMemo } from "react";
|
||||
import Socketio from ".";
|
||||
import { log } from "../utilites/logger";
|
||||
|
||||
export function useSocketIOReducer(
|
||||
key: SocketIO.EventType,
|
||||
any?: () => void,
|
||||
update?: SocketIO.ActionFn,
|
||||
remove?: SocketIO.ActionFn
|
||||
) {
|
||||
const reducer = useMemo<SocketIO.Reducer>(
|
||||
() => ({ key, any, update, delete: remove }),
|
||||
[key, any, update, remove]
|
||||
);
|
||||
useEffect(() => {
|
||||
Socketio.addReducer(reducer);
|
||||
log("info", "listening to SocketIO event", key);
|
||||
return () => {
|
||||
Socketio.removeReducer(reducer);
|
||||
};
|
||||
}, [reducer, key]);
|
||||
}
|
||||
|
||||
export function useWrapToOptionalId(
|
||||
fn: (id: number[]) => void
|
||||
): SocketIO.ActionFn {
|
||||
return useCallback(
|
||||
(id?: number[]) => {
|
||||
if (id) {
|
||||
fn(id);
|
||||
}
|
||||
},
|
||||
[fn]
|
||||
);
|
||||
}
|
@ -0,0 +1,123 @@
|
||||
import { debounce, forIn, remove, uniq } from "lodash";
|
||||
import { io, Socket } from "socket.io-client";
|
||||
import { getBaseUrl } from "../utilites";
|
||||
import { conditionalLog, log } from "../utilites/logger";
|
||||
import { createDefaultReducer } from "./reducer";
|
||||
|
||||
class SocketIOClient {
|
||||
private socket: Socket;
|
||||
private events: SocketIO.Event[];
|
||||
private debounceReduce: () => void;
|
||||
|
||||
private reducers: SocketIO.Reducer[];
|
||||
|
||||
constructor() {
|
||||
const baseUrl = getBaseUrl();
|
||||
this.socket = io({
|
||||
path: `${baseUrl}/api/socket.io`,
|
||||
transports: ["polling", "websocket"],
|
||||
upgrade: true,
|
||||
rememberUpgrade: true,
|
||||
autoConnect: false,
|
||||
});
|
||||
|
||||
this.socket.on("connect", this.onConnect.bind(this));
|
||||
this.socket.on("disconnect", this.onDisconnect.bind(this));
|
||||
this.socket.on("connect_error", this.onDisconnect.bind(this));
|
||||
this.socket.on("data", this.onEvent.bind(this));
|
||||
|
||||
this.events = [];
|
||||
this.debounceReduce = debounce(this.reduce, 200);
|
||||
this.reducers = [];
|
||||
}
|
||||
|
||||
initialize() {
|
||||
this.reducers.push(...createDefaultReducer());
|
||||
this.socket.connect();
|
||||
|
||||
// Debug Command
|
||||
window._socketio = {
|
||||
dump: this.dump.bind(this),
|
||||
emit: this.onEvent.bind(this),
|
||||
};
|
||||
}
|
||||
|
||||
private dump() {
|
||||
console.log("SocketIO reducers", this.reducers);
|
||||
}
|
||||
|
||||
addReducer(reducer: SocketIO.Reducer) {
|
||||
this.reducers.push(reducer);
|
||||
}
|
||||
|
||||
removeReducer(reducer: SocketIO.Reducer) {
|
||||
const removed = remove(this.reducers, (r) => r === reducer);
|
||||
conditionalLog(removed.length === 0, "Fail to remove reducer", reducer);
|
||||
}
|
||||
|
||||
private reduce() {
|
||||
const events = [...this.events];
|
||||
this.events = [];
|
||||
|
||||
const records: SocketIO.ActionRecord = {};
|
||||
|
||||
events.forEach((e) => {
|
||||
if (!(e.type in records)) {
|
||||
records[e.type] = {};
|
||||
}
|
||||
const record = records[e.type]!;
|
||||
if (!(e.action in record)) {
|
||||
record[e.action] = [];
|
||||
}
|
||||
if (e.payload) {
|
||||
record[e.action]?.push(e.payload);
|
||||
}
|
||||
});
|
||||
|
||||
forIn(records, (element, type) => {
|
||||
if (element) {
|
||||
const handlers = this.reducers.filter((v) => v.key === type);
|
||||
if (handlers.length === 0) {
|
||||
log("warning", "Unhandle SocketIO event", type);
|
||||
return;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-loop-func
|
||||
handlers.forEach((handler) => {
|
||||
const anyAction = handler.any;
|
||||
if (anyAction) {
|
||||
anyAction();
|
||||
}
|
||||
|
||||
forIn(element, (ids, key) => {
|
||||
ids = uniq(ids);
|
||||
const action = handler[key as SocketIO.ActionType];
|
||||
if (action) {
|
||||
action(ids);
|
||||
} else if (anyAction === undefined) {
|
||||
log("warning", "Unhandle action of SocketIO event", key, type);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private onConnect() {
|
||||
log("info", "Socket.IO has connected");
|
||||
this.onEvent({ type: "connect", action: "update", payload: null });
|
||||
}
|
||||
|
||||
private onDisconnect() {
|
||||
log("warning", "Socket.IO has disconnected");
|
||||
this.onEvent({ type: "disconnect", action: "update", payload: null });
|
||||
}
|
||||
|
||||
private onEvent(event: SocketIO.Event) {
|
||||
log("info", "Socket.IO receives", event);
|
||||
this.events.push(event);
|
||||
this.debounceReduce();
|
||||
}
|
||||
}
|
||||
|
||||
export default new SocketIOClient();
|
@ -0,0 +1,55 @@
|
||||
import {
|
||||
badgeUpdateAll,
|
||||
bootstrap,
|
||||
movieDeleteItems,
|
||||
movieUpdateList,
|
||||
seriesDeleteItems,
|
||||
seriesUpdateList,
|
||||
siteUpdateOffline,
|
||||
systemUpdateLanguagesAll,
|
||||
systemUpdateSettings,
|
||||
} from "../@redux/actions";
|
||||
import reduxStore from "../@redux/store";
|
||||
|
||||
function bindToReduxStore(fn: (ids?: number[]) => any): SocketIO.ActionFn {
|
||||
return (ids?: number[]) => reduxStore.dispatch(fn(ids));
|
||||
}
|
||||
|
||||
export function createDefaultReducer(): SocketIO.Reducer[] {
|
||||
return [
|
||||
{
|
||||
key: "connect",
|
||||
any: () => reduxStore.dispatch(siteUpdateOffline(false)),
|
||||
},
|
||||
{
|
||||
key: "connect",
|
||||
any: () => reduxStore.dispatch<any>(bootstrap()),
|
||||
},
|
||||
{
|
||||
key: "disconnect",
|
||||
any: () => reduxStore.dispatch(siteUpdateOffline(true)),
|
||||
},
|
||||
{
|
||||
key: "series",
|
||||
update: bindToReduxStore(seriesUpdateList),
|
||||
delete: bindToReduxStore(seriesDeleteItems),
|
||||
},
|
||||
{
|
||||
key: "movie",
|
||||
update: bindToReduxStore(movieUpdateList),
|
||||
delete: bindToReduxStore(movieDeleteItems),
|
||||
},
|
||||
{
|
||||
key: "settings",
|
||||
any: bindToReduxStore(systemUpdateSettings),
|
||||
},
|
||||
{
|
||||
key: "languages",
|
||||
any: bindToReduxStore(systemUpdateLanguagesAll),
|
||||
},
|
||||
{
|
||||
key: "badges",
|
||||
any: bindToReduxStore(badgeUpdateAll),
|
||||
},
|
||||
];
|
||||
}
|
@ -0,0 +1,39 @@
|
||||
namespace SocketIO {
|
||||
type EventType =
|
||||
| "connect"
|
||||
| "disconnect"
|
||||
| "movie"
|
||||
| "series"
|
||||
| "episode"
|
||||
| "episode-history"
|
||||
| "episode-blacklist"
|
||||
| "episode-wanted"
|
||||
| "movie-history"
|
||||
| "movie-blacklist"
|
||||
| "movie-wanted"
|
||||
| "badges"
|
||||
| "task"
|
||||
| "settings"
|
||||
| "languages"
|
||||
| "message";
|
||||
|
||||
type ActionType = "update" | "delete";
|
||||
|
||||
interface Event {
|
||||
type: EventType;
|
||||
action: ActionType;
|
||||
payload: any; // TODO: Use specific types
|
||||
}
|
||||
|
||||
type ActionFn = (payload?: any[]) => void;
|
||||
|
||||
type Reducer = {
|
||||
key: EventType;
|
||||
any?: () => any;
|
||||
} & Partial<Record<ActionType, ActionFn>>;
|
||||
|
||||
type ActionRecord = OptionalRecord<
|
||||
EventType,
|
||||
OptionalRecord<ActionType, any[]>
|
||||
>;
|
||||
}
|
@ -0,0 +1,27 @@
|
||||
import React, { FunctionComponent, useMemo } from "react";
|
||||
import { Column } from "react-table";
|
||||
import { SimpleTable } from "../../components";
|
||||
|
||||
interface Props {
|
||||
health: readonly System.Health[];
|
||||
}
|
||||
|
||||
const Table: FunctionComponent<Props> = (props) => {
|
||||
const columns: Column<System.Health>[] = useMemo<Column<System.Health>[]>(
|
||||
() => [
|
||||
{
|
||||
Header: "Object",
|
||||
accessor: "object",
|
||||
},
|
||||
{
|
||||
Header: "Issue",
|
||||
accessor: "issue",
|
||||
},
|
||||
],
|
||||
[]
|
||||
);
|
||||
|
||||
return <SimpleTable columns={columns} data={props.health}></SimpleTable>;
|
||||
};
|
||||
|
||||
export default Table;
|
@ -0,0 +1,128 @@
|
||||
import { isNull } from "lodash";
|
||||
import React, { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { PluginHook, TableOptions, useTable } from "react-table";
|
||||
import { LoadingIndicator } from "..";
|
||||
import { useReduxStore } from "../../@redux/hooks/base";
|
||||
import { buildOrderListFrom, isNonNullable, ScrollToTop } from "../../utilites";
|
||||
import BaseTable, { TableStyleProps, useStyleAndOptions } from "./BaseTable";
|
||||
import PageControl from "./PageControl";
|
||||
import { useDefaultSettings } from "./plugins";
|
||||
|
||||
type Props<T extends object> = TableOptions<T> &
|
||||
TableStyleProps<T> & {
|
||||
plugins?: PluginHook<T>[];
|
||||
aos: AsyncOrderState<T>;
|
||||
loader: (start: number, length: number) => void;
|
||||
};
|
||||
|
||||
export default function AsyncPageTable<T extends object>(props: Props<T>) {
|
||||
const { aos, plugins, loader, ...remain } = props;
|
||||
const { style, options } = useStyleAndOptions(remain);
|
||||
|
||||
const {
|
||||
updating,
|
||||
data: { order, items, fetched },
|
||||
} = aos;
|
||||
|
||||
const allPlugins: PluginHook<T>[] = [useDefaultSettings];
|
||||
|
||||
if (plugins) {
|
||||
allPlugins.push(...plugins);
|
||||
}
|
||||
|
||||
// Impl a new pagination system instead of hooking into the existing one
|
||||
const [pageIndex, setIndex] = useState(0);
|
||||
const pageSize = useReduxStore((s) => s.site.pageSize);
|
||||
const totalRows = order.length;
|
||||
const pageCount = Math.ceil(totalRows / pageSize);
|
||||
|
||||
const previous = useCallback(() => {
|
||||
setIndex((idx) => idx - 1);
|
||||
}, []);
|
||||
|
||||
const next = useCallback(() => {
|
||||
setIndex((idx) => idx + 1);
|
||||
}, []);
|
||||
|
||||
const goto = useCallback((idx: number) => {
|
||||
setIndex(idx);
|
||||
}, []);
|
||||
|
||||
const pageStart = pageIndex * pageSize;
|
||||
const pageEnd = pageStart + pageSize;
|
||||
|
||||
const visibleItemIds = useMemo(() => order.slice(pageStart, pageEnd), [
|
||||
pageStart,
|
||||
pageEnd,
|
||||
order,
|
||||
]);
|
||||
|
||||
const newData = useMemo(() => buildOrderListFrom(items, visibleItemIds), [
|
||||
items,
|
||||
visibleItemIds,
|
||||
]);
|
||||
|
||||
const newOptions = useMemo<TableOptions<T>>(
|
||||
() => ({
|
||||
...options,
|
||||
data: newData,
|
||||
}),
|
||||
[options, newData]
|
||||
);
|
||||
|
||||
const instance = useTable(newOptions, ...allPlugins);
|
||||
|
||||
const {
|
||||
getTableProps,
|
||||
getTableBodyProps,
|
||||
headerGroups,
|
||||
rows,
|
||||
prepareRow,
|
||||
} = instance;
|
||||
|
||||
useEffect(() => {
|
||||
ScrollToTop();
|
||||
}, [pageIndex]);
|
||||
|
||||
useEffect(() => {
|
||||
const needInit = visibleItemIds.length === 0 && fetched === false;
|
||||
const needRefresh = !visibleItemIds.every(isNonNullable);
|
||||
if (needInit || needRefresh) {
|
||||
loader(pageStart, pageSize);
|
||||
}
|
||||
}, [visibleItemIds, pageStart, pageSize, loader, fetched]);
|
||||
|
||||
const showLoading = useMemo(
|
||||
() =>
|
||||
updating && (visibleItemIds.every(isNull) || visibleItemIds.length === 0),
|
||||
[visibleItemIds, updating]
|
||||
);
|
||||
|
||||
if (showLoading) {
|
||||
return <LoadingIndicator></LoadingIndicator>;
|
||||
}
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<BaseTable
|
||||
{...style}
|
||||
headers={headerGroups}
|
||||
rows={rows}
|
||||
prepareRow={prepareRow}
|
||||
tableProps={getTableProps()}
|
||||
tableBodyProps={getTableBodyProps()}
|
||||
></BaseTable>
|
||||
<PageControl
|
||||
count={pageCount}
|
||||
index={pageIndex}
|
||||
size={pageSize}
|
||||
total={totalRows}
|
||||
canPrevious={pageIndex > 0}
|
||||
canNext={pageIndex < pageCount - 1}
|
||||
previous={previous}
|
||||
next={next}
|
||||
goto={goto}
|
||||
></PageControl>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
@ -1,3 +1,4 @@
|
||||
export { default as AsyncPageTable } from "./AsyncPageTable";
|
||||
export { default as GroupTable } from "./GroupTable";
|
||||
export { default as PageTable } from "./PageTable";
|
||||
export { default as SimpleTable } from "./SimpleTable";
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue