feat: Implement Auto-Tagging Settings and MusicBrainz integration
- Added AutoTaggingSettings component for configuring auto-tagging preferences. - Integrated localStorage for saving user preferences and options. - Developed useAutoTagging hook for fetching and applying metadata from MusicBrainz. - Created MusicBrainz API client for searching and retrieving music metadata. - Enhanced metadata structure with additional fields for tracks and albums. - Implemented rate-limiting for MusicBrainz API requests. - Added UI components for user interaction and feedback during the tagging process.
This commit is contained in:
221
app/components/AutoTaggingSettings.tsx
Normal file
221
app/components/AutoTaggingSettings.tsx
Normal file
@@ -0,0 +1,221 @@
|
||||
'use client';
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Switch } from '@/components/ui/switch';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { FaTags } from 'react-icons/fa';
|
||||
import { useToast } from '@/hooks/use-toast';
|
||||
import { AutoTaggingDialog } from './AutoTaggingDialog';
|
||||
|
||||
export const AutoTaggingSettings = () => {
|
||||
const { toast } = useToast();
|
||||
const [isClient, setIsClient] = useState(false);
|
||||
const [autoTaggingEnabled, setAutoTaggingEnabled] = useState(false);
|
||||
const [autoTagDialogOpen, setAutoTagDialogOpen] = useState(false);
|
||||
const [selectedItem, setSelectedItem] = useState({
|
||||
id: '',
|
||||
name: 'Library',
|
||||
mode: 'artist' as 'track' | 'album' | 'artist'
|
||||
});
|
||||
const [autoTagOptions, setAutoTagOptions] = useState({
|
||||
rateLimit: 1000, // milliseconds between requests
|
||||
autoProcess: false,
|
||||
preferLocalMetadata: true,
|
||||
tagsToUpdate: ['title', 'artist', 'album', 'year', 'genre'],
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
setIsClient(true);
|
||||
|
||||
// Load saved preferences from localStorage
|
||||
const savedAutoTagging = localStorage.getItem('auto-tagging-enabled');
|
||||
if (savedAutoTagging !== null) {
|
||||
setAutoTaggingEnabled(savedAutoTagging === 'true');
|
||||
}
|
||||
|
||||
// Load saved auto-tag options
|
||||
const savedOptions = localStorage.getItem('auto-tagging-options');
|
||||
if (savedOptions !== null) {
|
||||
try {
|
||||
setAutoTagOptions(JSON.parse(savedOptions));
|
||||
} catch (error) {
|
||||
console.error('Failed to parse stored auto-tagging options:', error);
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleAutoTaggingToggle = (enabled: boolean) => {
|
||||
setAutoTaggingEnabled(enabled);
|
||||
if (isClient) {
|
||||
localStorage.setItem('auto-tagging-enabled', enabled.toString());
|
||||
}
|
||||
toast({
|
||||
title: enabled ? 'Auto-Tagging Enabled' : 'Auto-Tagging Disabled',
|
||||
description: enabled
|
||||
? 'Music will be automatically tagged with metadata from MusicBrainz'
|
||||
: 'Auto-tagging has been disabled',
|
||||
});
|
||||
};
|
||||
|
||||
const handleOptionsChange = (key: string, value: unknown) => {
|
||||
setAutoTagOptions(prev => {
|
||||
const newOptions = { ...prev, [key]: value };
|
||||
if (isClient) {
|
||||
localStorage.setItem('auto-tagging-options', JSON.stringify(newOptions));
|
||||
}
|
||||
return newOptions;
|
||||
});
|
||||
};
|
||||
|
||||
const handleTagSelectionChange = (tag: string, isSelected: boolean) => {
|
||||
setAutoTagOptions(prev => {
|
||||
const currentTags = [...prev.tagsToUpdate];
|
||||
const newTags = isSelected
|
||||
? [...currentTags, tag]
|
||||
: currentTags.filter(t => t !== tag);
|
||||
|
||||
const newOptions = { ...prev, tagsToUpdate: newTags };
|
||||
if (isClient) {
|
||||
localStorage.setItem('auto-tagging-options', JSON.stringify(newOptions));
|
||||
}
|
||||
return newOptions;
|
||||
});
|
||||
};
|
||||
|
||||
const isTagSelected = (tag: string) => {
|
||||
return autoTagOptions.tagsToUpdate.includes(tag);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Card className="mb-6 break-inside-avoid py-5">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<FaTags className="w-5 h-5" />
|
||||
Auto-Tagging
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Configure metadata auto-tagging with MusicBrainz
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="font-medium">Enable Auto-Tagging</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Automatically fetch and apply metadata from MusicBrainz
|
||||
</p>
|
||||
</div>
|
||||
<Switch
|
||||
checked={autoTaggingEnabled}
|
||||
onCheckedChange={handleAutoTaggingToggle}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{autoTaggingEnabled && (
|
||||
<>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="rate-limit">API Rate Limit (ms)</Label>
|
||||
<Input
|
||||
id="rate-limit"
|
||||
type="number"
|
||||
min={500}
|
||||
max={5000}
|
||||
step={100}
|
||||
value={autoTagOptions.rateLimit}
|
||||
onChange={(e) => handleOptionsChange('rateLimit', Number(e.target.value))}
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Time between API requests in milliseconds (min: 500ms)
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="font-medium">Auto Process Results</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Automatically apply best matches without confirmation
|
||||
</p>
|
||||
</div>
|
||||
<Switch
|
||||
checked={autoTagOptions.autoProcess}
|
||||
onCheckedChange={(checked) => handleOptionsChange('autoProcess', checked)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="font-medium">Prefer Local Metadata</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Keep existing metadata when confidence is low
|
||||
</p>
|
||||
</div>
|
||||
<Switch
|
||||
checked={autoTagOptions.preferLocalMetadata}
|
||||
onCheckedChange={(checked) => handleOptionsChange('preferLocalMetadata', checked)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label>Tags to Update</Label>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
{['title', 'artist', 'album', 'year', 'genre', 'albumArtist', 'trackNumber', 'discNumber'].map(tag => (
|
||||
<div key={tag} className="flex items-center space-x-2">
|
||||
<Switch
|
||||
id={`tag-${tag}`}
|
||||
checked={isTagSelected(tag)}
|
||||
onCheckedChange={(checked) => handleTagSelectionChange(tag, checked)}
|
||||
/>
|
||||
<Label htmlFor={`tag-${tag}`} className="capitalize">
|
||||
{tag === 'albumArtist' ? 'Album Artist' :
|
||||
tag === 'trackNumber' ? 'Track Number' :
|
||||
tag === 'discNumber' ? 'Disc Number' : tag}
|
||||
</Label>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="pt-2">
|
||||
<Button onClick={() => {
|
||||
// Set selected item to represent the whole library
|
||||
setSelectedItem({
|
||||
id: 'library',
|
||||
name: 'Full Library',
|
||||
mode: 'artist'
|
||||
});
|
||||
setAutoTagDialogOpen(true);
|
||||
}} variant="outline">
|
||||
<FaTags className="w-4 h-4 mr-2" />
|
||||
Open Auto-Tagging Tool
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
<div className="text-sm text-muted-foreground space-y-2">
|
||||
<p><strong>How it works:</strong></p>
|
||||
<ul className="list-disc list-inside space-y-1 ml-2">
|
||||
<li>Metadata is fetched from MusicBrainz when you play tracks</li>
|
||||
<li>Tags can be applied automatically or manually reviewed</li>
|
||||
<li>Right-click on tracks or albums to tag them manually</li>
|
||||
<li>MusicBrainz API has rate limits, so don't set too fast</li>
|
||||
</ul>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<AutoTaggingDialog
|
||||
isOpen={autoTagDialogOpen}
|
||||
onClose={() => setAutoTagDialogOpen(false)}
|
||||
mode={selectedItem.mode}
|
||||
itemId={selectedItem.id}
|
||||
itemName={selectedItem.name}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user