Add updateExisting option to importers & UI
Introduce an updateExisting flag across importers and the Importer UI to control whether existing items should be updated or only new items imported. Changes: added updateExisting to XBVR, StashAPP, Playnite, and Jellyfin config types; added checkboxes in ImporterView (enabled by default) to toggle the behavior; import logic now skips existing items when updateExisting is false and logs/skips appropriately (XBVR, StashAPP, Playnite, Jellyfin). Also: minor Playnite env parsing tweak for port (undefined when not provided) and small logging/cleanup in the Jellyfin album handling.
This commit is contained in:
@@ -13,15 +13,17 @@ const BASE_URL = import.meta.env.VITE_BASE_URL || 'http://localhost:3000';
|
|||||||
|
|
||||||
export default function ImporterView() {
|
export default function ImporterView() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [xbvrConfig, setXbvrConfig] = useState<XBVRConfig>({ url: import.meta.env.VITE_XBVR_URL || BASE_URL });
|
const [xbvrConfig, setXbvrConfig] = useState<XBVRConfig>({ url: import.meta.env.VITE_XBVR_URL || BASE_URL, updateExisting: true });
|
||||||
const [stashappConfig, setStashappConfig] = useState<StashAPPConfig>({
|
const [stashappConfig, setStashappConfig] = useState<StashAPPConfig>({
|
||||||
url: import.meta.env.VITE_STASHAPP_URL || '',
|
url: import.meta.env.VITE_STASHAPP_URL || '',
|
||||||
apiKey: import.meta.env.VITE_STASHAPP_API_KEY || ''
|
apiKey: import.meta.env.VITE_STASHAPP_API_KEY || '',
|
||||||
|
updateExisting: true
|
||||||
});
|
});
|
||||||
const [playniteConfig, setPlayniteConfig] = useState<PlayniteConfig>({
|
const [playniteConfig, setPlayniteConfig] = useState<PlayniteConfig>({
|
||||||
ip: import.meta.env.VITE_PLAYNITE_IP || '',
|
ip: import.meta.env.VITE_PLAYNITE_IP || '',
|
||||||
apiToken: import.meta.env.VITE_PLAYNITE_API_TOKEN || '',
|
apiToken: import.meta.env.VITE_PLAYNITE_API_TOKEN || '',
|
||||||
port: parseInt(import.meta.env.VITE_PLAYNITE_PORT || '19821')
|
port: import.meta.env.VITE_PLAYNITE_PORT ? parseInt(import.meta.env.VITE_PLAYNITE_PORT) : undefined,
|
||||||
|
updateExisting: true
|
||||||
});
|
});
|
||||||
const [jellyfinConfig, setJellyfinConfig] = useState<JellyfinConfig>({
|
const [jellyfinConfig, setJellyfinConfig] = useState<JellyfinConfig>({
|
||||||
url: import.meta.env.VITE_JELLYFIN_URL || '',
|
url: import.meta.env.VITE_JELLYFIN_URL || '',
|
||||||
@@ -33,7 +35,8 @@ export default function ImporterView() {
|
|||||||
importMusic: true,
|
importMusic: true,
|
||||||
importCast: true,
|
importCast: true,
|
||||||
limit: undefined,
|
limit: undefined,
|
||||||
libraryMappings: []
|
libraryMappings: [],
|
||||||
|
updateExisting: true
|
||||||
});
|
});
|
||||||
const [jellyfinLibraries, setJellyfinLibraries] = useState<Array<{ Id: string; Name: string; CollectionType: string }>>([]);
|
const [jellyfinLibraries, setJellyfinLibraries] = useState<Array<{ Id: string; Name: string; CollectionType: string }>>([]);
|
||||||
const [libraryMappings, setLibraryMappings] = useState<LibraryMapping[]>([]);
|
const [libraryMappings, setLibraryMappings] = useState<LibraryMapping[]>([]);
|
||||||
@@ -396,6 +399,17 @@ export default function ImporterView() {
|
|||||||
placeholder="http://192.168.1.102:10001"
|
placeholder="http://192.168.1.102:10001"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
id="xbvr-update-existing"
|
||||||
|
checked={xbvrConfig.updateExisting}
|
||||||
|
onChange={(e) => setXbvrConfig({ ...xbvrConfig, updateExisting: e.target.checked })}
|
||||||
|
disabled={progress.stage !== 'idle'}
|
||||||
|
className="rounded border-border"
|
||||||
|
/>
|
||||||
|
<label htmlFor="xbvr-update-existing" className="text-xs text-muted-foreground cursor-pointer">Bestehende aktualisieren</label>
|
||||||
|
</div>
|
||||||
<Button
|
<Button
|
||||||
onClick={handleXBVRImport}
|
onClick={handleXBVRImport}
|
||||||
disabled={progress.stage !== 'idle' || !xbvrConfig.url}
|
disabled={progress.stage !== 'idle' || !xbvrConfig.url}
|
||||||
@@ -465,6 +479,17 @@ export default function ImporterView() {
|
|||||||
placeholder="Enter API key if required"
|
placeholder="Enter API key if required"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
id="stashapp-update-existing"
|
||||||
|
checked={stashappConfig.updateExisting}
|
||||||
|
onChange={(e) => setStashappConfig({ ...stashappConfig, updateExisting: e.target.checked })}
|
||||||
|
disabled={progress.stage !== 'idle'}
|
||||||
|
className="rounded border-border"
|
||||||
|
/>
|
||||||
|
<label htmlFor="stashapp-update-existing" className="text-xs text-muted-foreground cursor-pointer">Bestehende aktualisieren</label>
|
||||||
|
</div>
|
||||||
<Button
|
<Button
|
||||||
onClick={handleStashAPPImport}
|
onClick={handleStashAPPImport}
|
||||||
disabled={progress.stage !== 'idle' || !stashappConfig.url}
|
disabled={progress.stage !== 'idle' || !stashappConfig.url}
|
||||||
@@ -603,6 +628,17 @@ export default function ImporterView() {
|
|||||||
placeholder="pb_your_token_here"
|
placeholder="pb_your_token_here"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
id="playnite-update-existing"
|
||||||
|
checked={playniteConfig.updateExisting}
|
||||||
|
onChange={(e) => setPlayniteConfig({ ...playniteConfig, updateExisting: e.target.checked })}
|
||||||
|
disabled={progress.stage !== 'idle'}
|
||||||
|
className="rounded border-border"
|
||||||
|
/>
|
||||||
|
<label htmlFor="playnite-update-existing" className="text-xs text-muted-foreground cursor-pointer">Bestehende aktualisieren</label>
|
||||||
|
</div>
|
||||||
<Button
|
<Button
|
||||||
onClick={handlePlayniteImport}
|
onClick={handlePlayniteImport}
|
||||||
disabled={progress.stage !== 'idle' || !playniteConfig.ip || !playniteConfig.apiToken}
|
disabled={progress.stage !== 'idle' || !playniteConfig.ip || !playniteConfig.apiToken}
|
||||||
@@ -728,6 +764,17 @@ export default function ImporterView() {
|
|||||||
placeholder="e.g. 10"
|
placeholder="e.g. 10"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
id="jellyfin-update-existing"
|
||||||
|
checked={jellyfinOptions.updateExisting}
|
||||||
|
onChange={(e) => setJellyfinOptions({ ...jellyfinOptions, updateExisting: e.target.checked })}
|
||||||
|
disabled={progress.stage !== 'idle'}
|
||||||
|
className="rounded border-border"
|
||||||
|
/>
|
||||||
|
<label htmlFor="jellyfin-update-existing" className="text-xs text-muted-foreground cursor-pointer">Bestehende aktualisieren</label>
|
||||||
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="text-xs font-bold text-muted-foreground mb-2 block">Library Category Mapping</label>
|
<label className="text-xs font-bold text-muted-foreground mb-2 block">Library Category Mapping</label>
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ export interface JellyfinImportOptions {
|
|||||||
importCast?: boolean;
|
importCast?: boolean;
|
||||||
limit?: number;
|
limit?: number;
|
||||||
libraryMappings?: LibraryMapping[];
|
libraryMappings?: LibraryMapping[];
|
||||||
|
updateExisting?: boolean; // If true, update existing items; if false, only import new items
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ImportProgress {
|
export interface ImportProgress {
|
||||||
@@ -830,6 +831,12 @@ export async function importFromJellyfin(
|
|||||||
const existing = existingMedia.get(movie.Name);
|
const existing = existingMedia.get(movie.Name);
|
||||||
const isUpdate = existing !== undefined;
|
const isUpdate = existing !== undefined;
|
||||||
|
|
||||||
|
// Skip if updateExisting is false and item already exists
|
||||||
|
if (!options.updateExisting && isUpdate) {
|
||||||
|
logCallback(`⊘ Skipped movie: ${movie.Name} (already exists, updateExisting is false)`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const mediaData = await convertJellyfinMovieToMedia(movie, config, libraryIdToName, options.libraryMappings);
|
const mediaData = await convertJellyfinMovieToMedia(movie, config, libraryIdToName, options.libraryMappings);
|
||||||
|
|
||||||
@@ -908,6 +915,12 @@ export async function importFromJellyfin(
|
|||||||
const existing = existingMedia.get(show.Name);
|
const existing = existingMedia.get(show.Name);
|
||||||
const isUpdate = existing !== undefined;
|
const isUpdate = existing !== undefined;
|
||||||
|
|
||||||
|
// Skip if updateExisting is false and item already exists
|
||||||
|
if (!options.updateExisting && isUpdate) {
|
||||||
|
logCallback(`⊘ Skipped series: ${show.Name} (already exists, updateExisting is false)`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const mediaData = await convertJellyfinSeriesToMedia(show, config, libraryIdToName, options.libraryMappings);
|
const mediaData = await convertJellyfinSeriesToMedia(show, config, libraryIdToName, options.libraryMappings);
|
||||||
|
|
||||||
@@ -986,6 +999,12 @@ export async function importFromJellyfin(
|
|||||||
const existing = existingMedia.get(album.Name);
|
const existing = existingMedia.get(album.Name);
|
||||||
const isUpdate = existing !== undefined;
|
const isUpdate = existing !== undefined;
|
||||||
|
|
||||||
|
// Skip if updateExisting is false and item already exists
|
||||||
|
if (!options.updateExisting && isUpdate) {
|
||||||
|
logCallback(`⊘ Skipped album: ${album.Name} (already exists, updateExisting is false)`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const mediaData = await convertJellyfinAlbumToMedia(album, config, libraryIdToName, options.libraryMappings);
|
const mediaData = await convertJellyfinAlbumToMedia(album, config, libraryIdToName, options.libraryMappings);
|
||||||
|
|
||||||
@@ -1014,19 +1033,11 @@ export async function importFromJellyfin(
|
|||||||
musicImported++;
|
musicImported++;
|
||||||
logCallback(`✓ ${isUpdate ? 'Updated' : 'Imported'} album: ${album.Name}`);
|
logCallback(`✓ ${isUpdate ? 'Updated' : 'Imported'} album: ${album.Name}`);
|
||||||
} else {
|
} else {
|
||||||
const error = await response.text();
|
|
||||||
progress.errors.push(`Failed to ${isUpdate ? 'update' : 'import'} album ${album.Name}: ${error}`);
|
|
||||||
logCallback(`✗ Failed to ${isUpdate ? 'update' : 'import'} album: ${album.Name}`);
|
logCallback(`✗ Failed to ${isUpdate ? 'update' : 'import'} album: ${album.Name}`);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
progress.errors.push(`Error processing album ${album.Name}: ${error}`);
|
logCallback(`✗ Error processing album ${album.Name}: ${error}`);
|
||||||
logCallback(`✗ Error processing album: ${album.Name}`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
progressCallback({
|
|
||||||
musicImported,
|
|
||||||
errors: progress.errors
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
logCallback(`Processed ${musicImported}/${albums.length} albums`);
|
logCallback(`Processed ${musicImported}/${albums.length} albums`);
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ export interface PlayniteConfig {
|
|||||||
ip: string;
|
ip: string;
|
||||||
apiToken: string;
|
apiToken: string;
|
||||||
port?: number;
|
port?: number;
|
||||||
|
updateExisting?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ImportProgress {
|
export interface ImportProgress {
|
||||||
@@ -194,6 +195,15 @@ export async function importFromPlaynite(
|
|||||||
const existingGame = existingMedia.get(game.name);
|
const existingGame = existingMedia.get(game.name);
|
||||||
const isUpdate = existingGame !== undefined;
|
const isUpdate = existingGame !== undefined;
|
||||||
|
|
||||||
|
// Skip if updateExisting is false and item already exists
|
||||||
|
if (!config.updateExisting && isUpdate) {
|
||||||
|
logCallback(`⊘ Skipped game: ${game.name} (already exists, updateExisting is false)`);
|
||||||
|
progressCallback({
|
||||||
|
current: i + 1
|
||||||
|
});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Parse release date
|
// Parse release date
|
||||||
let year = new Date().getFullYear();
|
let year = new Date().getFullYear();
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ export interface StashAPPConfig {
|
|||||||
url: string;
|
url: string;
|
||||||
apiKey?: string;
|
apiKey?: string;
|
||||||
blacklist?: ['/AI/', 'temp', 'backup'];
|
blacklist?: ['/AI/', 'temp', 'backup'];
|
||||||
|
updateExisting?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ImportProgress {
|
export interface ImportProgress {
|
||||||
@@ -645,12 +646,15 @@ export async function importFromStashAPP(
|
|||||||
|
|
||||||
// Check for duplicate
|
// Check for duplicate
|
||||||
if (existingTitles.has(scene.title)) {
|
if (existingTitles.has(scene.title)) {
|
||||||
logCallback(`⊘ Skipped duplicate: ${scene.title}`);
|
if (!config.updateExisting) {
|
||||||
|
logCallback(`⊘ Skipped duplicate: ${scene.title} (updateExisting is false)`);
|
||||||
progressCallback({
|
progressCallback({
|
||||||
current: uniquePerformers.length + i + 1
|
current: uniquePerformers.length + i + 1
|
||||||
});
|
});
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
logCallback(`→ Updating existing: ${scene.title}`);
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Extract performers as staff
|
// Extract performers as staff
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { SOURCE_CATEGORY_MAPPING } from '@/types';
|
|||||||
export interface XBVRConfig {
|
export interface XBVRConfig {
|
||||||
url: string;
|
url: string;
|
||||||
apiKey?: string;
|
apiKey?: string;
|
||||||
|
updateExisting?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ImportProgress {
|
export interface ImportProgress {
|
||||||
@@ -258,12 +259,15 @@ export async function importFromXBVR(
|
|||||||
|
|
||||||
// Check for duplicate
|
// Check for duplicate
|
||||||
if (existingTitles.has(video.title)) {
|
if (existingTitles.has(video.title)) {
|
||||||
logCallback(`⊘ Skipped duplicate: ${video.title}`);
|
if (!config.updateExisting) {
|
||||||
|
logCallback(`⊘ Skipped duplicate: ${video.title} (updateExisting is false)`);
|
||||||
progressCallback({
|
progressCallback({
|
||||||
current: uniqueActors.length + i + 1
|
current: uniqueActors.length + i + 1
|
||||||
});
|
});
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
logCallback(`→ Updating existing: ${video.title}`);
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Extract categories/tags
|
// Extract categories/tags
|
||||||
|
|||||||
Reference in New Issue
Block a user