mirror of
https://github.com/Dictionarry-Hub/profilarr.git
synced 2026-01-22 10:51:02 +01:00
feature: Include Format when Renaming (#143)
- New: Field inside format general tab to enable include format in rename - New: Database migration that adds format renames table - New: Queries to get / update rename status for a format - Update: Format compiler checks for rename entries and add include rename field when found - Update: Parsing improvements for incoming commit messages
This commit is contained in:
@@ -6,6 +6,7 @@ from .utils import (get_category_directory, load_yaml_file, validate,
|
||||
save_yaml_file, update_yaml_file, get_file_modified_date,
|
||||
test_regex_pattern, test_format_conditions,
|
||||
check_delete_constraints, filename_to_display)
|
||||
from ..db import add_format_to_renames, remove_format_from_renames, is_format_in_renames
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logger.setLevel(logging.INFO)
|
||||
@@ -28,6 +29,12 @@ def retrieve_all(category):
|
||||
file_path = os.path.join(directory, file_name)
|
||||
try:
|
||||
content = load_yaml_file(file_path)
|
||||
# Add metadata for custom formats
|
||||
if category == 'custom_format':
|
||||
content['metadata'] = {
|
||||
'includeInRename':
|
||||
is_format_in_renames(content['name'])
|
||||
}
|
||||
result.append({
|
||||
"file_name":
|
||||
file_name,
|
||||
@@ -69,6 +76,12 @@ def handle_item(category, name):
|
||||
if request.method == 'GET':
|
||||
try:
|
||||
content = load_yaml_file(file_path)
|
||||
# Add metadata for custom formats
|
||||
if category == 'custom_format':
|
||||
content['metadata'] = {
|
||||
'includeInRename':
|
||||
is_format_in_renames(content['name'])
|
||||
}
|
||||
return jsonify({
|
||||
"file_name":
|
||||
file_name,
|
||||
@@ -97,6 +110,22 @@ def handle_item(category, name):
|
||||
return jsonify({"error": error_message}), 409
|
||||
|
||||
try:
|
||||
# If it's a custom format, remove from renames table first
|
||||
if category == 'custom_format':
|
||||
# Get the format name from the file before deleting it
|
||||
content = load_yaml_file(file_path)
|
||||
format_name = content.get('name')
|
||||
if format_name:
|
||||
# Check if it exists in renames before trying to remove
|
||||
if is_format_in_renames(format_name):
|
||||
remove_format_from_renames(format_name)
|
||||
logger.info(
|
||||
f"Removed {format_name} from renames table")
|
||||
else:
|
||||
logger.info(
|
||||
f"{format_name} was not in renames table")
|
||||
|
||||
# Then delete the file
|
||||
os.remove(file_path)
|
||||
return jsonify(
|
||||
{"message": f"Successfully deleted {file_name}"}), 200
|
||||
@@ -116,11 +145,27 @@ def handle_item(category, name):
|
||||
if data and 'name' in data:
|
||||
data['name'] = data['name'].strip()
|
||||
|
||||
# Handle rename inclusion for custom formats
|
||||
if category == 'custom_format':
|
||||
include_in_rename = data.get('metadata', {}).get(
|
||||
'includeInRename', False)
|
||||
# Remove metadata before saving YAML
|
||||
if 'metadata' in data:
|
||||
del data['metadata']
|
||||
|
||||
if validate(data, category):
|
||||
# Save YAML
|
||||
save_yaml_file(file_path, data, category)
|
||||
|
||||
# If custom format, handle rename table
|
||||
if category == 'custom_format' and include_in_rename:
|
||||
add_format_to_renames(data['name'])
|
||||
|
||||
return jsonify(
|
||||
{"message": f"Successfully created {file_name}"}), 201
|
||||
|
||||
return jsonify({"error": "Validation failed"}), 400
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error creating file: {e}")
|
||||
return jsonify({"error": str(e)}), 500
|
||||
@@ -131,15 +176,44 @@ def handle_item(category, name):
|
||||
|
||||
try:
|
||||
data = request.get_json()
|
||||
logger.info(f"Received PUT data for {name}: {data}")
|
||||
|
||||
if data and 'name' in data:
|
||||
data['name'] = data['name'].strip()
|
||||
if data and 'rename' in data:
|
||||
data['rename'] = data['rename'].strip()
|
||||
|
||||
# Handle rename inclusion for custom formats
|
||||
if category == 'custom_format':
|
||||
include_in_rename = data.get('metadata', {}).get(
|
||||
'includeInRename', False)
|
||||
|
||||
# Get current content to check for rename
|
||||
current_content = load_yaml_file(file_path)
|
||||
old_name = current_content.get('name')
|
||||
new_name = data['name']
|
||||
|
||||
# Handle renames and toggles
|
||||
if old_name != new_name and include_in_rename:
|
||||
# Handle rename while keeping in table
|
||||
remove_format_from_renames(old_name)
|
||||
add_format_to_renames(new_name)
|
||||
elif include_in_rename:
|
||||
# Just turning it on
|
||||
add_format_to_renames(new_name)
|
||||
else:
|
||||
# Turning it off
|
||||
remove_format_from_renames(data['name'])
|
||||
|
||||
# Remove metadata before saving YAML
|
||||
if 'metadata' in data:
|
||||
del data['metadata']
|
||||
|
||||
# Save YAML
|
||||
update_yaml_file(file_path, data, category)
|
||||
return jsonify(
|
||||
{"message": f"Successfully updated {file_name}"}), 200
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error updating file: {e}")
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
@@ -2,9 +2,11 @@
|
||||
from .connection import get_db
|
||||
from .queries.settings import get_settings, get_secret_key, save_settings
|
||||
from .queries.arr import get_unique_arrs
|
||||
from .queries.format_renames import add_format_to_renames, remove_format_from_renames, is_format_in_renames
|
||||
from .migrations.runner import run_migrations
|
||||
|
||||
__all__ = [
|
||||
'get_db', 'get_settings', 'get_secret_key', 'save_settings',
|
||||
'get_unique_arrs', 'run_migrations'
|
||||
'get_unique_arrs', 'run_migrations', 'add_format_to_renames',
|
||||
'remove_format_from_renames', 'is_format_in_renames'
|
||||
]
|
||||
|
||||
23
backend/app/db/migrations/versions/002_format_renames.py
Normal file
23
backend/app/db/migrations/versions/002_format_renames.py
Normal file
@@ -0,0 +1,23 @@
|
||||
# backend/app/db/migrations/versions/002_format_renames.py
|
||||
from ...connection import get_db
|
||||
|
||||
version = 2
|
||||
name = "format_renames"
|
||||
|
||||
|
||||
def up():
|
||||
"""Add table for tracking which formats to include in renames"""
|
||||
with get_db() as conn:
|
||||
conn.execute('''
|
||||
CREATE TABLE IF NOT EXISTS format_renames (
|
||||
format_name TEXT PRIMARY KEY NOT NULL
|
||||
)
|
||||
''')
|
||||
conn.commit()
|
||||
|
||||
|
||||
def down():
|
||||
"""Remove the format_renames table"""
|
||||
with get_db() as conn:
|
||||
conn.execute('DROP TABLE IF EXISTS format_renames')
|
||||
conn.commit()
|
||||
33
backend/app/db/queries/format_renames.py
Normal file
33
backend/app/db/queries/format_renames.py
Normal file
@@ -0,0 +1,33 @@
|
||||
# backend/app/db/queries/format_renames.py
|
||||
import logging
|
||||
from ..connection import get_db
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def add_format_to_renames(format_name: str) -> None:
|
||||
"""Add a format to the renames table"""
|
||||
with get_db() as conn:
|
||||
conn.execute(
|
||||
'INSERT OR REPLACE INTO format_renames (format_name) VALUES (?)',
|
||||
(format_name, ))
|
||||
conn.commit()
|
||||
logger.info(f"Added format to renames table: {format_name}")
|
||||
|
||||
|
||||
def remove_format_from_renames(format_name: str) -> None:
|
||||
"""Remove a format from the renames table"""
|
||||
with get_db() as conn:
|
||||
conn.execute('DELETE FROM format_renames WHERE format_name = ?',
|
||||
(format_name, ))
|
||||
conn.commit()
|
||||
logger.info(f"Removed format from renames table: {format_name}")
|
||||
|
||||
|
||||
def is_format_in_renames(format_name: str) -> bool:
|
||||
"""Check if a format is in the renames table"""
|
||||
with get_db() as conn:
|
||||
result = conn.execute(
|
||||
'SELECT 1 FROM format_renames WHERE format_name = ?',
|
||||
(format_name, )).fetchone()
|
||||
return bool(result)
|
||||
@@ -6,6 +6,7 @@ from pathlib import Path
|
||||
from ..data.utils import (load_yaml_file, get_category_directory, REGEX_DIR,
|
||||
FORMAT_DIR)
|
||||
from ..compile import CustomFormat, FormatConverter, TargetApp
|
||||
from ..db.queries.format_renames import is_format_in_renames
|
||||
|
||||
logger = logging.getLogger('importarr')
|
||||
|
||||
@@ -63,13 +64,21 @@ def import_formats_to_arr(format_names, base_url, api_key, arr_type,
|
||||
if not converted_format:
|
||||
raise ValueError("Format conversion failed")
|
||||
|
||||
# Use the potentially modified name (with [Dictionarry]) for arr
|
||||
compiled_data = {
|
||||
'name':
|
||||
format_name, # Use the possibly modified name
|
||||
'specifications':
|
||||
[vars(spec) for spec in converted_format.specifications]
|
||||
}
|
||||
# Create base compiled data with ordered fields
|
||||
compiled_data = {'name': format_name} # Start with name
|
||||
|
||||
# Check rename status and add field right after name if true
|
||||
if is_format_in_renames(original_name):
|
||||
compiled_data['includeCustomFormatWhenRenaming'] = True
|
||||
logger.info(
|
||||
f"Format {original_name} has renames enabled, including field"
|
||||
)
|
||||
|
||||
# Add specifications last
|
||||
compiled_data['specifications'] = [
|
||||
vars(spec) for spec in converted_format.specifications
|
||||
]
|
||||
|
||||
logger.info("Compiled to:\n" +
|
||||
json.dumps([compiled_data], indent=2))
|
||||
|
||||
|
||||
@@ -8,10 +8,12 @@ const FormatGeneralTab = ({
|
||||
description,
|
||||
tags,
|
||||
error,
|
||||
includeInRename,
|
||||
onNameChange,
|
||||
onDescriptionChange,
|
||||
onAddTag,
|
||||
onRemoveTag
|
||||
onRemoveTag,
|
||||
onIncludeInRenameChange
|
||||
}) => {
|
||||
const [newTag, setNewTag] = useState('');
|
||||
|
||||
@@ -41,13 +43,36 @@ const FormatGeneralTab = ({
|
||||
<div className='space-y-8'>
|
||||
{/* Name Input */}
|
||||
<div className='space-y-2'>
|
||||
<div className='space-y-1'>
|
||||
<label className='text-sm font-medium text-gray-700 dark:text-gray-300'>
|
||||
Format Name
|
||||
</label>
|
||||
<p className='text-xs text-gray-500 dark:text-gray-400'>
|
||||
Give your format a descriptive name
|
||||
</p>
|
||||
<div className='flex justify-between items-start'>
|
||||
<div className='space-y-1'>
|
||||
<label className='text-sm font-medium text-gray-700 dark:text-gray-300'>
|
||||
Format Name
|
||||
</label>
|
||||
<p className='text-xs text-gray-500 dark:text-gray-400'>
|
||||
Give your format a descriptive name
|
||||
</p>
|
||||
</div>
|
||||
<div className='flex flex-col items-end space-y-1'>
|
||||
<label className='flex items-center space-x-2 text-sm font-medium text-gray-700 dark:text-gray-300 cursor-pointer'>
|
||||
<input
|
||||
type='checkbox'
|
||||
checked={includeInRename}
|
||||
onChange={e =>
|
||||
onIncludeInRenameChange(
|
||||
e.target.checked
|
||||
)
|
||||
}
|
||||
className='rounded border-gray-300 dark:border-gray-600
|
||||
text-blue-500 focus:ring-blue-500
|
||||
h-4 w-4 cursor-pointer
|
||||
transition-colors duration-200'
|
||||
/>
|
||||
<span>Include Custom Format When Renaming</span>
|
||||
</label>
|
||||
<p className='text-xs text-gray-500 dark:text-gray-400'>
|
||||
Include this format's name in renamed files
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<input
|
||||
type='text'
|
||||
@@ -166,10 +191,12 @@ FormatGeneralTab.propTypes = {
|
||||
description: PropTypes.string.isRequired,
|
||||
tags: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||
error: PropTypes.string,
|
||||
includeInRename: PropTypes.bool.isRequired,
|
||||
onNameChange: PropTypes.func.isRequired,
|
||||
onDescriptionChange: PropTypes.func.isRequired,
|
||||
onAddTag: PropTypes.func.isRequired,
|
||||
onRemoveTag: PropTypes.func.isRequired
|
||||
onRemoveTag: PropTypes.func.isRequired,
|
||||
onIncludeInRenameChange: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default FormatGeneralTab;
|
||||
|
||||
@@ -27,7 +27,9 @@ const FormatModal = ({
|
||||
onConditionsChange,
|
||||
onTestsChange,
|
||||
onActiveTabChange,
|
||||
onRunTests
|
||||
onRunTests,
|
||||
includeInRename,
|
||||
onIncludeInRenameChange
|
||||
}) => {
|
||||
const tabs = [
|
||||
{id: 'general', label: 'General'},
|
||||
@@ -95,12 +97,14 @@ const FormatModal = ({
|
||||
description={description}
|
||||
tags={tags}
|
||||
error={error}
|
||||
includeInRename={includeInRename}
|
||||
onNameChange={onNameChange}
|
||||
onDescriptionChange={onDescriptionChange}
|
||||
onAddTag={tag => onTagsChange([...tags, tag])}
|
||||
onRemoveTag={tag =>
|
||||
onTagsChange(tags.filter(t => t !== tag))
|
||||
}
|
||||
onIncludeInRenameChange={onIncludeInRenameChange}
|
||||
/>
|
||||
)}
|
||||
{activeTab === 'conditions' && (
|
||||
@@ -145,7 +149,9 @@ FormatModal.propTypes = {
|
||||
onConditionsChange: PropTypes.func.isRequired,
|
||||
onTestsChange: PropTypes.func.isRequired,
|
||||
onActiveTabChange: PropTypes.func.isRequired,
|
||||
onRunTests: PropTypes.func.isRequired
|
||||
onRunTests: PropTypes.func.isRequired,
|
||||
includeInRename: PropTypes.bool.isRequired,
|
||||
onIncludeInRenameChange: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default FormatModal;
|
||||
|
||||
@@ -118,6 +118,7 @@ function FormatPage() {
|
||||
activeTab,
|
||||
isDeleting,
|
||||
isRunningTests,
|
||||
includeInRename,
|
||||
setName,
|
||||
setDescription,
|
||||
setTags,
|
||||
@@ -125,6 +126,7 @@ function FormatPage() {
|
||||
setTests,
|
||||
setActiveTab,
|
||||
setIsDeleting,
|
||||
setIncludeInRename,
|
||||
initializeForm,
|
||||
handleSave,
|
||||
handleRunTests,
|
||||
@@ -408,6 +410,8 @@ function FormatPage() {
|
||||
onRunTests={handleRunTests}
|
||||
onSave={handleSave}
|
||||
onDelete={handleDelete}
|
||||
includeInRename={includeInRename}
|
||||
onIncludeInRenameChange={setIncludeInRename}
|
||||
/>
|
||||
|
||||
<ImportModal
|
||||
|
||||
@@ -1,11 +1,28 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {GitCommit, Info} from 'lucide-react';
|
||||
import Tooltip from '@ui/Tooltip';
|
||||
|
||||
const DiffCommit = ({commitMessage}) => {
|
||||
const {subject, body} = commitMessage;
|
||||
|
||||
const renderLine = (line, index) => {
|
||||
// Just handle basic bullet points (* or -)
|
||||
if (line.startsWith('* ') || line.startsWith('- ')) {
|
||||
return (
|
||||
<div key={index} className='flex items-center py-0.5'>
|
||||
<span className='mr-2 h-1 w-1 flex-shrink-0 rounded-full bg-gray-400' />
|
||||
<span>{line.slice(2)}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div key={index} className='py-0.5'>
|
||||
{line}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className='overflow-hidden rounded-lg border border-gray-700'>
|
||||
<table className='w-full'>
|
||||
@@ -31,18 +48,12 @@ const DiffCommit = ({commitMessage}) => {
|
||||
{body && (
|
||||
<tr className='bg-gray-900'>
|
||||
<td className='py-3 px-4'>
|
||||
<div className='text-gray-300 text-sm whitespace-pre-wrap'>
|
||||
{body.split('\n').map((line, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className={`${
|
||||
line.startsWith('- ')
|
||||
? 'ml-4'
|
||||
: ''
|
||||
}`}>
|
||||
{line}
|
||||
</div>
|
||||
))}
|
||||
<div className='text-gray-300 text-sm'>
|
||||
{body
|
||||
.split('\n')
|
||||
.map((line, index) =>
|
||||
renderLine(line, index)
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -53,11 +64,4 @@ const DiffCommit = ({commitMessage}) => {
|
||||
);
|
||||
};
|
||||
|
||||
DiffCommit.propTypes = {
|
||||
commitMessage: PropTypes.shape({
|
||||
subject: PropTypes.string.isRequired,
|
||||
body: PropTypes.string
|
||||
})
|
||||
};
|
||||
|
||||
export default DiffCommit;
|
||||
|
||||
@@ -12,6 +12,7 @@ export const useFormatModal = (initialFormat, onSuccess) => {
|
||||
const [conditions, setConditions] = useState([]);
|
||||
const [tests, setTests] = useState([]);
|
||||
const [isCloning, setIsCloning] = useState(false);
|
||||
const [includeInRename, setIncludeInRename] = useState(false);
|
||||
|
||||
// Enhanced UI state with field-specific errors
|
||||
const [formErrors, setFormErrors] = useState({
|
||||
@@ -78,6 +79,7 @@ export const useFormatModal = (initialFormat, onSuccess) => {
|
||||
setTags([]);
|
||||
setConditions([]);
|
||||
setTests([]);
|
||||
setIncludeInRename(false);
|
||||
setFormErrors({name: '', conditions: '', tests: '', general: ''});
|
||||
setIsDeleting(false);
|
||||
setIsCloning(false);
|
||||
@@ -92,6 +94,7 @@ export const useFormatModal = (initialFormat, onSuccess) => {
|
||||
setTags(format.tags || []);
|
||||
setConditions(format.conditions || []);
|
||||
setTests(format.tests || []);
|
||||
setIncludeInRename(format.metadata?.includeInRename || false);
|
||||
setIsCloning(cloning || false);
|
||||
}, 0);
|
||||
}
|
||||
@@ -181,7 +184,10 @@ export const useFormatModal = (initialFormat, onSuccess) => {
|
||||
description,
|
||||
tags,
|
||||
conditions,
|
||||
tests
|
||||
tests,
|
||||
metadata: {
|
||||
includeInRename
|
||||
}
|
||||
};
|
||||
|
||||
if (initialFormat && !isCloning) {
|
||||
@@ -284,6 +290,7 @@ export const useFormatModal = (initialFormat, onSuccess) => {
|
||||
tags,
|
||||
conditions,
|
||||
tests,
|
||||
includeInRename,
|
||||
// UI state
|
||||
formErrors,
|
||||
activeTab,
|
||||
@@ -298,6 +305,7 @@ export const useFormatModal = (initialFormat, onSuccess) => {
|
||||
setTests,
|
||||
setActiveTab,
|
||||
setIsDeleting,
|
||||
setIncludeInRename,
|
||||
// Main handlers
|
||||
initializeForm,
|
||||
handleSave,
|
||||
|
||||
Reference in New Issue
Block a user