diff --git a/package.json b/package.json index b4bc405d0..ced2971f9 100644 --- a/package.json +++ b/package.json @@ -57,6 +57,7 @@ "@stylistic/stylelint-plugin": "^3.1.1", "@tanstack/eslint-plugin-query": "^5.62.1", "@types/format-thousands": "^2.0.3", + "@types/json-schema": "^7.0.15", "@types/lodash": "^4.17.13", "@types/node": "^22.10.2", "@types/react": "^19.0.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0cb7fd05c..966f66ea0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -150,6 +150,9 @@ importers: '@types/format-thousands': specifier: ^2.0.3 version: 2.0.3 + '@types/json-schema': + specifier: ^7.0.15 + version: 7.0.15 '@types/lodash': specifier: ^4.17.13 version: 4.17.13 @@ -1276,6 +1279,9 @@ packages: '@types/hoist-non-react-statics@3.3.6': resolution: {integrity: sha512-lPByRJUer/iN/xa4qpyL0qmL11DqNW81iU/IG1S3uvRUq4oKagz8VCxZjiWkumgt66YT3vOdDgZ0o32sGKtCEw==} + '@types/json-schema@7.0.15': + resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + '@types/json5@0.0.29': resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} @@ -4788,6 +4794,8 @@ snapshots: '@types/react': 19.0.1 hoist-non-react-statics: 3.3.2 + '@types/json-schema@7.0.15': {} + '@types/json5@0.0.29': {} '@types/lodash@4.17.13': {} diff --git a/src/components/Collection/Episode/EpisodeFiles.tsx b/src/components/Collection/Episode/EpisodeFiles.tsx index 6f8d603ec..cf315785d 100644 --- a/src/components/Collection/Episode/EpisodeFiles.tsx +++ b/src/components/Collection/Episode/EpisodeFiles.tsx @@ -9,7 +9,7 @@ import { mdiTrashCanOutline, } from '@mdi/js'; import { Icon } from '@mdi/react'; -import { get, map } from 'lodash'; +import { map } from 'lodash'; import DeleteFilesModal from '@/components/Dialogs/DeleteFilesModal'; import FileInfo from '@/components/FileInfo'; @@ -92,8 +92,7 @@ const EpisodeFiles = ({ anidbSeriesId, episodeFiles, episodeId, seriesId }: Prop return (
{map(episodeFiles, (file) => { - const ReleaseGroupID = get(file, 'AniDB.ReleaseGroup.ID', 0); - const ReleaseGroupName = get(file, 'AniDB.ReleaseGroup.Name', null); + const releaseGroup = file.ReleaseInfo?.Group; return (
@@ -141,8 +140,8 @@ const EpisodeFiles = ({ anidbSeriesId, episodeFiles, episodeId, seriesId }: Prop /> Copy ShokoID
- {file.AniDB && ( - + {file.ReleaseInfo?.ReleaseURI?.startsWith('https://anidb.net/file/') && ( +
AniDB @@ -150,15 +149,15 @@ const EpisodeFiles = ({ anidbSeriesId, episodeFiles, episodeId, seriesId }: Prop
)} - {ReleaseGroupID > 0 && ( + {releaseGroup && releaseGroup.Source === 'AniDB' && (
- {ReleaseGroupName ?? 'Unknown'} + {releaseGroup.Name}  (AniDB)
diff --git a/src/components/Collection/TimelineSidebar.tsx b/src/components/Collection/TimelineSidebar.tsx index b96ea390b..35fff299f 100644 --- a/src/components/Collection/TimelineSidebar.tsx +++ b/src/components/Collection/TimelineSidebar.tsx @@ -11,9 +11,9 @@ import type { SeriesType } from '@/core/types/api/series'; const TimelineItem = ({ series }: { series: SeriesType }) => { const mainPoster = useMainPoster(series); - const seriesType = series.AniDB?.Type === SeriesTypeEnum.TVSpecial - ? 'TV Special' - : series.AniDB?.Type; + let seriesType = series.AniDB?.Type as string | undefined; + if (seriesType === SeriesTypeEnum.TVSpecial) seriesType = 'TV Special'; + else if (seriesType === SeriesTypeEnum.MusicVideo) seriesType = 'Music Video'; return (
diff --git a/src/components/Dashboard/DashboardSettingsModal.tsx b/src/components/Dashboard/DashboardSettingsModal.tsx index 827ab5947..9cf850796 100644 --- a/src/components/Dashboard/DashboardSettingsModal.tsx +++ b/src/components/Dashboard/DashboardSettingsModal.tsx @@ -57,7 +57,7 @@ const DashboardSettingsModal = ({ onClose, show }: Props) => { combineContinueWatching, hideCollectionStats, hideContinueWatching, - hideImportFolders, + hideManagedFolders, hideMediaType, hideNextUp, hideQueueProcessor, @@ -175,9 +175,9 @@ const DashboardSettingsModal = ({ onClose, show }: Props) => { /> handleClose()} - header="Select Import Folder" + header="Select Managed Folder" size="sm" overlayClassName="!z-[90]" > diff --git a/src/components/Dialogs/ImportFolderModal.tsx b/src/components/Dialogs/ManagedFolderModal.tsx similarity index 70% rename from src/components/Dialogs/ImportFolderModal.tsx rename to src/components/Dialogs/ManagedFolderModal.tsx index b89f68efb..630dca707 100644 --- a/src/components/Dialogs/ImportFolderModal.tsx +++ b/src/components/Dialogs/ManagedFolderModal.tsx @@ -9,55 +9,55 @@ import Select from '@/components/Input/Select'; import ModalPanel from '@/components/Panels/ModalPanel'; import toast from '@/components/Toast'; import { - useCreateImportFolderMutation, - useDeleteImportFolderMutation, - useUpdateImportFolderMutation, -} from '@/core/react-query/import-folder/mutations'; -import { useImportFoldersQuery } from '@/core/react-query/import-folder/queries'; + useCreateManagedFolderMutation, + useDeleteManagedFolderMutation, + useUpdateManagedFolderMutation, +} from '@/core/react-query/managed-folder/mutations'; +import { useManagedFoldersQuery } from '@/core/react-query/managed-folder/queries'; import { setStatus as setBrowseStatus } from '@/core/slices/modals/browseFolder'; -import { setStatus } from '@/core/slices/modals/importFolder'; +import { setStatus } from '@/core/slices/modals/managedFolder'; import useEventCallback from '@/hooks/useEventCallback'; import BrowseFolderModal from './BrowseFolderModal'; import type { RootState } from '@/core/store'; -import type { ImportFolderType } from '@/core/types/api/import-folder'; +import type { ManagedFolderType } from '@/core/types/api/managed-folder'; -const defaultImportFolder = { +const defaultManagedFolder = { WatchForNewFiles: false, DropFolderType: 'None', Path: '', Name: '', ID: 0, -} as ImportFolderType; +} as ManagedFolderType; -function ImportFolderModal() { +function ManagedFolderModal() { const dispatch = useDispatch(); - const { ID, edit, status } = useSelector((state: RootState) => state.modals.importFolder); + const { ID, edit, status } = useSelector((state: RootState) => state.modals.managedFolder); - const importFolderQuery = useImportFoldersQuery(); - const importFolders = importFolderQuery?.data ?? [] as ImportFolderType[]; + const managedFolderQuery = useManagedFoldersQuery(); + const managedFolders = managedFolderQuery?.data ?? [] as ManagedFolderType[]; - const { isPending: isCreatePending, mutate: createFolder } = useCreateImportFolderMutation(); - const { isPending: isDeletePending, mutate: deleteFolder } = useDeleteImportFolderMutation(); - const { isPending: isUpdatePending, mutate: updateFolder } = useUpdateImportFolderMutation(); + const { isPending: isCreatePending, mutate: createFolder } = useCreateManagedFolderMutation(); + const { isPending: isDeletePending, mutate: deleteFolder } = useDeleteManagedFolderMutation(); + const { isPending: isUpdatePending, mutate: updateFolder } = useUpdateManagedFolderMutation(); - const [importFolder, setImportFolder] = useState(defaultImportFolder); + const [managedFolder, setManagedFolder] = useState(defaultManagedFolder); const getFolderDetails = () => { - setImportFolder(defaultImportFolder); + setManagedFolder(defaultManagedFolder); if (edit) { - const folderDetails = find(importFolders, { ID }) ?? {}; - setImportFolder({ ...importFolder, ...folderDetails }); + const folderDetails = find(managedFolders, { ID }) ?? {}; + setManagedFolder({ ...managedFolder, ...folderDetails }); } }; const handleInputChange = (event: React.ChangeEvent) => { const name = event.target.id; const value = name === 'WatchForNewFiles' ? event.target.value === '1' : event.target.value; - setImportFolder({ ...importFolder, [name]: value }); + setManagedFolder({ ...managedFolder, [name]: value }); }; const handleBrowse = () => dispatch(setBrowseStatus(true)); @@ -66,7 +66,7 @@ function ImportFolderModal() { const handleDelete = useEventCallback(() => { deleteFolder({ folderId: ID }, { onSuccess: () => { - toast.success('Import folder deleted!'); + toast.success('Managed folder deleted!'); dispatch(setStatus(false)); }, }); @@ -74,23 +74,23 @@ function ImportFolderModal() { const handleSave = useEventCallback(() => { if (edit) { - updateFolder(importFolder, { + updateFolder(managedFolder, { onSuccess: () => { - toast.success('Import folder edited!'); + toast.success('Managed folder edited!'); dispatch(setStatus(false)); }, }); } else { - createFolder(importFolder, { + createFolder(managedFolder, { onSuccess: () => { - toast.success('Import folder added!'); + toast.success('Managed folder added!'); dispatch(setStatus(false)); }, }); } }); - const onFolderSelect = (Path: string) => setImportFolder({ ...importFolder, Path }); + const onFolderSelect = (Path: string) => setManagedFolder({ ...managedFolder, Path }); const isLoading = isCreatePending || isDeletePending || isUpdatePending; return ( @@ -99,7 +99,7 @@ function ImportFolderModal() { show={status} onRequestClose={handleClose} onAfterOpen={() => getFolderDetails()} - header={edit ? 'Edit Import Folder' : 'Add New Import Folder'} + header={edit ? 'Edit Managed Folder' : 'Add New Managed Folder'} size="sm" noPadding > @@ -107,7 +107,7 @@ function ImportFolderModal() {
@@ -139,7 +139,7 @@ function ImportFolderModal() { - {file.AniDB?.ID && ( + {file.ReleaseInfo?.ReleaseURI?.startsWith('https://anidb.net/file/') && (
- {file.AniDB.ID} + {file.ReleaseInfo.ReleaseURI.split('/').pop()} (AniDB) @@ -150,17 +150,17 @@ const Episode = ({ episode, setFileOptions, type }: Props) => { - {file.AniDB?.ID && ( + {file.ReleaseInfo?.ReleaseURI?.startsWith('https://anidb.net/file/') && (
- {file.AniDB.ID} + {file.ReleaseInfo.ReleaseURI.split('/').pop()} (AniDB) diff --git a/src/components/Utilities/ReleaseManagement/QuickSelectModal.tsx b/src/components/Utilities/ReleaseManagement/QuickSelectModal.tsx index 01d96db03..3bfb5b6c0 100644 --- a/src/components/Utilities/ReleaseManagement/QuickSelectModal.tsx +++ b/src/components/Utilities/ReleaseManagement/QuickSelectModal.tsx @@ -7,13 +7,13 @@ import Checkbox from '@/components/Input/Checkbox'; import ModalPanel from '@/components/Panels/ModalPanel'; import toast from '@/components/Toast'; import { useDeleteFilesMutation } from '@/core/react-query/file/mutations'; -import { useImportFoldersQuery } from '@/core/react-query/import-folder/queries'; +import { useManagedFoldersQuery } from '@/core/react-query/managed-folder/queries'; import { resetQueries } from '@/core/react-query/queryClient'; import { ReleaseManagementItemType } from '@/core/react-query/release-management/types'; import { useSeriesFileSummaryQuery } from '@/core/react-query/webui/queries'; import useEventCallback from '@/hooks/useEventCallback'; -import type { ImportFolderType } from '@/core/types/api/import-folder'; +import type { ManagedFolderType } from '@/core/types/api/managed-folder'; type Props = { show: boolean; @@ -27,24 +27,24 @@ const QuickSelectModal = ({ onClose, seriesId, show, type }: Props) => { seriesId, { groupBy: type === ReleaseManagementItemType.MultipleReleases - ? 'GroupName,FileSource,FileVersion,ImportFolder,VideoCodecs,VideoResolution,AudioLanguages,SubtitleLanguages,VideoHasChapters' - : 'ImportFolder,FileLocation,MultipleLocations', + ? 'GroupName,FileSource,FileVersion,ManagedFolder,VideoCodecs,VideoResolution,AudioLanguages,SubtitleLanguages,VideoHasChapters' + : 'ManagedFolder,FileLocation,MultipleLocations', includeEpisodeDetails: true, }, show, ); const fileSummary = fileSummaryQuery.data; - const importFoldersQuery = useImportFoldersQuery(); - const importFolders = useMemo>(() => { + const managedFoldersQuery = useManagedFoldersQuery(); + const managedFolders = useMemo>(() => { const result = {}; - forEach(importFoldersQuery.data, (folder) => { + forEach(managedFoldersQuery.data, (folder) => { result[folder.ID] = folder; }); return result; - }, [importFoldersQuery]); + }, [managedFoldersQuery]); const { isPending: isDeleting, mutate: deleteFiles } = useDeleteFilesMutation(); @@ -91,7 +91,7 @@ const QuickSelectModal = ({ onClose, seriesId, show, type }: Props) => { map( fileSummary?.Groups, (group, index) => { - const importFolder = importFolders[group.ImportFolder!]; + const managedFolder = managedFolders[group.ManagedFolder!]; return (
@@ -99,12 +99,12 @@ const QuickSelectModal = ({ onClose, seriesId, show, type }: Props) => { {type === ReleaseManagementItemType.DuplicateFiles && ( <>
- Import Folder:  - {`${importFolder.Name} (ID: ${importFolder.ID})`} + Managed Folder:  + {`${managedFolder.Name} (ID: ${managedFolder.ID})`}
Location:  - {group.FileLocation?.replace(importFolder.Path, '')} + {group.FileLocation?.replace(managedFolder.Path, '')}
{group.Episodes?.length} @@ -138,8 +138,8 @@ const QuickSelectModal = ({ onClose, seriesId, show, type }: Props) => { {`v${group.FileVersion}`}
- Import Folder:  - {`${importFolder.Name} (ID: ${importFolder.ID})`} + Managed Folder:  + {`${managedFolder.Name} (ID: ${managedFolder.ID})`}
{group.FileSource} diff --git a/src/components/Utilities/constants.tsx b/src/components/Utilities/constants.tsx index 4bad515c9..ddf7a73d2 100644 --- a/src/components/Utilities/constants.tsx +++ b/src/components/Utilities/constants.tsx @@ -18,7 +18,7 @@ export type UtilityHeaderType; export const criteriaMap = { - importFolder: FileSortCriteriaEnum.ImportFolderName, + managedFolder: FileSortCriteriaEnum.ManagedFolderName, filename: FileSortCriteriaEnum.FileName, crc32: FileSortCriteriaEnum.CRC32, size: FileSortCriteriaEnum.FileSize, @@ -56,7 +56,7 @@ export const staticColumns: UtilityHeaderType[] = [ id: 'crc32', name: 'CRC32', className: 'w-32', - item: file => file.Hashes.CRC32, + item: file => file.Hashes.find(hash => hash.Type === 'CRC32')?.Value, }, { id: 'size', diff --git a/src/core/patches.ts b/src/core/patches.ts index 121177ee0..6346d2aed 100644 --- a/src/core/patches.ts +++ b/src/core/patches.ts @@ -45,5 +45,17 @@ export const webuiSettingsPatches = { webuiSettings.collection.anidb.filterDescription = false; return { ...webuiSettings, settingsRevision: 10 }; }, + 11: (oldWebuiSettings) => { + const webuiSettings = oldWebuiSettings; + webuiSettings.dashboard.hideManagedFolders = webuiSettings.dashboard.hideImportFolders; + delete webuiSettings.dashboard.hideImportFolders; + // eslint-disable-next-line @typescript-eslint/no-unsafe-call + let layoutItem = webuiSettings.layout.dashboard.lg.find(item => item.i === 'importFolders'); + if (layoutItem) layoutItem.i = 'managedFolders'; + // eslint-disable-next-line @typescript-eslint/no-unsafe-call + layoutItem = webuiSettings.layout.dashboard.md.find(item => item.i === 'importFolders'); + if (layoutItem) layoutItem.i = 'managedFolders'; + return { ...webuiSettings, settingsRevision: 11 }; + }, // eslint-disable-next-line @typescript-eslint/no-explicit-any } as Record WebUISettingsType>; diff --git a/src/core/quick-actions.ts b/src/core/quick-actions.ts index 510441bb6..645201c89 100644 --- a/src/core/quick-actions.ts +++ b/src/core/quick-actions.ts @@ -66,7 +66,7 @@ const quickActions = { 'import-new-files': { name: 'Import New Files', functionName: 'ImportNewFiles', - info: 'Queues a task to import only new files found in the import folder', + info: 'Queues a task to import only new files found in the managed folders.', }, 'avdump-mismatched-files': { name: 'AVDump Mismatched Files', diff --git a/src/core/react-query/dashboard/helpers.ts b/src/core/react-query/dashboard/helpers.ts index 70149c4d0..3e65a2a9f 100644 --- a/src/core/react-query/dashboard/helpers.ts +++ b/src/core/react-query/dashboard/helpers.ts @@ -2,8 +2,10 @@ import type { DashboardSeriesSummaryType } from '@/core/types/api/dashboard'; export const transformSeriesSummary = (response: DashboardSeriesSummaryType) => { const result = response; - result.Other += (result?.Special ?? 0) + (result?.None ?? 0); + result.Other += (result?.Special ?? 0) + (result?.MusicVideo ?? 0) + (result?.Unknown ?? 0) + (result?.None ?? 0); delete result.Special; + delete result.MusicVideo; + delete result.Unknown; delete result.None; return result; }; diff --git a/src/core/react-query/import-folder/mutations.ts b/src/core/react-query/import-folder/mutations.ts deleted file mode 100644 index da02b6fb8..000000000 --- a/src/core/react-query/import-folder/mutations.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { useMutation } from '@tanstack/react-query'; - -import { axios } from '@/core/axios'; -import { invalidateQueries } from '@/core/react-query/queryClient'; - -import type { DeleteImportFolderRequestType } from '@/core/react-query/import-folder/types'; -import type { ImportFolderType } from '@/core/types/api/import-folder'; - -export const useCreateImportFolderMutation = () => - useMutation({ - mutationFn: (folder: ImportFolderType) => axios.post('ImportFolder', folder), - onSuccess: () => invalidateQueries(['import-folder']), - }); - -export const useDeleteImportFolderMutation = () => - useMutation({ - mutationFn: ({ folderId, ...data }: DeleteImportFolderRequestType) => - axios.delete(`ImportFolder/${folderId}`, { data }), - onSuccess: () => invalidateQueries(['import-folder']), - }); - -export const useRescanImportFolderMutation = () => - useMutation({ - mutationFn: (folderId: number) => axios.get(`ImportFolder/${folderId}/Scan`), - }); - -export const useUpdateImportFolderMutation = () => - useMutation({ - mutationFn: (folder: ImportFolderType) => axios.put('ImportFolder', folder), - onSuccess: () => invalidateQueries(['import-folder']), - }); diff --git a/src/core/react-query/import-folder/queries.ts b/src/core/react-query/import-folder/queries.ts deleted file mode 100644 index 516981f74..000000000 --- a/src/core/react-query/import-folder/queries.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { useQuery } from '@tanstack/react-query'; - -import { axios } from '@/core/axios'; - -import type { ImportFolderType } from '@/core/types/api/import-folder'; - -export const useImportFoldersQuery = () => - useQuery({ - queryKey: ['import-folder'], - queryFn: () => axios.get('ImportFolder'), - }); diff --git a/src/core/react-query/managed-folder/mutations.ts b/src/core/react-query/managed-folder/mutations.ts new file mode 100644 index 000000000..041df53f4 --- /dev/null +++ b/src/core/react-query/managed-folder/mutations.ts @@ -0,0 +1,31 @@ +import { useMutation } from '@tanstack/react-query'; + +import { axios } from '@/core/axios'; +import { invalidateQueries } from '@/core/react-query/queryClient'; + +import type { DeleteManagedFolderRequestType } from '@/core/react-query/managed-folder/types'; +import type { ManagedFolderType } from '@/core/types/api/managed-folder'; + +export const useCreateManagedFolderMutation = () => + useMutation({ + mutationFn: (folder: ManagedFolderType) => axios.post('ManagedFolder', folder), + onSuccess: () => invalidateQueries(['managed-folder']), + }); + +export const useDeleteManagedFolderMutation = () => + useMutation({ + mutationFn: ({ folderId, ...data }: DeleteManagedFolderRequestType) => + axios.delete(`ManagedFolder/${folderId}`, { data }), + onSuccess: () => invalidateQueries(['managed-folder']), + }); + +export const useRescanManagedFolderMutation = () => + useMutation({ + mutationFn: (folderId: number) => axios.get(`ManagedFolder/${folderId}/Scan`), + }); + +export const useUpdateManagedFolderMutation = () => + useMutation({ + mutationFn: (folder: ManagedFolderType) => axios.put('ManagedFolder', folder), + onSuccess: () => invalidateQueries(['managed-folder']), + }); diff --git a/src/core/react-query/managed-folder/queries.ts b/src/core/react-query/managed-folder/queries.ts new file mode 100644 index 000000000..5aed13b22 --- /dev/null +++ b/src/core/react-query/managed-folder/queries.ts @@ -0,0 +1,11 @@ +import { useQuery } from '@tanstack/react-query'; + +import { axios } from '@/core/axios'; + +import type { ManagedFolderType } from '@/core/types/api/managed-folder'; + +export const useManagedFoldersQuery = () => + useQuery({ + queryKey: ['managed-folder'], + queryFn: () => axios.get('ManagedFolder'), + }); diff --git a/src/core/react-query/import-folder/types.ts b/src/core/react-query/managed-folder/types.ts similarity index 61% rename from src/core/react-query/import-folder/types.ts rename to src/core/react-query/managed-folder/types.ts index 6d7948a13..f3afdf5c1 100644 --- a/src/core/react-query/import-folder/types.ts +++ b/src/core/react-query/managed-folder/types.ts @@ -1,4 +1,4 @@ -export type DeleteImportFolderRequestType = { +export type DeleteManagedFolderRequestType = { folderId: number; removeRecords?: boolean; updateMyList?: boolean; diff --git a/src/core/react-query/settings/helpers.ts b/src/core/react-query/settings/helpers.ts index cc7c48b8e..470aa378c 100644 --- a/src/core/react-query/settings/helpers.ts +++ b/src/core/react-query/settings/helpers.ts @@ -67,7 +67,7 @@ const initialLayout = { static: false, }, { - i: 'importFolders', + i: 'managedFolders', x: 6, y: 37, w: 3, @@ -190,7 +190,7 @@ const initialLayout = { static: false, }, { - i: 'importFolders', + i: 'managedFolders', x: 5, y: 51, w: 5, @@ -298,7 +298,7 @@ export const initialSettings: SettingsType = { hideRecentlyImported: false, hideCollectionStats: false, hideMediaType: false, - hideImportFolders: false, + hideManagedFolders: false, hideShokoNews: false, hideContinueWatching: false, hideNextUp: false, diff --git a/src/core/router/index.tsx b/src/core/router/index.tsx index 42c3df537..67fa7fc81 100644 --- a/src/core/router/index.tsx +++ b/src/core/router/index.tsx @@ -22,8 +22,8 @@ import Acknowledgement from '@/pages/firstrun/Acknowledgement'; import AniDBAccount from '@/pages/firstrun/AniDBAccount'; import DataCollection from '@/pages/firstrun/DataCollection'; import FirstRunPage from '@/pages/firstrun/FirstRunPage'; -import ImportFolders from '@/pages/firstrun/ImportFolders'; import LocalAccount from '@/pages/firstrun/LocalAccount'; +import ManagedFolders from '@/pages/firstrun/ManagedFolders'; import MetadataSources from '@/pages/firstrun/MetadataSources'; import StartServer from '@/pages/firstrun/StartServer'; import LoginPage from '@/pages/login/LoginPage'; @@ -69,7 +69,7 @@ const router = sentryCreateBrowserRouter( } /> } /> } /> - } /> + } /> } /> } /> diff --git a/src/core/signalr/eventHandlers.ts b/src/core/signalr/eventHandlers.ts index ac8095af3..4be5d4463 100644 --- a/src/core/signalr/eventHandlers.ts +++ b/src/core/signalr/eventHandlers.ts @@ -14,8 +14,8 @@ const invalidateFiles = debounce( 1000, ); -const invalidateImportFolders = debounce( - () => invalidateQueries(['import-folder']), +const invalidateManagedFolders = debounce( + () => invalidateQueries(['managed-folder']), 1000, ); @@ -47,12 +47,12 @@ export const handleEvent = (event: string, data?: SeriesUpdateEventType) => { case 'FileMatched': invalidateDashboard(); invalidateFiles(); - invalidateImportFolders(); + invalidateManagedFolders(); invalidateReleaseManagement(); break; case 'FileMoved': invalidateFiles(); - invalidateImportFolders(); + invalidateManagedFolders(); break; case 'FileRenamed': invalidateFiles(); @@ -62,7 +62,7 @@ export const handleEvent = (event: string, data?: SeriesUpdateEventType) => { break; case 'SeriesUpdated': invalidateDashboard(); - invalidateImportFolders(); + invalidateManagedFolders(); if (!data?.ShokoGroupIDs || !data?.ShokoSeriesIDs) return; invalidateSeries(data.ShokoSeriesIDs[0], data.ShokoGroupIDs); break; diff --git a/src/core/slices/modals.ts b/src/core/slices/modals.ts index d550c509d..1cbd53d36 100644 --- a/src/core/slices/modals.ts +++ b/src/core/slices/modals.ts @@ -3,12 +3,12 @@ import { combineReducers } from '@reduxjs/toolkit'; import browseFolderReducer from './modals/browseFolder'; import editGroupReducer from './modals/editGroup'; import editSeriesReducer from './modals/editSeries'; -import importFolderReducer from './modals/importFolder'; +import managedFolderReducer from './modals/managedFolder'; import profileReducer from './modals/profile'; export default combineReducers({ browseFolder: browseFolderReducer, - importFolder: importFolderReducer, + managedFolder: managedFolderReducer, profile: profileReducer, editSeries: editSeriesReducer, editGroup: editGroupReducer, diff --git a/src/core/slices/modals/importFolder.ts b/src/core/slices/modals/managedFolder.ts similarity index 75% rename from src/core/slices/modals/importFolder.ts rename to src/core/slices/modals/managedFolder.ts index bdaaa1651..a9f09343d 100644 --- a/src/core/slices/modals/importFolder.ts +++ b/src/core/slices/modals/managedFolder.ts @@ -2,8 +2,8 @@ import { createSlice } from '@reduxjs/toolkit'; import type { PayloadAction } from '@reduxjs/toolkit'; -const importFolderSlice = createSlice({ - name: 'importFolder', +const managedFolderSlice = createSlice({ + name: 'managedFolder', initialState: { status: false, edit: false, @@ -23,6 +23,6 @@ const importFolderSlice = createSlice({ }, }); -export const { setEdit, setStatus } = importFolderSlice.actions; +export const { setEdit, setStatus } = managedFolderSlice.actions; -export default importFolderSlice.reducer; +export default managedFolderSlice.reducer; diff --git a/src/core/slices/utilities/renamer.ts b/src/core/slices/utilities/renamer.ts index 80fe4c6f1..46155ff02 100644 --- a/src/core/slices/utilities/renamer.ts +++ b/src/core/slices/utilities/renamer.ts @@ -34,7 +34,7 @@ const renamerSlice = createSlice({ if (!file) return; if (result.RelativePath) file.Locations[0].RelativePath = result.RelativePath; if (result.AbsolutePath) file.Locations[0].AbsolutePath = result.AbsolutePath; - if (result.ImportFolderID) file.Locations[0].ImportFolderID = result.ImportFolderID; + if (result.ManagedFolderID) file.Locations[0].ManagedFolderID = result.ManagedFolderID; }); }, addRenameResults(sliceState, action: PayloadAction>) { diff --git a/src/core/types/api/collection.ts b/src/core/types/api/collection.ts index 0d79b04d4..a09fa708c 100644 --- a/src/core/types/api/collection.ts +++ b/src/core/types/api/collection.ts @@ -32,6 +32,7 @@ export type GroupSizesSeriesTypesType = { Web: number; Movie: number; OVA: number; + MusicVideo: number; }; export type CollectionFilterType = { diff --git a/src/core/types/api/dashboard.ts b/src/core/types/api/dashboard.ts index 3079a2a6b..523a3eb7e 100644 --- a/src/core/types/api/dashboard.ts +++ b/src/core/types/api/dashboard.ts @@ -8,6 +8,8 @@ export type DashboardSeriesSummaryType = { Special?: number; Web: number; Other: number; + MusicVideo?: number; + Unknown?: number; None?: number; }; diff --git a/src/core/types/api/file.ts b/src/core/types/api/file.ts index 0acf23146..d85471630 100644 --- a/src/core/types/api/file.ts +++ b/src/core/types/api/file.ts @@ -11,7 +11,7 @@ type XRefsType = { type FileTypeLocation = { ID: number; FileID: number; - ImportFolderID: number; + ManagedFolderID: number; RelativePath: string; AbsolutePath?: string; IsAccessible: boolean; @@ -20,12 +20,7 @@ type FileTypeLocation = { export type FileType = { ID: number; Size: number; - Hashes: { - ED2K: string; - SHA1: string; - CRC32: string; - MD5: string; - }; + Hashes: FileHashDigestType[]; Locations: FileTypeLocation[]; Duration: string; ResumePosition: string | null; @@ -35,30 +30,68 @@ export type FileType = { Updated: string; IsVariation: boolean; SeriesIDs?: XRefsType[]; - AniDB?: FileAniDBType; + ReleaseInfo?: ReleaseInfoType; MediaInfo?: FileMediaInfoType; AVDump: FileAVDumpType; }; -export type FileAniDBType = { +export type ReleaseInfoType = { + ID: string | null; + ProviderName: string; + ReleaseURI: string | null; + Revision: number; + FileSize: number | null; + Comment: string | null; + OriginalFilename: string | null; + IsCensored: boolean | null; + IsChaptered: boolean | null; + IsCorrupted: boolean; + Source: ReleaseSource; + Group: ReleaseGroupType | null; + Hashes: FileHashDigestType[] | null; + MediaInfo: ReleaseMediaInfoType | null; + CrossReferences: ReleaseCrossReferenceType[]; + Released: string | null; + Updated: string; + Created: string; +}; + +export enum ReleaseSource { + Unknown = 'Unknown', + Other = 'Other', + TV = 'TV', + DVD = 'DVD', + BluRay = 'BluRay', + Web = 'Web', + VHS = 'VHS', + VCD = 'VCD', + LaserDisc = 'LaserDisc', + Camera = 'Camera', +} + +export type ReleaseGroupType = { ID: number; - Source: FileSourceEnum; - ReleaseGroup: FileAniDBReleaseGroupType; - ReleaseDate: string | null; - Version: number; - IsDeprecated: boolean; - IsCensored: boolean; - OriginalFileName: string; - FileSize: bigint; - Duration: string; - Resolution: string; - Description: string; - AudioCodecs: string[]; + Name: string; + ShortName: string; + Source: string; +}; + +export type ReleaseCrossReferenceType = { + AnidbEpisodeID: number; + AnidbAnimeID: number | null; + PercentageStart: number; + PercentageEnd: number; +}; + +export type ReleaseMediaInfoType = { AudioLanguages: string[]; - SubLanguages: string[]; - VideoCodec: string; - Chaptered: boolean; - Updated: string; + SubtitleLanguages: string[]; +}; + +export type FileHashDigestType = { + Type: string; + Value: string; + Metadata?: string; }; export type FileAVDumpType = { @@ -214,8 +247,8 @@ export type FileMediaInfoChapterType = { export enum FileSortCriteriaEnum { None = 0, - ImportFolderName = 1, - ImportFolderID = 2, + ManagedFolderName = 1, + ManagedFolderID = 2, AbsolutePath = 3, RelativePath = 4, FileSize = 5, diff --git a/src/core/types/api/import-folder.ts b/src/core/types/api/managed-folder.ts similarity index 84% rename from src/core/types/api/import-folder.ts rename to src/core/types/api/managed-folder.ts index f0c93d58f..57aa714bb 100644 --- a/src/core/types/api/import-folder.ts +++ b/src/core/types/api/managed-folder.ts @@ -1,4 +1,4 @@ -export type ImportFolderType = { +export type ManagedFolderType = { ID: number; WatchForNewFiles?: boolean; DropFolderType?: 'None' | 'Source' | 'Destination' | 'Both'; diff --git a/src/core/types/api/renamer.ts b/src/core/types/api/renamer.ts index 194f26d9c..96683fa61 100644 --- a/src/core/types/api/renamer.ts +++ b/src/core/types/api/renamer.ts @@ -58,7 +58,7 @@ export type RenamerResultType = { FileID: number; FileLocationID?: number; ConfigName?: string; - ImportFolderID?: number; + ManagedFolderID?: number; IsSuccess: boolean; IsRelocated?: boolean; IsPreview?: boolean; diff --git a/src/core/types/api/series.ts b/src/core/types/api/series.ts index 243590d7d..8d6d6e011 100644 --- a/src/core/types/api/series.ts +++ b/src/core/types/api/series.ts @@ -75,6 +75,7 @@ export const enum SeriesTypeEnum { Web = 'Web', Movie = 'Movie', OVA = 'OVA', + MusicVideo = 'MusicVideo', } export const enum SeriesRelationTypeEnum { diff --git a/src/core/types/api/settings.ts b/src/core/types/api/settings.ts index d8b3f07c9..4466df545 100644 --- a/src/core/types/api/settings.ts +++ b/src/core/types/api/settings.ts @@ -420,7 +420,7 @@ export type WebUISettingsType = { hideRecentlyImported: boolean; hideCollectionStats: boolean; hideMediaType: boolean; - hideImportFolders: boolean; + hideManagedFolders: boolean; hideShokoNews: boolean; hideContinueWatching: boolean; hideNextUp: boolean; diff --git a/src/core/types/api/webui.ts b/src/core/types/api/webui.ts index 40862a63d..c7712d343 100644 --- a/src/core/types/api/webui.ts +++ b/src/core/types/api/webui.ts @@ -41,7 +41,7 @@ export type WebuiSeriesFileSummaryGroupType = { FileSource?: string; FileLocation?: string; FileIsDeprecated?: boolean; - ImportFolder?: number; + ManagedFolder?: number; VideoCodecs?: string; VideoBitDepth?: number; VideoResolution?: string; diff --git a/src/core/utilities/getEd2kLink.ts b/src/core/utilities/getEd2kLink.ts index 072dc0d07..b6b616a9e 100644 --- a/src/core/utilities/getEd2kLink.ts +++ b/src/core/utilities/getEd2kLink.ts @@ -1,6 +1,8 @@ import type { FileType } from '@/core/types/api/file'; const getEd2kLink = (file: FileType) => - `ed2k://|file|${file.Locations[0]?.RelativePath?.split(/[\\/]+/g).pop() ?? ''}|${file.Size}|${file.Hashes.ED2K}|/`; + `ed2k://|file|${file.Locations[0]?.RelativePath?.split(/[\\/]+/g).pop() ?? ''}|${file.Size}|${ + file.Hashes.find(hash => hash.Type === 'ED2K')!.Value + }|/`; export default getEd2kLink; diff --git a/src/hooks/useMediaInfo.ts b/src/hooks/useMediaInfo.ts index c64138b55..6add4b2b1 100644 --- a/src/hooks/useMediaInfo.ts +++ b/src/hooks/useMediaInfo.ts @@ -58,9 +58,9 @@ const useMediaInfo = (file: FileType): FileInfo => const fileName = absolutePath.split(/[/\\]+/).pop(); const folderPath = absolutePath.slice(0, absolutePath.replaceAll('\\', '/').lastIndexOf('/') + 1); - const groupInfo = [file.AniDB?.ReleaseGroup?.Name ?? 'Unknown']; - if (file.AniDB?.Source) groupInfo.push(file.AniDB.Source); - if (file.AniDB?.Version) groupInfo.push(`v${file.AniDB.Version}`); + const groupInfo = [file.ReleaseInfo?.Group?.Name ?? 'Unknown']; + if (file.ReleaseInfo?.Source) groupInfo.push(file.ReleaseInfo.Source); + if (file.ReleaseInfo?.Revision) groupInfo.push(`v${file.ReleaseInfo.Revision}`); return { Name: fileName ?? '', @@ -68,14 +68,14 @@ const useMediaInfo = (file: FileType): FileInfo => Size: file.Size ?? 0, Group: groupInfo.join(' | '), Hashes: { - ED2K: file.Hashes?.ED2K ?? '', - SHA1: file.Hashes?.SHA1 ?? '', - CRC32: file.Hashes?.CRC32 ?? '', - MD5: file.Hashes?.MD5 ?? '', + ED2K: file.Hashes?.find(hash => hash.Type === 'ED2K')?.Value ?? '', + SHA1: file.Hashes?.find(hash => hash.Type === 'SHA1')?.Value ?? '', + CRC32: file.Hashes?.find(hash => hash.Type === 'CRC32')?.Value ?? '', + MD5: file.Hashes?.find(hash => hash.Type === 'MD5')?.Value ?? '', }, VideoInfo: videoInfo, AudioInfo: audioInfo, - Chapters: file.AniDB?.Chaptered ?? false, + Chapters: file.ReleaseInfo?.IsChaptered ?? false, }; }, [file]); diff --git a/src/pages/dashboard/DashboardPage.tsx b/src/pages/dashboard/DashboardPage.tsx index f68065356..da675f1ee 100644 --- a/src/pages/dashboard/DashboardPage.tsx +++ b/src/pages/dashboard/DashboardPage.tsx @@ -17,7 +17,7 @@ import WelcomeModal from '@/pages/dashboard/components/WelcomeModal'; import CollectionStats from './panels/CollectionStats'; import ContinueWatching from './panels/ContinueWatching'; -import ImportFolders from './panels/ImportFolders'; +import ManagedFolders from './panels/ManagedFolders'; import MediaType from './panels/MediaType'; import NextUp from './panels/NextUp'; import QueueProcessor from './panels/QueueProcessor'; @@ -69,7 +69,7 @@ function DashboardPage() { combineContinueWatching, hideCollectionStats, hideContinueWatching, - hideImportFolders, + hideManagedFolders, hideMediaType, hideNextUp, hideQueueProcessor, @@ -189,9 +189,9 @@ function DashboardPage() {
)} - {!hideImportFolders && ( -
- + {!hideManagedFolders && ( +
+
)} {!hideShokoNews && ( diff --git a/src/pages/dashboard/panels/ImportFolders.tsx b/src/pages/dashboard/panels/ManagedFolders.tsx similarity index 66% rename from src/pages/dashboard/panels/ImportFolders.tsx rename to src/pages/dashboard/panels/ManagedFolders.tsx index 9dfc68a83..c2a522330 100644 --- a/src/pages/dashboard/panels/ImportFolders.tsx +++ b/src/pages/dashboard/panels/ManagedFolders.tsx @@ -7,12 +7,12 @@ import prettyBytes from 'pretty-bytes'; import Button from '@/components/Input/Button'; import ShokoPanel from '@/components/Panels/ShokoPanel'; import toast from '@/components/Toast'; -import { useRescanImportFolderMutation } from '@/core/react-query/import-folder/mutations'; -import { useImportFoldersQuery } from '@/core/react-query/import-folder/queries'; -import { setEdit, setStatus } from '@/core/slices/modals/importFolder'; +import { useRescanManagedFolderMutation } from '@/core/react-query/managed-folder/mutations'; +import { useManagedFoldersQuery } from '@/core/react-query/managed-folder/queries'; +import { setEdit, setStatus } from '@/core/slices/modals/managedFolder'; import type { RootState } from '@/core/store'; -import type { ImportFolderType } from '@/core/types/api/import-folder'; +import type { ManagedFolderType } from '@/core/types/api/managed-folder'; const Options = ({ onClick }: { onClick: () => void }) => ( ); -function ImportFolders() { +function ManagedFolders() { const dispatch = useDispatch(); const layoutEditMode = useSelector((state: RootState) => state.mainpage.layoutEditMode); - const { mutate: rescanImportFolder } = useRescanImportFolderMutation(); - const importFolderQuery = useImportFoldersQuery(); - const importFolders = importFolderQuery?.data ?? [] as ImportFolderType[]; + const { mutate: rescanManagedFolder } = useRescanManagedFolderMutation(); + const managedFolderQuery = useManagedFoldersQuery(); + const managedFolders = managedFolderQuery?.data ?? [] as ManagedFolderType[]; const rescanFolder = (ID: number, name: string) => { - rescanImportFolder(ID, { - onSuccess: () => toast.success('Scan Import Folder Success', `Import Folder ${name} queued for scanning.`), + rescanManagedFolder(ID, { + onSuccess: () => toast.success('Scan Managed Folder Success', `Managed Folder ${name} queued for scanning.`), }); }; - const setImportFolderModalStatus = (status: boolean) => dispatch(setStatus(status)); - const openImportFolderModalEdit = (ID: number) => dispatch(setEdit(ID)); + const setManagedFolderModalStatus = (status: boolean) => dispatch(setStatus(status)); + const openManagedFolderModalEdit = (ID: number) => dispatch(setEdit(ID)); - const renderFolder = (folder: ImportFolderType) => { + const renderFolder = (folder: ManagedFolderType) => { let flags = ''; if (folder.DropFolderType === 'Both') flags = 'Source, Destination'; @@ -63,7 +63,7 @@ function ImportFolders() { rotate={180} /> -
diff --git a/src/pages/firstrun/ImportFolders.tsx b/src/pages/firstrun/ManagedFolders.tsx similarity index 61% rename from src/pages/firstrun/ImportFolders.tsx rename to src/pages/firstrun/ManagedFolders.tsx index 05c20ca14..2f4bd53ca 100644 --- a/src/pages/firstrun/ImportFolders.tsx +++ b/src/pages/firstrun/ManagedFolders.tsx @@ -3,23 +3,20 @@ import { useDispatch } from 'react-redux'; import { mdiMinusCircleOutline, mdiPencilCircleOutline } from '@mdi/js'; import { Icon } from '@mdi/react'; -import ImportFolderModal from '@/components/Dialogs/ImportFolderModal'; +import ManagedFolderModal from '@/components/Dialogs/ManagedFolderModal'; import Button from '@/components/Input/Button'; import toast from '@/components/Toast'; import TransitionDiv from '@/components/TransitionDiv'; -import { useDeleteImportFolderMutation } from '@/core/react-query/import-folder/mutations'; -import { useImportFoldersQuery } from '@/core/react-query/import-folder/queries'; +import { useDeleteManagedFolderMutation } from '@/core/react-query/managed-folder/mutations'; +import { useManagedFoldersQuery } from '@/core/react-query/managed-folder/queries'; import { setSaved as setFirstRunSaved } from '@/core/slices/firstrun'; -import { - setEdit as setImportFolderModalEdit, - setStatus as setImportFolderModalStatus, -} from '@/core/slices/modals/importFolder'; +import { setEdit as setFolderModalEdit, setStatus as setFolderModalStatus } from '@/core/slices/modals/managedFolder'; import Footer from './Footer'; -import type { ImportFolderType } from '@/core/types/api/import-folder'; +import type { ManagedFolderType } from '@/core/types/api/managed-folder'; -const Folder = (props: ImportFolderType) => { +const Folder = (props: ManagedFolderType) => { const { DropFolderType, ID, @@ -29,11 +26,11 @@ const Folder = (props: ImportFolderType) => { } = props; const dispatch = useDispatch(); - const { mutate: deleteFolder } = useDeleteImportFolderMutation(); + const { mutate: deleteFolder } = useDeleteManagedFolderMutation(); const handleDeleteFolder = (folderId: number) => { deleteFolder({ folderId }, { - onSuccess: () => toast.success('Import folder deleted!'), + onSuccess: () => toast.success('Managed folder deleted!'), }); }; @@ -53,7 +50,7 @@ const Folder = (props: ImportFolderType) => {
{Name}
-
- {importFolders.length > 0 + {managedFolders.length > 0 ? ( <>
- Current Import Folders + Current Managed Folders
- {importFolders.map(folder => )} + {managedFolders.map(folder => )}
) - :
No Import Folders Added
} -