From 27314d5b61660349d8ae01e09db0e7e1ba90b358 Mon Sep 17 00:00:00 2001 From: enigma0Z <6117628+enigma0Z@users.noreply.github.com> Date: Mon, 8 Jun 2026 18:13:27 -0400 Subject: [PATCH 01/10] (unrelated) remove old scripts --- attach.sh | 7 ------- run.sh | 3 --- start.sh | 42 ------------------------------------------ stop.sh | 6 ------ 4 files changed, 58 deletions(-) delete mode 100755 attach.sh delete mode 100755 run.sh delete mode 100755 start.sh delete mode 100755 stop.sh diff --git a/attach.sh b/attach.sh deleted file mode 100755 index d09edcf..0000000 --- a/attach.sh +++ /dev/null @@ -1,7 +0,0 @@ -#!/bin/bash - -if [[ "$1" == "api" ]]; then - screen -R makemkv-headless-api -elif [[ "$1" == "web" ]]; then - screen -R makemkv-headless-web -fi; \ No newline at end of file diff --git a/run.sh b/run.sh deleted file mode 100755 index cba9ad8..0000000 --- a/run.sh +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/bash - -uv --project api run mmh "$@" \ No newline at end of file diff --git a/start.sh b/start.sh deleted file mode 100755 index a74c226..0000000 --- a/start.sh +++ /dev/null @@ -1,42 +0,0 @@ -#!/bin/bash - -SCREEN="false" -DEV="false" -API_PORT="" -WEB_PORT="" - -if [[ "$1" == "dev" ]]; then - DEV="true"; shift - API_PORT="$1"; shift - WEB_PORT="$1"; shift -fi - -if [[ "$DEV" == "true" ]]; then - WEB_PID="" - API_PID="" - cleanup() { - echo "Cleaning up" - set -x - kill $WEB_PID - kill $API_PID - set +x - } - - uv --project api sync - uv --project api run mmh start \ - --listen-port $API_PORT "$@" \ - --cors-origin http://127.0.0.1:3000 \ - --cors-origin http://localhost:3000 & - API_PID=$! - - export VITE_BACKEND_PORT=$API_PORT - cd web - npm run dev -- --port 3000 - WEB_PID=$! - - trap 'cleanup' SIGINT - wait $API_PID - wait $WEB_PID -else - ./run.sh start "$@" -fi diff --git a/stop.sh b/stop.sh deleted file mode 100755 index ee31502..0000000 --- a/stop.sh +++ /dev/null @@ -1,6 +0,0 @@ -#!/bin/bash - -for session in $(ls /run/screen/S-$USER/*.mmh-api-*); do - pid=$(basename $session | cut -d. -f1) - kill $pid -done From ab7f6a7bc48b917e4c70924c46972340c2e17f28 Mon Sep 17 00:00:00 2001 From: enigma0Z <6117628+enigma0Z@users.noreply.github.com> Date: Wed, 10 Jun 2026 01:25:32 -0400 Subject: [PATCH 02/10] Rewrite toc and state API clients to not need the redux state store --- api/src/makemkv_headless/api/state.py | 4 + api/src/makemkv_headless/api/v1/state.py | 2 +- api/src/makemkv_headless/api/v1/toc.py | 52 +++---- api/src/makemkv_headless/models/state.py | 26 ++-- web/src/api/endpoints.ts | 40 ++---- web/src/api/index.ts | 56 ++++---- web/src/api/v1/config/api.ts | 8 +- web/src/api/v1/config/store.ts | 22 --- web/src/api/v1/error/api.ts | 2 +- web/src/api/v1/index.ts | 20 ++- web/src/api/v1/socket/api.ts | 3 +- web/src/api/v1/socket/middleware.ts | 12 +- web/src/api/v1/state/api.ts | 27 +++- web/src/api/v1/state/types.ts | 7 +- web/src/api/v1/toc/api.ts | 24 +++- web/src/api/v1/toc/store.tsx | 37 ----- web/src/components/ButtonBar.tsx | 23 +-- .../components/forms/ContentForm/index.tsx | 38 ++--- .../modals/ConfigDialog/LegacyTab.tsx | 62 ++++++++ .../modals/ConfigDialog/index.styles.ts | 19 ++- .../components/modals/ConfigDialog/index.tsx | 132 ++++++++++-------- web/src/components/toc/TocGrid.tsx | 24 ++-- web/src/pages/App.tsx | 16 --- 23 files changed, 351 insertions(+), 305 deletions(-) delete mode 100644 web/src/api/v1/config/store.ts delete mode 100644 web/src/api/v1/toc/store.tsx create mode 100644 web/src/components/modals/ConfigDialog/LegacyTab.tsx diff --git a/api/src/makemkv_headless/api/state.py b/api/src/makemkv_headless/api/state.py index 4d78be4..19833e5 100644 --- a/api/src/makemkv_headless/api/state.py +++ b/api/src/makemkv_headless/api/state.py @@ -3,6 +3,7 @@ from makemkv_headless.models.socket import CurrentProgressMessage, ProgressValueMessage, TotalProgressMessage from makemkv_headless.models.state import ProgressStateModel, StateModel +from makemkv_headless.models.toc import TocStateModel logger = logging.getLogger(__name__) @@ -20,6 +21,9 @@ def reset_all(self): setattr(self, field, default_value) self.model_fields_set.clear() + + def reset_toc(self): + self.toc = TocStateModel() def reset_socket(self): for field, field_info in type(self.socket.rip).model_fields.items(): diff --git a/api/src/makemkv_headless/api/v1/state.py b/api/src/makemkv_headless/api/v1/state.py index 4c29784..5f53ccf 100644 --- a/api/src/makemkv_headless/api/v1/state.py +++ b/api/src/makemkv_headless/api/v1/state.py @@ -67,7 +67,7 @@ def get_state_by_path(path: str, page=None, page_size=None, filter_keys: list[st @router.get('') @router.get('/') def get_state(): - return STATE + return APIResponse(status="success", data=STATE) @router.get('/{state_path:path}') def get_state_select(state_path: str): diff --git a/api/src/makemkv_headless/api/v1/toc.py b/api/src/makemkv_headless/api/v1/toc.py index 83ac049..aa90605 100644 --- a/api/src/makemkv_headless/api/v1/toc.py +++ b/api/src/makemkv_headless/api/v1/toc.py @@ -23,37 +23,40 @@ class TocError(Exception): ... router = APIRouter(prefix="/toc") -# @lru_cache -async def get_toc_from_disc(source): +async def get_toc_from_disc(source = CONFIG.source): with LOCK: toc = Toc() - # STATE.reset_rip_indexes() # Reset which indexes are selected + STATE.reset_toc() + STATE.reset_rip_indexes() # Reset which indexes are selected STATE.reset_socket() # Reset which indexes are complete logger.info('Getting TOC...') await cancellable_async(toc.get_from_disc(source), CANCEL) - STATE.toc = toc + STATE.toc.source = toc.source get_interface().send(TocStatusMessage(state="complete")) return toc @router.get('') @router.get('/') -@router.get('.async') # Deprecated -async def get_toc_async(background_tasks: BackgroundTasks): - try: - if LOCK.locked(): - return APIResponse("in progress") +async def get_toc(): + if LOCK.locked(): + return APIResponse("in progress") + else: + if STATE.toc.source is not None: + return APIResponse(status='success', data=STATE.toc) else: - background_tasks.add_task(get_toc_from_disc, CONFIG.source) - return APIResponse("started") - except TocError as ex: - logger.error(ex.args[0]) - except Exception as ex: - logger.error(format_exc()) - raise APIException(500, GenericAPIError("error", ex)) + return APIResponse(status='stopped') + + +@router.get('.start') +async def get_toc_start(background_tasks: BackgroundTasks): + if LOCK.locked(): + return APIResponse("in progress") + else: + background_tasks.add_task(get_toc_from_disc) + return APIResponse("started") @router.get('.status') -@router.get('.async.status') # Deprecated -async def get_toc_async_status(): +async def get_toc_status(): if LOCK.locked(): return APIResponse("in progress") else: @@ -65,15 +68,4 @@ async def toc_get_stop(): CANCEL.set() return APIResponse("in progress") else: - return APIResponse("done") - -@router.get('.cache.clear') -async def toc_clear_cache(): - try: - get_toc_from_disc.cache_clear() - except Exception as ex: - raise APIException(500, GenericAPIError("error", ex)) - -@router.get('.cache.info') -async def toc_clear_cache(): - return get_toc_from_disc.cache_info() \ No newline at end of file + return APIResponse("done") \ No newline at end of file diff --git a/api/src/makemkv_headless/models/state.py b/api/src/makemkv_headless/models/state.py index f73aa54..e5ce82d 100644 --- a/api/src/makemkv_headless/models/state.py +++ b/api/src/makemkv_headless/models/state.py @@ -19,26 +19,26 @@ ] class RipDestinationModel(BaseModel): - library: str = None - media: str = None - content: str = None + library: str | None = None + media: str | None = None + content: str | None = None class RipSortInfoModel(BaseModel): - name: str = None - id: str = None - main_indexes: list[int] = None - extra_indexes: list[int] = None - split_segments: list[int] = [] + name: str | None = None + id: str | None = None + main_indexes: list[int] | None = None + extra_indexes: list[int] | None = None + split_segments: list[int] | None = [] id_db: str = "tmdbid" class RipShowInfoModel(RipSortInfoModel): - first_episode: int = None - season_number: int = None + first_episode: int | None = None + season_number: int | None = None class RipStateModel(BaseModel): - destination: RipDestinationModel = None - sort_info: RipSortInfoModel | RipShowInfoModel = None - rip_all: bool = None + destination: RipDestinationModel | None = None + sort_info: RipSortInfoModel | RipShowInfoModel | None = None + rip_all: bool | None = None tmdb_selection: TMDBShowSearchResultModel | TMDBMovieSearchResultModel | None = None class ProgressStateModel(BaseModel): diff --git a/web/src/api/endpoints.ts b/web/src/api/endpoints.ts index b34a3d2..26ff925 100644 --- a/web/src/api/endpoints.ts +++ b/web/src/api/endpoints.ts @@ -1,23 +1,11 @@ import type { TmdbConfiguration } from "./v1/tmdb/store" import type { Toc as TocV1 } from "./v1/toc/types" -import type { Response as ResponseV1 } from "./v1" +import { BACKEND, type Response as ResponseV1 } from "./v1" import type { State as StateV1 } from "./v1/state/types" import type { TmdbSearchResultMovie, TmdbSearchResultShow } from "./v1/tmdb/types" import type { Config } from "./v1/config/types" import type { ApiError } from "./v1/error/types" -let backend_port = window.location.port; - -if (import.meta.env.DEV) { - backend_port = import.meta.env.BACKEND_PORT ?? 4000 - console.info(`Setting backend port to ${backend_port}`) -} - -export const BACKEND_HOST_PORT = `${window.location.hostname}:${backend_port}` -export const BACKEND = `${window.location.protocol}//${BACKEND_HOST_PORT}` - -// export const BACKEND = `${window.location.protocol}//${window.location.hostname}:${window.location.port}` - export const endpoints = { disc: { eject: () => `${BACKEND}/api/v1/disc/eject`, @@ -43,34 +31,34 @@ export const endpoints = { export const endpointsV1 = { disc: { - eject: () => `/api/v1/disc/eject`, + eject: () => `/disc/eject`, }, - toc: () => `/api/v1/toc`, + toc: () => `/toc`, state: { - get: (path?: string) => `/api/v1/state${path ? "/" + path : ""}`, - resetSocket: () => `/api/v1/state.reset/socket`, + get: (path: string | void) => `/state${path ? "/" + path : ""}`, + resetSocket: () => `/state.reset/socket`, }, rip: { - start: () => `/api/v1/rip`, - stop: () => `/api/v1/rip.stop` + start: () => `/rip`, + stop: () => `/rip.stop` }, tmdb: { - show: (query: string) => `/api/v1/tmdb/show?${new URLSearchParams({q: query})}`, - movie: (query: string) => `/api/v1/tmdb/movie?${new URLSearchParams({q: query})}`, - configuration: () => `/api/v1/tmdb/configuration` + show: (query: string) => `/tmdb/show?${new URLSearchParams({q: query})}`, + movie: (query: string) => `/tmdb/movie?${new URLSearchParams({q: query})}`, + configuration: () => `/tmdb/configuration` }, config: { - get: () => `/api/v1/config`, + get: () => `/config`, put: () => (body: Partial) => ({ url: '/api/v1/config', method: 'PUT', body }), - reload: () => () => `/api/v1/config.reload` + reload: () => () => `/config.reload` }, error: { - get: () => `/api/v1/error`, - clear: () => `/api/v1/error.clear` + get: () => `/error`, + clear: () => `/error.clear` } } diff --git a/web/src/api/index.ts b/web/src/api/index.ts index 02a8bab..0011a94 100644 --- a/web/src/api/index.ts +++ b/web/src/api/index.ts @@ -1,52 +1,42 @@ import { configureStore, type Middleware, type UnknownAction } from "@reduxjs/toolkit"; import { useDispatch, useSelector } from "react-redux"; -import { throttle } from "lodash"; -import rip, { ripStateIsValid, type RipState } from './v1/rip/store' +import rip, { ripActions, type RipState } from './v1/rip/store' import tmdb, { type TmdbState } from "./v1/tmdb/store"; import socket, { type SocketState } from "./v1/socket/store"; -import { BACKEND, endpoints } from "./endpoints"; -import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/query/react"; -import type { Toc } from "./v1/toc/types"; -import toc from './v1/toc/store' import { SOCKET_URI, SocketConnection } from "./v1/socket/api"; +import { setupListeners } from "@reduxjs/toolkit/query"; +import { stateApi } from "./v1/state/api"; +import { api as apiV1 } from "./v1"; export type RootState = { rip: RipState, - toc: Partial, tmdb: TmdbState, socket: SocketState } -const updateRipStateOnApi = throttle(async (ripState: RipState) => { - if (ripStateIsValid(ripState)) { - console.info("Updating rip state on API") - fetch(endpoints.state.get(), { - method: 'PUT', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ - rip: ripState - }) - }) - } else { - console.info("Cannot update rip state, is not valid", ripState) +const updateApiMiddleware: Middleware<{}, RootState> = store => next => _action => { + const action = (_action as any) + if (action.type === 'api/executeQuery/fulfilled') { + if (action.meta?.arg?.endpointName === 'getState') { + console.log('Update rip state from API', action.payload.rip) + store.dispatch(ripActions.setRipData(action.payload.rip)) + } } -}, 500, { leading: false, trailing: true }) - -export const api = createApi({ - tagTypes: ['error', 'config'], - baseQuery: fetchBaseQuery({ baseUrl: BACKEND }), - endpoints: () => ({}), -}) - -const updateApiMiddleware: Middleware<{}, RootState> = store => next => action => { + if ((action as UnknownAction).type.startsWith('rip/')) { + const currentRipState = store.getState().rip const result = next(action) const nextRipState = store.getState().rip - updateRipStateOnApi(nextRipState) - + if (JSON.stringify(currentRipState) !== JSON.stringify(nextRipState)) { + console.log('Put state into API', currentRipState, nextRipState) + const initiate: UnknownAction = (stateApi.endpoints.putState.initiate({ rip: nextRipState }) as any) + store.dispatch(initiate) + } return result - } else { - return next(action) } + + return next(action) } export type ThunkExtraArgument = { @@ -63,13 +53,15 @@ export const store = configureStore({ }).concat( updateApiMiddleware ).concat( - api.middleware + apiV1.middleware ), reducer: { - rip: rip.reducer, toc, tmdb, socket, api: api.reducer + rip: rip.reducer, tmdb, socket, api: apiV1.reducer }, }) +setupListeners(store.dispatch) + export type AppDispatch = typeof store.dispatch export const useAppDispatch = useDispatch.withTypes() diff --git a/web/src/api/v1/config/api.ts b/web/src/api/v1/config/api.ts index 38994e5..110815a 100644 --- a/web/src/api/v1/config/api.ts +++ b/web/src/api/v1/config/api.ts @@ -1,21 +1,21 @@ -import { api } from "@/api" import { endpointsV1, type ApiModel } from "@/api/endpoints" import type { Config } from "./types" +import { api } from ".." const configApi = api.injectEndpoints({ endpoints: (build) => ({ getConfig: build.query({ - providesTags: ['config'], + providesTags: ['api/config'], query: () => endpointsV1.config.get(), transformResponse: (response) => response.data }), reloadConfig: build.mutation({ - invalidatesTags: ['config'], + invalidatesTags: ['api/config'], query: endpointsV1.config.reload(), transformResponse: (response) => response.data, }), putConfig: build.mutation, ApiModel['v1']['config']>({ - invalidatesTags: ['config'], + invalidatesTags: ['api/config'], query: endpointsV1.config.put(), transformResponse: (response) => response.data, }) diff --git a/web/src/api/v1/config/store.ts b/web/src/api/v1/config/store.ts deleted file mode 100644 index f4cd616..0000000 --- a/web/src/api/v1/config/store.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { createSlice, type PayloadAction } from "@reduxjs/toolkit" -import type { Config } from "./types" - -const initialState: Partial = { } - -const configSlice = createSlice({ - name: "config", - initialState, - reducers: { - updateConfig: (state, action: PayloadAction) => { - if (action.payload) { - state = {...state, ...action.payload} - } else { - state = {...initialState} - } - }, - } -}) - -export const configActions = configSlice.actions - -export default configSlice.reducer \ No newline at end of file diff --git a/web/src/api/v1/error/api.ts b/web/src/api/v1/error/api.ts index 6061527..cceeb4b 100644 --- a/web/src/api/v1/error/api.ts +++ b/web/src/api/v1/error/api.ts @@ -1,6 +1,6 @@ import { type ApiModel, endpointsV1 } from "@/api/endpoints"; -import { api } from "@/api"; import type { ApiError } from "./types"; +import { api } from ".."; // Define a service using a base URL and expected endpoints const errorApi = api.injectEndpoints({ diff --git a/web/src/api/v1/index.ts b/web/src/api/v1/index.ts index f6f2e42..9174ca9 100644 --- a/web/src/api/v1/index.ts +++ b/web/src/api/v1/index.ts @@ -1,3 +1,15 @@ +import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/query/react"; + +let backend_port = window.location.port; + +if (import.meta.env.DEV) { + backend_port = import.meta.env.BACKEND_PORT ?? 4000 + console.info(`Setting backend port to ${backend_port}`) +} + +export const BACKEND_HOST_PORT = `${window.location.hostname}:${backend_port}` +export const BACKEND = `${window.location.protocol}//${BACKEND_HOST_PORT}` + export type Response = { status: ( 'success' | @@ -9,4 +21,10 @@ export type Response = { 'error' ), data: T -} \ No newline at end of file +} + +export const api = createApi({ + tagTypes: ['error', 'api/config', 'api/state', 'api/toc'], + baseQuery: fetchBaseQuery({ baseUrl: `${BACKEND}/api/v1` }), + endpoints: () => ({}), +}) \ No newline at end of file diff --git a/web/src/api/v1/socket/api.ts b/web/src/api/v1/socket/api.ts index 9a3ea9f..7c2792b 100644 --- a/web/src/api/v1/socket/api.ts +++ b/web/src/api/v1/socket/api.ts @@ -1,7 +1,8 @@ import { store } from "@/api" -import { BACKEND_HOST_PORT, endpoints, type ApiModel } from "@/api/endpoints" +import { endpoints, type ApiModel } from "@/api/endpoints" import { type BaseMessageType, ClientPingMessage, type ServerToClientMessages } from "./model" import { socketActions } from "./store" +import { BACKEND_HOST_PORT } from ".." export const SOCKET_URI = `ws://${BACKEND_HOST_PORT}/api/v1/socket` diff --git a/web/src/api/v1/socket/middleware.ts b/web/src/api/v1/socket/middleware.ts index 58322d7..b3ad6cd 100644 --- a/web/src/api/v1/socket/middleware.ts +++ b/web/src/api/v1/socket/middleware.ts @@ -3,10 +3,9 @@ import type { UnknownAction } from "@reduxjs/toolkit"; import type { ThunkAction } from "redux-thunk"; import { socketActions, type SocketState } from "./store"; import { isRippingStatus, type ErrorMessage, type LogMessage, type MkvLogMessage, type ProgressMessage, type ProgressValueMessage, type RipStartStopMessage, type TocStatusMessage } from "@/api/v1/socket/model"; -import { endpoints, type ApiModel } from "@/api/endpoints"; -import { tocActions } from "../toc/store"; import { throttle } from "lodash"; import { errorApiUtil } from '@/api/v1/error/api' +import { api } from ".."; type SocketThunkAction = ThunkAction @@ -46,14 +45,7 @@ export const socketConnect = (): SocketThunkAction => ( socketConnection.on("TocStatusMessage", (value: TocStatusMessage) => { if (value.state === "complete") { - fetch(endpoints.state.get("toc"), { method: 'GET' }) - .then(response => response.json() as Promise) - .then(({ data }) => { - if (data.toc) { - dispatch(tocActions.setTocData(data.toc)) - dispatch(tocActions.setTocLoading(false)) - } - }) + dispatch(api.util.invalidateTags(['api/toc'])) } }) diff --git a/web/src/api/v1/state/api.ts b/web/src/api/v1/state/api.ts index 647dc77..cd5f930 100644 --- a/web/src/api/v1/state/api.ts +++ b/web/src/api/v1/state/api.ts @@ -1,18 +1,37 @@ import { type ApiModel, endpointsV1 } from "@/api/endpoints"; import type { State } from "./types"; -import { api } from "@/api"; +import { api } from ".."; // Define a service using a base URL and expected endpoints -const stateApi = api.injectEndpoints({ +export const stateApi = api.injectEndpoints({ endpoints: (build) => ({ getState: build.query({ - query: () => endpointsV1.state.get(), + providesTags: ['api/state'], + query: () => '/state', transformResponse: (response) => response.data }), getStateByPath: build.query({ - query: (path: string) => endpointsV1.state.get(path), + query: () => '/state', transformResponse: (response) => response.data }), + putState: build.mutation, ApiModel['v1']['state']>({ + query: (state) => ({ + url: '/state', + method: 'PUT', + body: state + }), + invalidatesTags: ['api/state'], + transformResponse: (response) => response.data + }), + updateState: build.mutation, State, ApiModel['v1']['state']>({ + query: (state) => ({ + url: '/state', + method: 'PUT', + body: state + }), + invalidatesTags: ['api/state'], + transformResponse: (response) => response.data + }) }), }) diff --git a/web/src/api/v1/state/types.ts b/web/src/api/v1/state/types.ts index 8f628aa..1434051 100644 --- a/web/src/api/v1/state/types.ts +++ b/web/src/api/v1/state/types.ts @@ -1,7 +1,12 @@ import type { RipState } from "@/api/v1/rip/store"; -import type { TocState } from "@/api/v1/toc/store"; import type { SocketState } from "@/api/v1/socket/store"; +import type { Toc } from "../toc/types"; +export interface TocState { + lines: Toc['lines']; + source?: Toc['source']; + loading?: Toc['loading'] +} export type CurrentStatus = "Scanning CD-ROM devices" | "Opening DVD disc" | "Processing title sets" diff --git a/web/src/api/v1/toc/api.ts b/web/src/api/v1/toc/api.ts index 794ae86..8769441 100644 --- a/web/src/api/v1/toc/api.ts +++ b/web/src/api/v1/toc/api.ts @@ -1,15 +1,25 @@ -import { type ApiModel, endpointsV1 } from "@/api/endpoints"; -import { api } from "@/api"; -import type { Toc } from "./types"; +import { type ApiModel } from "@/api/endpoints"; +import { api } from ".."; +import {type Response as ResponseV1 } from ".." // Define a service using a base URL and expected endpoints -const tocApi = api.injectEndpoints({ +export const tocApi = api.injectEndpoints({ endpoints: (build) => ({ - getToc: build.query({ - query: () => endpointsV1.state.get(), + getToc: build.query({ + providesTags: ['api/toc'], + query: () => '/toc', + }), + startTocLoad: build.mutation>({ + invalidatesTags: ['api/toc'], + query: () => '/toc.start', + transformResponse: (response) => response.data + }), + stopTocLoad: build.mutation>({ + invalidatesTags: ['api/toc'], + query: () => '/toc.stop', transformResponse: (response) => response.data }) }), }) -export const { useGetTocQuery } = tocApi \ No newline at end of file +export const { useGetTocQuery, useStartTocLoadMutation, useStopTocLoadMutation } = tocApi \ No newline at end of file diff --git a/web/src/api/v1/toc/store.tsx b/web/src/api/v1/toc/store.tsx deleted file mode 100644 index 9c4cbf3..0000000 --- a/web/src/api/v1/toc/store.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import { createSlice, type PayloadAction } from "@reduxjs/toolkit"; -import type { Toc } from "./types"; - -export interface TocState { - lines: Toc['lines']; - source?: Toc['source']; - loading?: Toc['loading'] -} - -const initialState: TocState = { - lines: [], - source: undefined, - loading: false -} - -const tocSlice = createSlice({ - name: "toc", - initialState, - reducers: { - setTocData: (state, action: PayloadAction) => { - if (action.payload) { - state.lines = action.payload.lines - state.source = action.payload.source - } else { - state.lines = [] - state.source = undefined - } - }, - setTocLoading: (state, action: PayloadAction) => { - state.loading = action.payload - } - } -}) - -export const tocActions = tocSlice.actions - -export default tocSlice.reducer \ No newline at end of file diff --git a/web/src/components/ButtonBar.tsx b/web/src/components/ButtonBar.tsx index 7b825dc..3fc4d8d 100644 --- a/web/src/components/ButtonBar.tsx +++ b/web/src/components/ButtonBar.tsx @@ -1,7 +1,6 @@ import { AppBar, Button, CircularProgress, IconButton, LinearProgress, Toolbar, Tooltip, Typography } from "@mui/material"; import { useAppDispatch, useAppSelector } from "@/api"; import { socketActions, type SocketProgress } from "@/api/v1/socket/store"; -import { tocActions } from "@/api/v1/toc/store"; import { ConfirmationDialog } from "./ConfirmationModal"; import { useState } from "react"; @@ -15,25 +14,33 @@ import { endpoints, type ApiModel } from "@/api/endpoints"; import { ripActions } from "@/api/v1/rip/store"; import { uniqueFilter } from "@/util/array"; import { ConfigDialog } from "./modals/ConfigDialog"; +import { useGetTocQuery, useStartTocLoadMutation, useStopTocLoadMutation } from "@/api/v1/toc/api"; type Props = {} export const ButtonBar = ({ }: Props) => { const dispatch = useAppDispatch() + const { data: tocData } = useGetTocQuery() + const sortInfo = useAppSelector((state) => state.rip.sort_info) const library = useAppSelector((state) => state.rip.destination?.library) const media = useAppSelector((state) => state.rip.destination?.media) const content = useAppSelector((state) => state.rip.destination?.content) const socketRipState = useAppSelector((state) => state.socket.rip) + + // This should be handled by the API const ripAll = useAppSelector((state) => - state.toc.source?.titles.length == [ + tocData && tocData.data && tocData.data.source.titles.length == [ ...state.rip.sort_info.extra_indexes, ...state.rip.sort_info.main_indexes ].filter(uniqueFilter).length ) - const tocLoading = useAppSelector((state) => state.toc.loading) + const [ startTocLoad, _startTocLoadResult ] = useStartTocLoadMutation() + const [ stopTocLoad, _stopTocLoadResult ] = useStopTocLoadMutation() + + const tocLoading = tocData?.status && tocData.status === 'in progress' const [cancelModalOpen, setCancelModalOpen] = useState(false) const [configDialogOpen, setConfigDialogOpen] = useState(false) @@ -51,18 +58,16 @@ export const ButtonBar = ({ }: Props) => { const handleLoadToc = () => { console.info('Fetching Toc') - dispatch(tocActions.setTocLoading(true)) - dispatch(tocActions.setTocData(undefined)) dispatch(socketActions.setSocketRipState()) dispatch(ripActions.setMainIndexes([])) dispatch(ripActions.setExtraIndexes([])) - fetch(endpoints.toc.get(), { method: 'GET' }) + // fetch(endpoints.toc.get(), { method: 'GET' }) + startTocLoad() } const handleCancelLoadToc = () => { - console.info('Cancelling laod TOC') - fetch(endpoints.toc.stop(), { method: 'GET' }) - dispatch(tocActions.setTocLoading(false)) + console.info('Cancelling load TOC') + stopTocLoad() } const handleCancelRip = () => { diff --git a/web/src/components/forms/ContentForm/index.tsx b/web/src/components/forms/ContentForm/index.tsx index 5105944..f0cca79 100644 --- a/web/src/components/forms/ContentForm/index.tsx +++ b/web/src/components/forms/ContentForm/index.tsx @@ -1,28 +1,33 @@ -import { useAppSelector } from "@/api" +import { useAppDispatch, useAppSelector } from "@/api" import { ripActions } from "@/api/v1/rip/store" import { Autocomplete, Card, InputLabel, Link, MenuItem, Select, TextField } from "@mui/material" -import { useDispatch } from "react-redux" import { ContentFormControl, FirstEpisodeFormControl, LibraryFormControl, MediaFormControl, NameIdFormControl, NameOptionWrapper, SeasonFormControl, SplitSegmentsFormControl, StyledFormGroup } from "./index.styles" import React, { useCallback, useState } from "react" import { throttle } from "lodash" import type { TmdbSearchResult } from "@/api/v1/tmdb/types" import { AutocompleteWrapper } from "@/theme" import { endpoints, type ApiModel } from "@/api/endpoints" +import { useGetStateQuery } from "@/api/v1/state/api" export const CombinedShowMovieForm = ({ onError, onClearError }: BaseProps) => { - const dispatch = useDispatch(); + const { error: stateError, isLoading, isSuccess } = useGetStateQuery() - const name = useAppSelector((state) => state.rip.sort_info?.name) - const id = useAppSelector((state) => state.rip.sort_info?.id) - const library = useAppSelector((state) => state.rip.destination?.library) - const media = useAppSelector((state) => state.rip.destination?.media) - const content = useAppSelector((state) => state.rip.destination?.content) - const seasonNumber = useAppSelector((state) => state.rip.sort_info?.season_number) - const firstEpisode = useAppSelector((state) => state.rip.sort_info?.first_episode) + const rip = useAppSelector((state) => state.rip) + + const dispatch = useAppDispatch(); + + // const name = useAppSelector((state) => state.rip.sort_info?.name) + const name = rip?.sort_info?.name ?? '' + const id = rip?.sort_info?.id ?? '' + const library = rip?.destination?.library ?? '' + const media = rip?.destination?.media ?? '' + const content = rip?.destination?.content ?? '' + const seasonNumber = rip?.sort_info?.season_number ?? '' + const firstEpisode = rip?.sort_info?.first_episode ?? '' // const splitSegments = useAppSelector((state) => state.rip.sort_info?.split_segments) - const tmdbConfiguration = useAppSelector((state) => state.tmdb.configuration) - const tmdbSelection = useAppSelector((state) => state.rip.tmdb_selection) + const tmdbConfiguration = useAppSelector((store) => store.tmdb.configuration) + const tmdbSelection = rip?.tmdb_selection ?? '' const [ nameValue, setNameValue ] = useState(null) const [ nameOptions, setNameOptions ] = useState<(TmdbSearchResult)[]>([]) @@ -45,12 +50,6 @@ export const CombinedShowMovieForm = ({ onError, onClearError }: BaseProps) => { )) if (!foundOption && searchText !== '') { if (content?.toLowerCase() === 'show') { - fetch(endpoints.tmdb.show(searchText), { method: 'GET' }) - .then(response => response.json() as Promise) - .then(({ data }) => { - setNameOptions(data) - }) - } else if (content?.toLowerCase() === 'movie') { fetch(endpoints.tmdb.movie(searchText), { method: 'GET' }) .then(response => response.json() as Promise) .then(({ data }) => { @@ -89,6 +88,9 @@ export const CombinedShowMovieForm = ({ onError, onClearError }: BaseProps) => { if (isValid) onClearError && onClearError(); else onError && onError(); + if (isLoading) return <>; + if (stateError) return <>Error + return <> diff --git a/web/src/components/modals/ConfigDialog/LegacyTab.tsx b/web/src/components/modals/ConfigDialog/LegacyTab.tsx new file mode 100644 index 0000000..7816011 --- /dev/null +++ b/web/src/components/modals/ConfigDialog/LegacyTab.tsx @@ -0,0 +1,62 @@ +import { useGetConfigQuery } from "@/api/v1/config/api" +import type { Config } from "@/api/v1/config/types" +import { TextField } from "@mui/material" +import { useState } from "react" + +interface Props { + formData: Partial + setFormData: React.Dispatch>> +} + +export const AllConfigValuesTab = (props: Props) => { + const { formData, setFormData } = props + + + + return <> + setFormData((prev) => ({...prev, config_file: event.target.value})) } + /> + setFormData((prev) => ({...prev, source: event.target.value})) } + /> + setFormData((prev) => ({...prev, destination: event.target.value})) } + /> + setFormData((prev) => ({...prev, tmdb_token: event.target.value})) } + /> + setFormData((prev) => ({...prev, makemkvcon_path: event.target.value})) } + /> + {/* + TODO: Make this one a select with log levels + setFormData((prev) => ({...prev, log_level: event.target.value})) } + /> + */} + setFormData((prev) => ({...prev, log_file: event.target.value})) } + /> + setFormData((prev) => ({...prev, temp_prefix: event.target.value})) } + /> + setFormData((prev) => ({...prev, frontend: event.target.value})) } + /> + setFormData((prev) => ({...prev, listen_port: event.target.value})) } + /> + {/* */} + +} \ No newline at end of file diff --git a/web/src/components/modals/ConfigDialog/index.styles.ts b/web/src/components/modals/ConfigDialog/index.styles.ts index a1d1406..96b57b5 100644 --- a/web/src/components/modals/ConfigDialog/index.styles.ts +++ b/web/src/components/modals/ConfigDialog/index.styles.ts @@ -1,13 +1,26 @@ -import { DialogActions, DialogContent, DialogTitle, styled } from "@mui/material" +import { Box, DialogActions, DialogContent, DialogTitle, styled, Tabs } from "@mui/material" + +export const StyledTabContent = styled(Box)(({}) => ({ + marginLeft: '1em' +})) export const ConfigDialogTitle = styled(DialogTitle)(({ }) => ({ display: "flex", justifyContent: "space-between" })); + export const ConfigDialogContent = styled(DialogContent)(({ }) => ({ - paddingTop: 0, - paddingBottom: 0 + display: "flex", + flexDirection: "row", + [".MuiTabs-root"]: { + display: 'flex', + flexBasis: '10em', + flexGrow: 0, + flexShrink: 0, + + }, })); + export const ConfigDialogActions = styled(DialogActions)(({ }) => ({ padding: "20px 24px" })); \ No newline at end of file diff --git a/web/src/components/modals/ConfigDialog/index.tsx b/web/src/components/modals/ConfigDialog/index.tsx index 02ac01c..c863710 100644 --- a/web/src/components/modals/ConfigDialog/index.tsx +++ b/web/src/components/modals/ConfigDialog/index.tsx @@ -1,34 +1,70 @@ -import { Button, Dialog, IconButton, TextField, Typography } from "@mui/material" +import { Box, Button, Dialog, IconButton, Tab, Tabs, TextField, Typography } from "@mui/material" import RefreshIcon from '@mui/icons-material/Refresh'; import CloseIcon from '@mui/icons-material/Close'; import { useGetConfigQuery, usePutConfigMutation, useReloadConfigMutation } from "@/api/v1/config/api"; import { useState } from "react"; import type { Config } from "@/api/v1/config/types"; -import { ConfigDialogActions, ConfigDialogContent, ConfigDialogTitle } from "./index.styles"; +import { ConfigDialogActions, ConfigDialogContent, ConfigDialogTitle, StyledTabContent } from "./index.styles"; +import { AllConfigValuesTab } from "./LegacyTab"; type Props = { open: boolean onClose?: () => void } +const a11yProps = (index: number) => { + return { + id: `vertical-tab-${index}`, + 'aria-controls': `vertical-tabpanel-${index}`, + }; +} + +interface TabContentProps { + children?: React.ReactNode; + index: number; + value: number; +} + +const TabContent = (props: TabContentProps) => { + const { children, value, index, ...other } = props + + return ( + + ) +} + export const ConfigDialog = ({ open, onClose = () => {} }: Props) => { - const { data, isLoading, isSuccess } = useGetConfigQuery() const [putConfig, _putConfigResult] = usePutConfigMutation() const [reloadConfig, _reloadConfigResult] = useReloadConfigMutation() - const [formData, setFormData] = useState>(!isLoading && isSuccess ? data : {}) + const [currentTab, setCurrentTab] = useState(0) + const [formData, setFormData] = useState>({}) + + const { data, isLoading, isSuccess } = useGetConfigQuery() - const config_file = formData?.config_file ?? data?.config_file ?? ''; - const source = formData?.source ?? data?.source ?? ''; - const destination = formData?.destination ?? data?.destination ?? ''; - const tmdb_token = formData?.tmdb_token ?? data?.tmdb_token ?? ''; - const makemkvcon_path = formData?.makemkvcon_path ?? data?.makemkvcon_path ?? ''; + formData.config_file = formData?.config_file ?? data?.config_file ?? ''; + formData.source = formData?.source ?? data?.source ?? ''; + formData.destination = formData?.destination ?? data?.destination ?? ''; + formData.tmdb_token = formData?.tmdb_token ?? data?.tmdb_token ?? ''; + formData.makemkvcon_path = formData?.makemkvcon_path ?? data?.makemkvcon_path ?? ''; // const log_level = formData?.log_level ?? data?.log_level ?? ''; - const log_file = formData?.log_file ?? data?.log_file ?? ''; - const temp_prefix = formData?.temp_prefix ?? data?.temp_prefix ?? ''; - const frontend = formData?.frontend ?? data?.frontend ?? ''; - const listen_port = formData?.listen_port ?? data?.listen_port ?? ''; + formData.log_file = formData?.log_file ?? data?.log_file ?? ''; + formData.temp_prefix = formData?.temp_prefix ?? data?.temp_prefix ?? ''; + formData.frontend = formData?.frontend ?? data?.frontend ?? ''; + formData.listen_port = formData?.listen_port ?? data?.listen_port ?? ''; // useEffect(() => { // if (!isLoading && isSuccess) { @@ -48,58 +84,36 @@ export const ConfigDialog = ({ open, onClose = () => {} }: Props) => { setFormData({}) } - return + const handleChangeTab = (_event: React.SyntheticEvent, newValue: number) => { + setCurrentTab(newValue) + } + + return Settings - - setFormData((prev) => ({...prev, config_file: event.target.value})) } - /> - setFormData((prev) => ({...prev, source: event.target.value})) } - /> - setFormData((prev) => ({...prev, destination: event.target.value})) } - /> - setFormData((prev) => ({...prev, tmdb_token: event.target.value})) } - /> - setFormData((prev) => ({...prev, makemkvcon_path: event.target.value})) } - /> - {/* - TODO: Make this one a select with log levels - setFormData((prev) => ({...prev, log_level: event.target.value})) } - /> - */} - setFormData((prev) => ({...prev, log_file: event.target.value})) } - /> - setFormData((prev) => ({...prev, temp_prefix: event.target.value})) } - /> - setFormData((prev) => ({...prev, frontend: event.target.value})) } - /> - setFormData((prev) => ({...prev, listen_port: event.target.value})) } - /> - {/* */} + + + + + + + + + Tab Two! + + + + diff --git a/web/src/components/toc/TocGrid.tsx b/web/src/components/toc/TocGrid.tsx index 0d7f800..2e54c77 100644 --- a/web/src/components/toc/TocGrid.tsx +++ b/web/src/components/toc/TocGrid.tsx @@ -13,6 +13,7 @@ import AddCircleOutlineOutlinedIcon from '@mui/icons-material/AddCircleOutlineOu import RemoveCircleOutlineOutlinedIcon from '@mui/icons-material/RemoveCircleOutlineOutlined'; import { isRippingStatus } from "@/api/v1/socket/model" import type { TitleInfo, Toc } from "@/api/v1/toc/types" +import { useGetTocQuery } from "@/api/v1/toc/api" type Props = { @@ -36,7 +37,10 @@ export const TocGrid = ({ }: Props) => { const dispatch = useAppDispatch() - const data = useAppSelector((state) => state.toc) + const { data } = useGetTocQuery() + + const tocData = data?.data + const mainIndexes = useAppSelector((state) => state.rip.sort_info?.main_indexes) const extraIndexes = useAppSelector((state) => state.rip.sort_info?.extra_indexes) const content = useAppSelector((state) => state.rip.destination?.content) @@ -52,10 +56,10 @@ export const TocGrid = ({ }: Props) => { titleGroup.matches )).flat()) - data?.source?.titles.forEach((outerTitle, outerIndex) => { + tocData?.source?.titles.forEach((outerTitle, outerIndex) => { const newTitleGroup: TitleGroup = { title: outerTitle, index: outerIndex, matches: [outerIndex] } const outerTitleLength = hmsToSeconds(outerTitle.runtime) - data?.source?.titles.forEach((innerTitle, innerIndex) => { + tocData?.source?.titles.forEach((innerTitle, innerIndex) => { if ( matchedIndexes().indexOf(innerIndex) === -1 // No match already on this index && hmsToSeconds(innerTitle.runtime) > outerTitleLength - EPISODE_LENGTH_TOLERANCE_SECONDS @@ -126,7 +130,7 @@ export const TocGrid = ({ }: Props) => { console.info("TitleGroups", titleGroups) console.info("Biggest (show)", biggestTitleGroup) console.info("Longest (movie)", longestTitleGroup) - for (let i = 0; i < (data?.source?.titles.length ?? 0); i++) { + for (let i = 0; i < (tocData?.source?.titles.length ?? 0); i++) { if (activeTitleGroup.matches.indexOf(i) > -1) { mainIndexes.push(i) } else { @@ -146,19 +150,19 @@ export const TocGrid = ({ }: Props) => { useEffect(() => { const makeItAsync = async () => { - if (data && !socketRipState?.started) { - dispatch(ripActions.setTocLength(data?.source?.titles.length)); + if (tocData && !socketRipState?.started) { + dispatch(ripActions.setTocLength(tocData?.source?.titles.length)); setIndexes(); } } makeItAsync() - }, [data, content]) + }, [tocData, content]) const handleSelectAllOnClick = (_event: React.ChangeEvent, checked: boolean) => { if (checked) { const { mainIndexes: newMainIndexes, extraIndexes: newExtraIndexes } = getIndexes() - data?.source?.titles.forEach((_value, index) => { + tocData?.source?.titles.forEach((_value, index) => { const isInMainIndexes = (mainIndexes.indexOf(index) > -1) const isInExtraIndexes = (extraIndexes.indexOf(index) > -1) const wasInMainIndexes = (oldMainIndexes.indexOf(index) > -1) @@ -230,9 +234,9 @@ export const TocGrid = ({ }: Props) => { - {data + {tocData ? ( - data?.source?.titles.map((title, index) => { + tocData?.source?.titles.map((title, index) => { return ( diff --git a/web/src/pages/App.tsx b/web/src/pages/App.tsx index a347213..cdcf945 100644 --- a/web/src/pages/App.tsx +++ b/web/src/pages/App.tsx @@ -1,14 +1,10 @@ import { AppContainer } from './App.styles' import { ButtonBar } from '@/components/ButtonBar' -import { useEffect } from 'react' import { endpoints, type ApiModel } from '@/api/endpoints' import { useAppDispatch, useAppSelector } from '@/api' -import { tocActions } from '@/api/v1/toc/store' import { tmdbActions } from '@/api/v1/tmdb/store' -import { ripActions } from '@/api/v1/rip/store' import { StatusScroller } from '@/components/status/StatusScroller' import { TocGrid } from '@/components/toc/TocGrid' -import { useGetStateByPathQuery } from '@/api/v1/state/api' import { useGetErrorQuery } from '@/api/v1/error/api' import { ErrorDialog } from '@/components/modals/ErrorDialog' import { socketConnect } from '@/api/v1/socket/middleware' @@ -18,10 +14,8 @@ function App() { const dispatch = useAppDispatch() const tmdbConfiguration = useAppSelector((state) => state.tmdb.configuration) - const ripStateData = useGetStateByPathQuery('rip').data?.rip const errorData = useGetErrorQuery() - dispatch(ripActions.setRipData(ripStateData)) dispatch(socketConnect()) if (!tmdbConfiguration) { @@ -32,16 +26,6 @@ function App() { }) } - useEffect(() => { - fetch(endpoints.state.get("toc"), { method: 'GET' }) - .then(response => response.json() as Promise) - .then(({ data }) => { - if (data.toc) { - dispatch(tocActions.setTocData(data.toc)) - } - }) - }, []); - return ( From 0123fd068f4d28d9c0c50f94fe299e74113b7499 Mon Sep 17 00:00:00 2001 From: enigma0Z <6117628+enigma0Z@users.noreply.github.com> Date: Wed, 10 Jun 2026 01:25:42 -0400 Subject: [PATCH 03/10] Add compound launch --- .vscode/launch.json | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index d8a60b4..2d88c2f 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -2,14 +2,7 @@ "version": "0.2.0", "configurations": [ { - "name": "Python: Current File", - "type": "debugpy", - "request": "launch", - "program": "${file}", - "console": "integratedTerminal" - }, - { - "name": "Debug API", + "name": "API", "type": "debugpy", "request": "launch", "module": "makemkv_headless.__main__", @@ -19,10 +12,17 @@ }, { "type": "node-terminal", - "name": "Launch Web", + "name": "Web", "request": "launch", "command": "npm run dev", "cwd": "${workspaceFolder}/web" } + ], + "compounds": [ + { + "name": "API + Web", + "configurations": ["API", "Web"], + "stopAll": true + } ] } \ No newline at end of file From d0a3dc87da9e0a663e5eece0ed853793785cbd37 Mon Sep 17 00:00:00 2001 From: enigma0Z <6117628+enigma0Z@users.noreply.github.com> Date: Wed, 10 Jun 2026 02:22:22 -0400 Subject: [PATCH 04/10] Factor out tmdb store --- web/src/api/index.ts | 4 +- web/src/api/v1/index.ts | 2 +- web/src/api/v1/tmdb/api.ts | 19 +++++++ web/src/api/v1/tmdb/store.tsx | 40 --------------- web/src/api/v1/tmdb/types.ts | 17 +++++++ .../components/forms/ContentForm/index.tsx | 51 +++++++++++-------- web/src/pages/App.tsx | 13 +---- 7 files changed, 69 insertions(+), 77 deletions(-) create mode 100644 web/src/api/v1/tmdb/api.ts delete mode 100644 web/src/api/v1/tmdb/store.tsx diff --git a/web/src/api/index.ts b/web/src/api/index.ts index 0011a94..a9c0241 100644 --- a/web/src/api/index.ts +++ b/web/src/api/index.ts @@ -2,7 +2,6 @@ import { configureStore, type Middleware, type UnknownAction } from "@reduxjs/to import { useDispatch, useSelector } from "react-redux"; import rip, { ripActions, type RipState } from './v1/rip/store' -import tmdb, { type TmdbState } from "./v1/tmdb/store"; import socket, { type SocketState } from "./v1/socket/store"; import { SOCKET_URI, SocketConnection } from "./v1/socket/api"; import { setupListeners } from "@reduxjs/toolkit/query"; @@ -11,7 +10,6 @@ import { api as apiV1 } from "./v1"; export type RootState = { rip: RipState, - tmdb: TmdbState, socket: SocketState } @@ -56,7 +54,7 @@ export const store = configureStore({ apiV1.middleware ), reducer: { - rip: rip.reducer, tmdb, socket, api: apiV1.reducer + rip: rip.reducer, socket, api: apiV1.reducer }, }) diff --git a/web/src/api/v1/index.ts b/web/src/api/v1/index.ts index 9174ca9..3ac0665 100644 --- a/web/src/api/v1/index.ts +++ b/web/src/api/v1/index.ts @@ -24,7 +24,7 @@ export type Response = { } export const api = createApi({ - tagTypes: ['error', 'api/config', 'api/state', 'api/toc'], + tagTypes: ['error', 'api/config', 'api/state', 'api/toc', 'api/tmdb', 'api/tmdb/configuration'], baseQuery: fetchBaseQuery({ baseUrl: `${BACKEND}/api/v1` }), endpoints: () => ({}), }) \ No newline at end of file diff --git a/web/src/api/v1/tmdb/api.ts b/web/src/api/v1/tmdb/api.ts new file mode 100644 index 0000000..8ad0f88 --- /dev/null +++ b/web/src/api/v1/tmdb/api.ts @@ -0,0 +1,19 @@ +import { type ApiModel } from "@/api/endpoints" +import { api } from ".." +import type { TmdbConfiguration, TmdbSearchResultMovie, TmdbSearchResultShow } from "./types" + +const tmdbApi = api.injectEndpoints({ + endpoints: (build) => ({ + getTmdbConfiguration: build.query({ + providesTags: [{ type: 'api/tmdb/configuration' }], + query: () => '/tmdb/configuration', + transformResponse: (response) => response.data + }), + getTmdbSearch: build.query({ + query: ({content, query}) => `/tmdb/${content.toLowerCase()}?${new URLSearchParams({q: query})}`, + transformResponse: (response) => response.data ?? [] + }), + }), +}) + +export const { useGetTmdbConfigurationQuery, useGetTmdbSearchQuery } = tmdbApi \ No newline at end of file diff --git a/web/src/api/v1/tmdb/store.tsx b/web/src/api/v1/tmdb/store.tsx deleted file mode 100644 index 65d3028..0000000 --- a/web/src/api/v1/tmdb/store.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import { createSlice, type PayloadAction } from "@reduxjs/toolkit"; - -export type TmdbConfiguration = { - change_keys?: string[]; - images?: { - base_url: string; - secure_base_url: string; - backdrop_sizes: string[]; - logo_sizes: string[]; - poster_sizes: string[]; - profile_sizes: string[]; - still_sizes: string[]; - } -} - -export type TmdbState = { - configuration?: TmdbConfiguration -} - -const initialState: TmdbState = { - configuration: undefined, -} - -const tmdbSlice = createSlice({ - name: "tmdb", - initialState, - reducers: { - setTmdbConfiguration: (state, action: PayloadAction) => { - if (action.payload) { - state.configuration = action.payload - } else { - state.configuration = undefined - } - } - } -}) - -export const tmdbActions = tmdbSlice.actions - -export default tmdbSlice.reducer \ No newline at end of file diff --git a/web/src/api/v1/tmdb/types.ts b/web/src/api/v1/tmdb/types.ts index 2e4ca1a..1ca94e0 100644 --- a/web/src/api/v1/tmdb/types.ts +++ b/web/src/api/v1/tmdb/types.ts @@ -1,3 +1,20 @@ +export type TmdbConfiguration = { + change_keys?: string[]; + images?: { + base_url: string; + secure_base_url: string; + backdrop_sizes: string[]; + logo_sizes: string[]; + poster_sizes: string[]; + profile_sizes: string[]; + still_sizes: string[]; + } +} + +export type TmdbState = { + configuration?: TmdbConfiguration +} + export type TmdbSearchResultCommon = { adult: boolean; backdrop_path: string; diff --git a/web/src/components/forms/ContentForm/index.tsx b/web/src/components/forms/ContentForm/index.tsx index f0cca79..dbbe720 100644 --- a/web/src/components/forms/ContentForm/index.tsx +++ b/web/src/components/forms/ContentForm/index.tsx @@ -2,12 +2,11 @@ import { useAppDispatch, useAppSelector } from "@/api" import { ripActions } from "@/api/v1/rip/store" import { Autocomplete, Card, InputLabel, Link, MenuItem, Select, TextField } from "@mui/material" import { ContentFormControl, FirstEpisodeFormControl, LibraryFormControl, MediaFormControl, NameIdFormControl, NameOptionWrapper, SeasonFormControl, SplitSegmentsFormControl, StyledFormGroup } from "./index.styles" -import React, { useCallback, useState } from "react" -import { throttle } from "lodash" +import React, { useState } from "react" import type { TmdbSearchResult } from "@/api/v1/tmdb/types" import { AutocompleteWrapper } from "@/theme" -import { endpoints, type ApiModel } from "@/api/endpoints" import { useGetStateQuery } from "@/api/v1/state/api" +import { useGetTmdbConfigurationQuery, useGetTmdbSearchQuery } from "@/api/v1/tmdb/api" export const CombinedShowMovieForm = ({ onError, onClearError }: BaseProps) => { @@ -25,12 +24,15 @@ export const CombinedShowMovieForm = ({ onError, onClearError }: BaseProps) => { const content = rip?.destination?.content ?? '' const seasonNumber = rip?.sort_info?.season_number ?? '' const firstEpisode = rip?.sort_info?.first_episode ?? '' + // const splitSegments = useAppSelector((state) => state.rip.sort_info?.split_segments) - const tmdbConfiguration = useAppSelector((store) => store.tmdb.configuration) + + const { data: tmdbConfiguration } = useGetTmdbConfigurationQuery() + const tmdbSelection = rip?.tmdb_selection ?? '' const [ nameValue, setNameValue ] = useState(null) - const [ nameOptions, setNameOptions ] = useState<(TmdbSearchResult)[]>([]) + // const [ nameOptions, setNameOptions ] = useState<(TmdbSearchResult)[]>([]) const [ splitSegmentsValue, setSplitSegmentsValue ] = useState() @@ -44,24 +46,31 @@ export const CombinedShowMovieForm = ({ onError, onClearError }: BaseProps) => { } } - const updateOptions = useCallback(throttle((searchText: string) => { - const foundOption = nameOptions?.find((option) => ( - getOptionLabel(option) === searchText - )) - if (!foundOption && searchText !== '') { - if (content?.toLowerCase() === 'show') { - fetch(endpoints.tmdb.movie(searchText), { method: 'GET' }) - .then(response => response.json() as Promise) - .then(({ data }) => { - setNameOptions(data) - }) - } - } - }, 2000, {trailing: true, leading: false}), [content, nameOptions]); + const { data: nameOptions } = useGetTmdbSearchQuery({content, query: nameValue ?? ''}) + + // const updateOptions = useCallback(throttle((searchText: string) => { + // const foundOption = nameOptions?.find((option) => ( + // getOptionLabel(option) === searchText + // )) + // if (!foundOption && searchText !== '') { + // if (content?.toLowerCase() === 'show') { + // fetch(endpoints.tmdb.show(searchText), { method: 'GET' }) + // .then(response => response.json() as Promise) + // .then(({ data }) => { + // setNameOptions(data) + // }) + // } else if (content?.toLowerCase() === 'movie') { + // fetch(endpoints.tmdb.movie(searchText), { method: 'GET' }) + // .then(response => response.json() as Promise) + // .then(({ data }) => { + // setNameOptions(data) + // }) + // } + // } + // }, 2000, {trailing: true, leading: false}), [content, nameOptions]); const handleNameOnInputChange = (_event: React.SyntheticEvent, value: string) => { setNameValue(value) - updateOptions(value) } const handleNameOnChange = (_event: React.SyntheticEvent, value: TmdbSearchResult | string | undefined | null) => { @@ -158,7 +167,7 @@ export const CombinedShowMovieForm = ({ onError, onClearError }: BaseProps) => { renderInput={(params) => ( )} onInputChange={handleNameOnInputChange} onChange={handleNameOnChange} - options={[...nameOptions, tmdbSelection]} + options={[...(nameOptions ?? []), tmdbSelection]} getOptionLabel={(option) => getOptionLabel(option)} filterOptions={(options, state) => { const filteredOptions = options.filter((option) => { diff --git a/web/src/pages/App.tsx b/web/src/pages/App.tsx index cdcf945..b02becf 100644 --- a/web/src/pages/App.tsx +++ b/web/src/pages/App.tsx @@ -1,8 +1,6 @@ import { AppContainer } from './App.styles' import { ButtonBar } from '@/components/ButtonBar' -import { endpoints, type ApiModel } from '@/api/endpoints' -import { useAppDispatch, useAppSelector } from '@/api' -import { tmdbActions } from '@/api/v1/tmdb/store' +import { useAppDispatch } from '@/api' import { StatusScroller } from '@/components/status/StatusScroller' import { TocGrid } from '@/components/toc/TocGrid' import { useGetErrorQuery } from '@/api/v1/error/api' @@ -13,19 +11,10 @@ import { CombinedShowMovieForm } from '@/components/forms/ContentForm' function App() { const dispatch = useAppDispatch() - const tmdbConfiguration = useAppSelector((state) => state.tmdb.configuration) const errorData = useGetErrorQuery() dispatch(socketConnect()) - if (!tmdbConfiguration) { - fetch(endpoints.tmdb.configuration()) - .then(response => response.json() as Promise) - .then(({ data }) => { - dispatch(tmdbActions.setTmdbConfiguration(data)) - }) - } - return ( From 87c8cf636b8f123b2d9c1150c3bb08172cef5880 Mon Sep 17 00:00:00 2001 From: enigma0Z <6117628+enigma0Z@users.noreply.github.com> Date: Wed, 10 Jun 2026 02:25:39 -0400 Subject: [PATCH 05/10] Clean up view with no data --- web/src/components/toc/TocGrid.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/web/src/components/toc/TocGrid.tsx b/web/src/components/toc/TocGrid.tsx index 2e54c77..c32ec19 100644 --- a/web/src/components/toc/TocGrid.tsx +++ b/web/src/components/toc/TocGrid.tsx @@ -262,7 +262,6 @@ export const TocGrid = ({ }: Props) => { }) ) : ( - No data ) } From cfd41da4842607aafea28fd8a95c6706c41ee64d Mon Sep 17 00:00:00 2001 From: enigma0Z <6117628+enigma0Z@users.noreply.github.com> Date: Thu, 11 Jun 2026 11:25:00 -0400 Subject: [PATCH 06/10] Config API / Store refactor --- web/src/api/v1/config/api.ts | 14 +++++++++----- web/src/api/v1/config/store.ts | 31 +++++++++++++++++++++++++++++++ web/src/api/v1/config/types.ts | 2 +- 3 files changed, 41 insertions(+), 6 deletions(-) create mode 100644 web/src/api/v1/config/store.ts diff --git a/web/src/api/v1/config/api.ts b/web/src/api/v1/config/api.ts index 110815a..ab06d85 100644 --- a/web/src/api/v1/config/api.ts +++ b/web/src/api/v1/config/api.ts @@ -1,22 +1,26 @@ import { endpointsV1, type ApiModel } from "@/api/endpoints" -import type { Config } from "./types" import { api } from ".." +import type { Config } from "./types" -const configApi = api.injectEndpoints({ +export const configApi = api.injectEndpoints({ endpoints: (build) => ({ getConfig: build.query({ providesTags: ['api/config'], - query: () => endpointsV1.config.get(), + query: () => '/config', transformResponse: (response) => response.data }), reloadConfig: build.mutation({ invalidatesTags: ['api/config'], - query: endpointsV1.config.reload(), + query: () => '/config.reload', transformResponse: (response) => response.data, }), putConfig: build.mutation, ApiModel['v1']['config']>({ invalidatesTags: ['api/config'], - query: endpointsV1.config.put(), + query: (body: Partial) => ({ + url: '/config', + method: 'PUT', + body + }), transformResponse: (response) => response.data, }) }), diff --git a/web/src/api/v1/config/store.ts b/web/src/api/v1/config/store.ts new file mode 100644 index 0000000..e8be921 --- /dev/null +++ b/web/src/api/v1/config/store.ts @@ -0,0 +1,31 @@ +import { createSlice, type PayloadAction } from "@reduxjs/toolkit" +import type { Config } from "./types" + +const initialState: Config = { + config_file: "", + destination: "", + source: "", + tmdb_token: "", + makemkvcon_path: "", + log_level: "ERROR", + log_file: "", + temp_prefix: "", + frontend: "", + listen_port: "", + cors_origins: [], + ui_path: "" +} + +const configSlice = createSlice({ + name: "config", + initialState, + reducers: { + updateConfig: (state, action: PayloadAction>) => ({ + ...state, ...action.payload + }), + } +}) + +export const configActions = configSlice.actions + +export default configSlice \ No newline at end of file diff --git a/web/src/api/v1/config/types.ts b/web/src/api/v1/config/types.ts index 008ced1..2a2af8c 100644 --- a/web/src/api/v1/config/types.ts +++ b/web/src/api/v1/config/types.ts @@ -1,6 +1,6 @@ export type LogLevel = "ERROR" | "WARNING" | "INFO" | "DEBUG" -export type Config = { +export interface Config { config_file: string; destination: string; source: string; From def7dc9be725614ee6ac01356ae28d89ed52fb79 Mon Sep 17 00:00:00 2001 From: enigma0Z <6117628+enigma0Z@users.noreply.github.com> Date: Thu, 11 Jun 2026 11:25:12 -0400 Subject: [PATCH 07/10] Remove unneeded endpoints --- web/src/api/endpoints.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/web/src/api/endpoints.ts b/web/src/api/endpoints.ts index 26ff925..e76c045 100644 --- a/web/src/api/endpoints.ts +++ b/web/src/api/endpoints.ts @@ -49,11 +49,6 @@ export const endpointsV1 = { }, config: { get: () => `/config`, - put: () => (body: Partial) => ({ - url: '/api/v1/config', - method: 'PUT', - body - }), reload: () => () => `/config.reload` }, error: { From 34148170f5d9beb6bcbea3231e5bbabb5ef7ee6b Mon Sep 17 00:00:00 2001 From: enigma0Z <6117628+enigma0Z@users.noreply.github.com> Date: Thu, 11 Jun 2026 11:25:34 -0400 Subject: [PATCH 08/10] Add auto api write for config and throttling --- web/src/api/index.ts | 61 ++++++++++++++++++++++++++++++++++---------- 1 file changed, 48 insertions(+), 13 deletions(-) diff --git a/web/src/api/index.ts b/web/src/api/index.ts index a9c0241..ae650c6 100644 --- a/web/src/api/index.ts +++ b/web/src/api/index.ts @@ -7,29 +7,68 @@ import { SOCKET_URI, SocketConnection } from "./v1/socket/api"; import { setupListeners } from "@reduxjs/toolkit/query"; import { stateApi } from "./v1/state/api"; import { api as apiV1 } from "./v1"; +import type { Config } from "./v1/config/types"; +import config, { configActions } from './v1/config/store' +import { configApi } from "./v1/config/api"; +import { throttle } from "lodash"; -export type RootState = { +export interface RootState { + config: Config, rip: RipState, socket: SocketState } +// Exploded currying syntax, for reference +// const updateApiMiddleware: Middleware<{}, RootState> = (store) => { +// return function wrapDispatch(next) { // next +// return function updateApi(action) { // action +// // Do things with store, next, action +// } +// } +// } + +const API_THROTTLE_RATE = 1000 + +const throttledApiUpdate = { + config: throttle((callback: () => void) => callback(), API_THROTTLE_RATE), + rip: throttle((callback: () => void) => callback(), API_THROTTLE_RATE) +} + const updateApiMiddleware: Middleware<{}, RootState> = store => next => _action => { - const action = (_action as any) + const action = (_action as any) // Make TS happy + if (action.type === 'api/executeQuery/fulfilled') { if (action.meta?.arg?.endpointName === 'getState') { - console.log('Update rip state from API', action.payload.rip) + console.log('Update rip store from API', action.payload.rip) store.dispatch(ripActions.setRipData(action.payload.rip)) + } else if (action.meta?.arg?.endpointName === 'getConfig') { + console.log('Update config store from API', action.payload) + store.dispatch(configActions.updateConfig(action.payload)) } } - - if ((action as UnknownAction).type.startsWith('rip/')) { + + if (action.type.startsWith('rip/')) { const currentRipState = store.getState().rip const result = next(action) const nextRipState = store.getState().rip if (JSON.stringify(currentRipState) !== JSON.stringify(nextRipState)) { - console.log('Put state into API', currentRipState, nextRipState) - const initiate: UnknownAction = (stateApi.endpoints.putState.initiate({ rip: nextRipState }) as any) - store.dispatch(initiate) + throttledApiUpdate.rip(() => { + console.log('Put rip state API', currentRipState, nextRipState) + const initiate: UnknownAction = (stateApi.endpoints.putState.initiate({ rip: nextRipState }) as any) + store.dispatch(initiate) + }) + } + return result + } else if (action.type == 'config/updateConfig') { + const currentConfigState = store.getState().config + const result = next(action) + const nextConfigState = store.getState().config + if (JSON.stringify(currentConfigState) !== JSON.stringify(nextConfigState)) { + throttledApiUpdate.config(() => { + console.log('Put config into API', currentConfigState, nextConfigState) + const initiate: UnknownAction = (configApi.endpoints.putConfig.initiate(nextConfigState) as any) + store.dispatch(initiate) + }) } return result } @@ -37,10 +76,6 @@ const updateApiMiddleware: Middleware<{}, RootState> = store => next => _action return next(action) } -export type ThunkExtraArgument = { - socketConnection: SocketConnection -} - export const store = configureStore({ middleware: (getDefaultMiddleware) => getDefaultMiddleware({ thunk: { @@ -54,7 +89,7 @@ export const store = configureStore({ apiV1.middleware ), reducer: { - rip: rip.reducer, socket, api: apiV1.reducer + rip: rip.reducer, config: config.reducer, socket, api: apiV1.reducer }, }) From df0841701d7d9e4fdc87433e65201d9ab316cfc0 Mon Sep 17 00:00:00 2001 From: enigma0Z <6117628+enigma0Z@users.noreply.github.com> Date: Thu, 11 Jun 2026 17:22:42 -0400 Subject: [PATCH 09/10] [minor] Config dialog rewrite --- .../fields/config/ConfigTextField.tsx | 39 ++++++++++++ .../ConfigDialog/AllConfigValuesTab.tsx | 51 +++++++++++++++ .../modals/ConfigDialog/LegacyTab.tsx | 62 ------------------- .../modals/ConfigDialog/MakeMkvTab.tsx | 20 ++++++ .../modals/ConfigDialog/RippingPathsTab.tsx | 19 ++++++ .../modals/ConfigDialog/ServerSettingsTab.tsx | 23 +++++++ .../modals/ConfigDialog/TmdbTab.tsx | 15 +++++ .../components/modals/ConfigDialog/index.tsx | 52 +++++++--------- 8 files changed, 190 insertions(+), 91 deletions(-) create mode 100644 web/src/components/fields/config/ConfigTextField.tsx create mode 100644 web/src/components/modals/ConfigDialog/AllConfigValuesTab.tsx delete mode 100644 web/src/components/modals/ConfigDialog/LegacyTab.tsx create mode 100644 web/src/components/modals/ConfigDialog/MakeMkvTab.tsx create mode 100644 web/src/components/modals/ConfigDialog/RippingPathsTab.tsx create mode 100644 web/src/components/modals/ConfigDialog/ServerSettingsTab.tsx create mode 100644 web/src/components/modals/ConfigDialog/TmdbTab.tsx diff --git a/web/src/components/fields/config/ConfigTextField.tsx b/web/src/components/fields/config/ConfigTextField.tsx new file mode 100644 index 0000000..e7ef9dd --- /dev/null +++ b/web/src/components/fields/config/ConfigTextField.tsx @@ -0,0 +1,39 @@ +import { useAppDispatch, useAppSelector } from "@/api"; +import { useGetConfigQuery } from "@/api/v1/config/api"; +import { configActions } from "@/api/v1/config/store" +import type { Config } from "@/api/v1/config/types"; +import { Box, CircularProgress, TextField } from "@mui/material" +import type React from "react" + +interface Props { + configItem: keyof Config + label?: string +} + +export const ConfigTextField = ({configItem, label}: Props) => { + + const dispatch = useAppDispatch() + + const fieldValue = useAppSelector((store) => store.config[configItem]) + const { data } = useGetConfigQuery() + const apiValue = data?.[configItem] + + type FieldOnChange = React.ChangeEvent + + const handleFieldUpdate = ({ target: { value } }: FieldOnChange) => { + dispatch(configActions.updateConfig({[configItem]: value})) + } + + return + + + +} \ No newline at end of file diff --git a/web/src/components/modals/ConfigDialog/AllConfigValuesTab.tsx b/web/src/components/modals/ConfigDialog/AllConfigValuesTab.tsx new file mode 100644 index 0000000..6f31ec3 --- /dev/null +++ b/web/src/components/modals/ConfigDialog/AllConfigValuesTab.tsx @@ -0,0 +1,51 @@ +import { ConfigTextField } from "@/components/fields/config/ConfigTextField" + +export const AllConfigValuesTab = () => { + + return <> + + + + + + {/* + TODO: Make this one a select with log levels + setFormData((prev) => ({...prev, log_level: event.target.value})) } + /> + */} + + + + + {/* */} + +} \ No newline at end of file diff --git a/web/src/components/modals/ConfigDialog/LegacyTab.tsx b/web/src/components/modals/ConfigDialog/LegacyTab.tsx deleted file mode 100644 index 7816011..0000000 --- a/web/src/components/modals/ConfigDialog/LegacyTab.tsx +++ /dev/null @@ -1,62 +0,0 @@ -import { useGetConfigQuery } from "@/api/v1/config/api" -import type { Config } from "@/api/v1/config/types" -import { TextField } from "@mui/material" -import { useState } from "react" - -interface Props { - formData: Partial - setFormData: React.Dispatch>> -} - -export const AllConfigValuesTab = (props: Props) => { - const { formData, setFormData } = props - - - - return <> - setFormData((prev) => ({...prev, config_file: event.target.value})) } - /> - setFormData((prev) => ({...prev, source: event.target.value})) } - /> - setFormData((prev) => ({...prev, destination: event.target.value})) } - /> - setFormData((prev) => ({...prev, tmdb_token: event.target.value})) } - /> - setFormData((prev) => ({...prev, makemkvcon_path: event.target.value})) } - /> - {/* - TODO: Make this one a select with log levels - setFormData((prev) => ({...prev, log_level: event.target.value})) } - /> - */} - setFormData((prev) => ({...prev, log_file: event.target.value})) } - /> - setFormData((prev) => ({...prev, temp_prefix: event.target.value})) } - /> - setFormData((prev) => ({...prev, frontend: event.target.value})) } - /> - setFormData((prev) => ({...prev, listen_port: event.target.value})) } - /> - {/* */} - -} \ No newline at end of file diff --git a/web/src/components/modals/ConfigDialog/MakeMkvTab.tsx b/web/src/components/modals/ConfigDialog/MakeMkvTab.tsx new file mode 100644 index 0000000..61d6cd3 --- /dev/null +++ b/web/src/components/modals/ConfigDialog/MakeMkvTab.tsx @@ -0,0 +1,20 @@ +import { ConfigTextField } from "@/components/fields/config/ConfigTextField" +import { Link, List } from "@mui/material" + +export const MakeMkvTab = () => { + + return <> + +
    +
  • + Get MakeMKV (for Linux) +
  • +
  • + Purchase a license for MakeMKV +
  • +
+ +} \ No newline at end of file diff --git a/web/src/components/modals/ConfigDialog/RippingPathsTab.tsx b/web/src/components/modals/ConfigDialog/RippingPathsTab.tsx new file mode 100644 index 0000000..2e11bf1 --- /dev/null +++ b/web/src/components/modals/ConfigDialog/RippingPathsTab.tsx @@ -0,0 +1,19 @@ +import { ConfigTextField } from "@/components/fields/config/ConfigTextField" + +export const RippingPathsTab = () => { + + return <> + + + + +} \ No newline at end of file diff --git a/web/src/components/modals/ConfigDialog/ServerSettingsTab.tsx b/web/src/components/modals/ConfigDialog/ServerSettingsTab.tsx new file mode 100644 index 0000000..bf233a9 --- /dev/null +++ b/web/src/components/modals/ConfigDialog/ServerSettingsTab.tsx @@ -0,0 +1,23 @@ +import { ConfigTextField } from "@/components/fields/config/ConfigTextField" + +export const ServerSettingsTab = () => { + + return <> + + + + + +} \ No newline at end of file diff --git a/web/src/components/modals/ConfigDialog/TmdbTab.tsx b/web/src/components/modals/ConfigDialog/TmdbTab.tsx new file mode 100644 index 0000000..42a4506 --- /dev/null +++ b/web/src/components/modals/ConfigDialog/TmdbTab.tsx @@ -0,0 +1,15 @@ +import { ConfigTextField } from "@/components/fields/config/ConfigTextField" +import { Link } from "@mui/material" + +export const TmdbTab = () => { + + return <> + + + Get an API key + + +} \ No newline at end of file diff --git a/web/src/components/modals/ConfigDialog/index.tsx b/web/src/components/modals/ConfigDialog/index.tsx index c863710..e030c1f 100644 --- a/web/src/components/modals/ConfigDialog/index.tsx +++ b/web/src/components/modals/ConfigDialog/index.tsx @@ -1,12 +1,16 @@ -import { Box, Button, Dialog, IconButton, Tab, Tabs, TextField, Typography } from "@mui/material" +import { Button, Dialog, IconButton, Tab, Tabs, Typography } from "@mui/material" import RefreshIcon from '@mui/icons-material/Refresh'; import CloseIcon from '@mui/icons-material/Close'; -import { useGetConfigQuery, usePutConfigMutation, useReloadConfigMutation } from "@/api/v1/config/api"; +import { usePutConfigMutation, useReloadConfigMutation } from "@/api/v1/config/api"; import { useState } from "react"; -import type { Config } from "@/api/v1/config/types"; import { ConfigDialogActions, ConfigDialogContent, ConfigDialogTitle, StyledTabContent } from "./index.styles"; -import { AllConfigValuesTab } from "./LegacyTab"; +import { AllConfigValuesTab } from "./AllConfigValuesTab"; +import { useAppSelector } from "@/api"; +import { RippingPathsTab } from "./RippingPathsTab"; +import { ServerSettingsTab } from "./ServerSettingsTab"; +import { TmdbTab } from "./TmdbTab"; +import { MakeMkvTab } from "./MakeMkvTab"; type Props = { open: boolean @@ -35,6 +39,7 @@ const TabContent = (props: TabContentProps) => { hidden={value !== index} id={`vertical-tabpanel-${index}`} aria-labelledby={`vertical-tab-${index}`} + style={{width: "100%"}} {...other} > { value === index && ( @@ -49,39 +54,19 @@ const TabContent = (props: TabContentProps) => { export const ConfigDialog = ({ open, onClose = () => {} }: Props) => { const [putConfig, _putConfigResult] = usePutConfigMutation() const [reloadConfig, _reloadConfigResult] = useReloadConfigMutation() - const [currentTab, setCurrentTab] = useState(0) - const [formData, setFormData] = useState>({}) - - const { data, isLoading, isSuccess } = useGetConfigQuery() - formData.config_file = formData?.config_file ?? data?.config_file ?? ''; - formData.source = formData?.source ?? data?.source ?? ''; - formData.destination = formData?.destination ?? data?.destination ?? ''; - formData.tmdb_token = formData?.tmdb_token ?? data?.tmdb_token ?? ''; - formData.makemkvcon_path = formData?.makemkvcon_path ?? data?.makemkvcon_path ?? ''; - // const log_level = formData?.log_level ?? data?.log_level ?? ''; - formData.log_file = formData?.log_file ?? data?.log_file ?? ''; - formData.temp_prefix = formData?.temp_prefix ?? data?.temp_prefix ?? ''; - formData.frontend = formData?.frontend ?? data?.frontend ?? ''; - formData.listen_port = formData?.listen_port ?? data?.listen_port ?? ''; + const config = useAppSelector(state => state.config) - // useEffect(() => { - // if (!isLoading && isSuccess) { - // setFormFromApi(data) - // } - // }, [isLoading, isSuccess]) - const handleClose = () => { onClose() } const handleSave = () => { - putConfig(formData) + putConfig(config) onClose() } const handleRefresh = () => { reloadConfig() - setFormData({}) } const handleChangeTab = (_event: React.SyntheticEvent, newValue: number) => { @@ -102,17 +87,26 @@ export const ConfigDialog = ({ open, onClose = () => {} }: Props) => { onChange={handleChangeTab} sx={{ borderRight: 1, borderColor: 'divider'}} > - + - Tab Two! + + + + + + + + + + - + From befc1670844f183bbf8b454454345d67acab1ba8 Mon Sep 17 00:00:00 2001 From: enigma0Z <6117628+enigma0Z@users.noreply.github.com> Date: Thu, 11 Jun 2026 17:34:39 -0400 Subject: [PATCH 10/10] Fix linter complaints --- web/src/api/endpoints.ts | 3 +-- web/src/api/index.ts | 4 ++++ web/src/api/v1/config/api.ts | 2 +- web/src/api/v1/state/api.ts | 2 +- web/src/components/forms/ContentForm/index.tsx | 4 ++-- web/src/components/modals/ConfigDialog/MakeMkvTab.tsx | 2 +- web/src/components/modals/ConfigDialog/index.styles.ts | 2 +- 7 files changed, 11 insertions(+), 8 deletions(-) diff --git a/web/src/api/endpoints.ts b/web/src/api/endpoints.ts index e76c045..3ac2607 100644 --- a/web/src/api/endpoints.ts +++ b/web/src/api/endpoints.ts @@ -1,8 +1,7 @@ -import type { TmdbConfiguration } from "./v1/tmdb/store" import type { Toc as TocV1 } from "./v1/toc/types" import { BACKEND, type Response as ResponseV1 } from "./v1" import type { State as StateV1 } from "./v1/state/types" -import type { TmdbSearchResultMovie, TmdbSearchResultShow } from "./v1/tmdb/types" +import type { TmdbConfiguration, TmdbSearchResultMovie, TmdbSearchResultShow } from "./v1/tmdb/types" import type { Config } from "./v1/config/types" import type { ApiError } from "./v1/error/types" diff --git a/web/src/api/index.ts b/web/src/api/index.ts index ae650c6..4777ba1 100644 --- a/web/src/api/index.ts +++ b/web/src/api/index.ts @@ -18,6 +18,10 @@ export interface RootState { socket: SocketState } +export type ThunkExtraArgument = { + socketConnection: SocketConnection +} + // Exploded currying syntax, for reference // const updateApiMiddleware: Middleware<{}, RootState> = (store) => { // return function wrapDispatch(next) { // next diff --git a/web/src/api/v1/config/api.ts b/web/src/api/v1/config/api.ts index ab06d85..8cb667d 100644 --- a/web/src/api/v1/config/api.ts +++ b/web/src/api/v1/config/api.ts @@ -1,4 +1,4 @@ -import { endpointsV1, type ApiModel } from "@/api/endpoints" +import { type ApiModel } from "@/api/endpoints" import { api } from ".." import type { Config } from "./types" diff --git a/web/src/api/v1/state/api.ts b/web/src/api/v1/state/api.ts index cd5f930..aec3a46 100644 --- a/web/src/api/v1/state/api.ts +++ b/web/src/api/v1/state/api.ts @@ -1,4 +1,4 @@ -import { type ApiModel, endpointsV1 } from "@/api/endpoints"; +import { type ApiModel } from "@/api/endpoints"; import type { State } from "./types"; import { api } from ".."; diff --git a/web/src/components/forms/ContentForm/index.tsx b/web/src/components/forms/ContentForm/index.tsx index dbbe720..58eb82c 100644 --- a/web/src/components/forms/ContentForm/index.tsx +++ b/web/src/components/forms/ContentForm/index.tsx @@ -10,7 +10,7 @@ import { useGetTmdbConfigurationQuery, useGetTmdbSearchQuery } from "@/api/v1/tm export const CombinedShowMovieForm = ({ onError, onClearError }: BaseProps) => { - const { error: stateError, isLoading, isSuccess } = useGetStateQuery() + const { error: stateError, isLoading } = useGetStateQuery() const rip = useAppSelector((state) => state.rip) @@ -29,7 +29,7 @@ export const CombinedShowMovieForm = ({ onError, onClearError }: BaseProps) => { const { data: tmdbConfiguration } = useGetTmdbConfigurationQuery() - const tmdbSelection = rip?.tmdb_selection ?? '' + const tmdbSelection = rip?.tmdb_selection const [ nameValue, setNameValue ] = useState(null) // const [ nameOptions, setNameOptions ] = useState<(TmdbSearchResult)[]>([]) diff --git a/web/src/components/modals/ConfigDialog/MakeMkvTab.tsx b/web/src/components/modals/ConfigDialog/MakeMkvTab.tsx index 61d6cd3..cbffba2 100644 --- a/web/src/components/modals/ConfigDialog/MakeMkvTab.tsx +++ b/web/src/components/modals/ConfigDialog/MakeMkvTab.tsx @@ -1,5 +1,5 @@ import { ConfigTextField } from "@/components/fields/config/ConfigTextField" -import { Link, List } from "@mui/material" +import { Link } from "@mui/material" export const MakeMkvTab = () => { diff --git a/web/src/components/modals/ConfigDialog/index.styles.ts b/web/src/components/modals/ConfigDialog/index.styles.ts index 96b57b5..db9c39e 100644 --- a/web/src/components/modals/ConfigDialog/index.styles.ts +++ b/web/src/components/modals/ConfigDialog/index.styles.ts @@ -1,4 +1,4 @@ -import { Box, DialogActions, DialogContent, DialogTitle, styled, Tabs } from "@mui/material" +import { Box, DialogActions, DialogContent, DialogTitle, styled } from "@mui/material" export const StyledTabContent = styled(Box)(({}) => ({ marginLeft: '1em'