df
Some checks failed
smoke-mm / Install & smoke test mm --help (push) Has been cancelled
Some checks failed
smoke-mm / Install & smoke test mm --help (push) Has been cancelled
This commit is contained in:
@@ -23,16 +23,21 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
class ExportModal(ModalScreen):
|
||||
"""Modal screen for exporting files with metadata and tags."""
|
||||
|
||||
|
||||
BINDINGS = [
|
||||
Binding("escape", "cancel", "Cancel"),
|
||||
]
|
||||
|
||||
|
||||
CSS_PATH = "export.tcss"
|
||||
|
||||
def __init__(self, result_data: Optional[dict] = None, hydrus_available: bool = False, debrid_available: bool = False):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
result_data: Optional[dict] = None,
|
||||
hydrus_available: bool = False,
|
||||
debrid_available: bool = False,
|
||||
):
|
||||
"""Initialize the export modal with result data.
|
||||
|
||||
|
||||
Args:
|
||||
result_data: Dictionary containing:
|
||||
- title: str - Item title
|
||||
@@ -57,22 +62,22 @@ class ExportModal(ModalScreen):
|
||||
self.file_ext: Optional[str] = None # Store the file extension for format filtering
|
||||
self.file_type: Optional[str] = None # Store the file type (audio, video, image, document)
|
||||
self.default_format: Optional[str] = None # Store the default format to set after mount
|
||||
|
||||
|
||||
def _determine_file_type(self, ext: str) -> tuple[str, list]:
|
||||
"""Determine file type from extension and return type and format options.
|
||||
|
||||
|
||||
Args:
|
||||
ext: File extension (e.g., '.mp3', '.mp4', '.jpg')
|
||||
|
||||
|
||||
Returns:
|
||||
Tuple of (file_type, format_options) where format_options is a list of (label, value) tuples
|
||||
"""
|
||||
ext_lower = ext.lower() if ext else ''
|
||||
|
||||
ext_lower = ext.lower() if ext else ""
|
||||
|
||||
from SYS.utils_constant import mime_maps
|
||||
|
||||
|
||||
found_type = "unknown"
|
||||
|
||||
|
||||
# Find type based on extension
|
||||
for category, formats in mime_maps.items():
|
||||
for fmt_key, fmt_info in formats.items():
|
||||
@@ -81,13 +86,13 @@ class ExportModal(ModalScreen):
|
||||
break
|
||||
if found_type != "unknown":
|
||||
break
|
||||
|
||||
|
||||
# Build format options for the found type
|
||||
format_options = []
|
||||
|
||||
|
||||
# If unknown, fallback to audio (matching legacy behavior)
|
||||
target_type = found_type if found_type in mime_maps else "audio"
|
||||
|
||||
|
||||
if target_type in mime_maps:
|
||||
# Sort formats alphabetically
|
||||
sorted_formats = sorted(mime_maps[target_type].items())
|
||||
@@ -95,15 +100,21 @@ class ExportModal(ModalScreen):
|
||||
label = fmt_key.upper()
|
||||
value = fmt_key
|
||||
format_options.append((label, value))
|
||||
|
||||
|
||||
return (target_type, format_options)
|
||||
|
||||
|
||||
def _get_library_options(self) -> list:
|
||||
"""Get available library options from config.conf."""
|
||||
options = [("Local", "local")]
|
||||
|
||||
|
||||
try:
|
||||
from config import load_config, get_hydrus_access_key, get_hydrus_url, get_debrid_api_key
|
||||
from config import (
|
||||
load_config,
|
||||
get_hydrus_access_key,
|
||||
get_hydrus_url,
|
||||
get_debrid_api_key,
|
||||
)
|
||||
|
||||
config = load_config()
|
||||
|
||||
hydrus_url = (get_hydrus_url(config, "home") or "").strip()
|
||||
@@ -114,29 +125,39 @@ class ExportModal(ModalScreen):
|
||||
debrid_api_key = get_debrid_api_key(config)
|
||||
if self.debrid_available and debrid_api_key:
|
||||
options.append(("Debrid", "debrid"))
|
||||
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error loading config for libraries: {e}")
|
||||
|
||||
|
||||
return options
|
||||
|
||||
|
||||
def _get_metadata_text(self) -> str:
|
||||
"""Format metadata from result data in a consistent display format."""
|
||||
metadata = self.result_data.get('metadata', {})
|
||||
source = self.result_data.get('source', 'unknown')
|
||||
logger.info(f"_get_metadata_text called - source: {source}, metadata type: {type(metadata)}, keys: {list(metadata.keys()) if metadata else 'empty'}")
|
||||
|
||||
metadata = self.result_data.get("metadata", {})
|
||||
source = self.result_data.get("source", "unknown")
|
||||
logger.info(
|
||||
f"_get_metadata_text called - source: {source}, metadata type: {type(metadata)}, keys: {list(metadata.keys()) if metadata else 'empty'}"
|
||||
)
|
||||
|
||||
if not metadata:
|
||||
logger.info(f"_get_metadata_text - No metadata found, returning 'No metadata available'")
|
||||
logger.info(
|
||||
f"_get_metadata_text - No metadata found, returning 'No metadata available'"
|
||||
)
|
||||
return "No metadata available"
|
||||
|
||||
|
||||
lines = []
|
||||
|
||||
|
||||
# Only display these specific fields in this order
|
||||
display_fields = [
|
||||
'duration', 'size', 'ext', 'media_type', 'time_imported', 'time_modified', 'hash'
|
||||
"duration",
|
||||
"size",
|
||||
"ext",
|
||||
"media_type",
|
||||
"time_imported",
|
||||
"time_modified",
|
||||
"hash",
|
||||
]
|
||||
|
||||
|
||||
# Display fields in a consistent order
|
||||
for field in display_fields:
|
||||
if field in metadata:
|
||||
@@ -147,9 +168,9 @@ class ExportModal(ModalScreen):
|
||||
# Use central formatting rule
|
||||
formatted_value = format_metadata_value(field, value)
|
||||
# Format: "Field Name: value"
|
||||
field_label = field.replace('_', ' ').title()
|
||||
field_label = field.replace("_", " ").title()
|
||||
lines.append(f"{field_label}: {formatted_value}")
|
||||
|
||||
|
||||
# If we found any fields, display them
|
||||
if lines:
|
||||
logger.info(f"_get_metadata_text - Returning {len(lines)} formatted metadata lines")
|
||||
@@ -157,12 +178,12 @@ class ExportModal(ModalScreen):
|
||||
else:
|
||||
logger.info(f"_get_metadata_text - No matching fields found in metadata")
|
||||
return "No metadata available"
|
||||
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
"""Compose the export modal screen."""
|
||||
with Container(id="export-container"):
|
||||
yield Static("Export File with Metadata", id="export-title")
|
||||
|
||||
|
||||
# Row 1: Three columns (Tag, Metadata, Export-To Options)
|
||||
self.tags_textarea = TextArea(
|
||||
text=self._format_tags(),
|
||||
@@ -171,7 +192,7 @@ class ExportModal(ModalScreen):
|
||||
)
|
||||
yield self.tags_textarea
|
||||
self.tags_textarea.border_title = "Tag"
|
||||
|
||||
|
||||
# Metadata display instead of files tree
|
||||
self.metadata_display = Static(
|
||||
self._get_metadata_text(),
|
||||
@@ -179,120 +200,128 @@ class ExportModal(ModalScreen):
|
||||
)
|
||||
yield self.metadata_display
|
||||
self.metadata_display.border = ("solid", "dodgerblue")
|
||||
|
||||
|
||||
# Right column: Export options
|
||||
with Vertical(id="export-options"):
|
||||
# Export To selector
|
||||
self.export_to_select = Select(
|
||||
[("0x0", "0x0"), ("Libraries", "libraries"), ("Custom Path", "path")],
|
||||
id="export-to-select"
|
||||
id="export-to-select",
|
||||
)
|
||||
yield self.export_to_select
|
||||
|
||||
|
||||
# Libraries selector (initially hidden)
|
||||
library_options = self._get_library_options()
|
||||
self.libraries_select = Select(
|
||||
library_options,
|
||||
id="libraries-select"
|
||||
)
|
||||
self.libraries_select = Select(library_options, id="libraries-select")
|
||||
yield self.libraries_select
|
||||
|
||||
|
||||
# Custom path input (initially hidden)
|
||||
self.custom_path_input = Input(
|
||||
placeholder="Enter custom export path",
|
||||
id="custom-path-input"
|
||||
placeholder="Enter custom export path", id="custom-path-input"
|
||||
)
|
||||
yield self.custom_path_input
|
||||
|
||||
|
||||
# Get metadata for size and format options
|
||||
metadata = self.result_data.get('metadata', {})
|
||||
original_size = metadata.get('size', '')
|
||||
ext = metadata.get('ext', '')
|
||||
|
||||
metadata = self.result_data.get("metadata", {})
|
||||
original_size = metadata.get("size", "")
|
||||
ext = metadata.get("ext", "")
|
||||
|
||||
# Store the extension and determine file type
|
||||
self.file_ext = ext
|
||||
self.file_type, format_options = self._determine_file_type(ext)
|
||||
|
||||
|
||||
# Format size in MB for display
|
||||
if original_size:
|
||||
size_mb = int(original_size / (1024 * 1024)) if isinstance(original_size, (int, float)) else original_size
|
||||
size_mb = (
|
||||
int(original_size / (1024 * 1024))
|
||||
if isinstance(original_size, (int, float))
|
||||
else original_size
|
||||
)
|
||||
size_display = f"{size_mb}Mb"
|
||||
else:
|
||||
size_display = ""
|
||||
|
||||
|
||||
# Size input
|
||||
self.size_input = Input(
|
||||
value=size_display,
|
||||
placeholder="Size (can reduce)",
|
||||
id="size-input",
|
||||
disabled=(self.file_type == 'document') # Disable for documents - no resizing needed
|
||||
disabled=(
|
||||
self.file_type == "document"
|
||||
), # Disable for documents - no resizing needed
|
||||
)
|
||||
yield self.size_input
|
||||
|
||||
|
||||
# Determine the default format value (match current extension to format options)
|
||||
default_format = None
|
||||
if ext and format_options:
|
||||
# Map extension to format value (e.g., .flac -> "flac", .mp3 -> "mp3", .m4a -> "m4a")
|
||||
ext_lower = ext.lower().lstrip('.') # Remove leading dot if present
|
||||
ext_lower = ext.lower().lstrip(".") # Remove leading dot if present
|
||||
# Try to find matching format option
|
||||
for _, value in format_options:
|
||||
if value and (ext_lower == value or f".{ext_lower}" == ext or ext.endswith(f".{value}")):
|
||||
if value and (
|
||||
ext_lower == value
|
||||
or f".{ext_lower}" == ext
|
||||
or ext.endswith(f".{value}")
|
||||
):
|
||||
default_format = value
|
||||
logger.debug(f"Matched extension {ext} to format {value}")
|
||||
break
|
||||
# If no exact match, use first option
|
||||
if not default_format and format_options:
|
||||
default_format = format_options[0][1]
|
||||
logger.debug(f"No format match for {ext}, using first option: {default_format}")
|
||||
|
||||
logger.debug(
|
||||
f"No format match for {ext}, using first option: {default_format}"
|
||||
)
|
||||
|
||||
# Store the default format to apply after mount
|
||||
self.default_format = default_format
|
||||
|
||||
|
||||
# Format selector based on file type
|
||||
self.format_select = Select(
|
||||
format_options if format_options else [("No conversion", "")],
|
||||
id="format-select",
|
||||
disabled=not format_options # Disable if no format options (e.g., documents)
|
||||
disabled=not format_options, # Disable if no format options (e.g., documents)
|
||||
)
|
||||
yield self.format_select
|
||||
|
||||
|
||||
# Row 2: Buttons
|
||||
with Horizontal(id="export-buttons"):
|
||||
yield Button("Cancel", id="cancel-btn", variant="default")
|
||||
yield Button("Export", id="export-btn", variant="primary")
|
||||
|
||||
|
||||
def _format_tags(self) -> str:
|
||||
"""Format tags from result data."""
|
||||
tags = self.result_data.get('tags', '')
|
||||
tags = self.result_data.get("tags", "")
|
||||
if isinstance(tags, str):
|
||||
# Split by comma and rejoin with newlines
|
||||
tags_list = [tag.strip() for tag in tags.split(',') if tag.strip()]
|
||||
return '\n'.join(tags_list)
|
||||
tags_list = [tag.strip() for tag in tags.split(",") if tag.strip()]
|
||||
return "\n".join(tags_list)
|
||||
elif isinstance(tags, list):
|
||||
return '\n'.join(tags)
|
||||
return ''
|
||||
|
||||
return "\n".join(tags)
|
||||
return ""
|
||||
|
||||
def on_button_pressed(self, event: Button.Pressed) -> None:
|
||||
"""Handle button press events."""
|
||||
button_id = event.button.id
|
||||
|
||||
|
||||
if button_id == "export-btn":
|
||||
self._handle_export()
|
||||
elif button_id == "cancel-btn":
|
||||
self.action_cancel()
|
||||
|
||||
|
||||
def on_select_changed(self, event: Select.Changed) -> None:
|
||||
"""Handle select widget changes."""
|
||||
if event.control.id == "export-to-select":
|
||||
# Show/hide custom path and libraries based on selection
|
||||
if self.custom_path_input:
|
||||
self.custom_path_input.display = (event.value == "path")
|
||||
self.custom_path_input.display = event.value == "path"
|
||||
if self.libraries_select:
|
||||
self.libraries_select.display = (event.value == "libraries")
|
||||
self.libraries_select.display = event.value == "libraries"
|
||||
elif event.control.id == "libraries-select":
|
||||
# Handle library selection (no special action needed currently)
|
||||
logger.debug(f"Library selected: {event.value}")
|
||||
|
||||
|
||||
def on_mount(self) -> None:
|
||||
"""Handle mount event."""
|
||||
# Initially hide custom path and libraries inputs (default is "0x0")
|
||||
@@ -300,24 +329,27 @@ class ExportModal(ModalScreen):
|
||||
self.custom_path_input.display = False
|
||||
if self.libraries_select:
|
||||
self.libraries_select.display = False
|
||||
|
||||
|
||||
# Set the default format value to show it selected instead of "Select"
|
||||
if self.default_format and self.format_select:
|
||||
self.format_select.value = self.default_format
|
||||
logger.debug(f"Set format selector to default value: {self.default_format}")
|
||||
|
||||
|
||||
# Refresh metadata display after mount to ensure data is loaded
|
||||
if self.metadata_display:
|
||||
metadata_text = self._get_metadata_text()
|
||||
self.metadata_display.update(metadata_text)
|
||||
logger.debug(f"Updated metadata display on mount: {bool(self.result_data.get('metadata'))}")
|
||||
logger.debug(
|
||||
f"Updated metadata display on mount: {bool(self.result_data.get('metadata'))}"
|
||||
)
|
||||
|
||||
def _handle_export(self) -> None:
|
||||
"""Handle the export action."""
|
||||
try:
|
||||
tags_text = self.tags_textarea.text.strip()
|
||||
export_to = self.export_to_select.value if self.export_to_select else "0x0"
|
||||
custom_path = self.custom_path_input.value.strip() if self.custom_path_input else ""
|
||||
|
||||
|
||||
# Get library value - handle Select.BLANK case
|
||||
library = "local" # default
|
||||
if self.libraries_select and str(self.libraries_select.value) != "Select.BLANK":
|
||||
@@ -331,55 +363,87 @@ class ExportModal(ModalScreen):
|
||||
library = options[0][1] # Get the value part of first option tuple
|
||||
except Exception:
|
||||
library = "local"
|
||||
|
||||
|
||||
size = self.size_input.value.strip() if self.size_input else ""
|
||||
file_format = self.format_select.value if self.format_select else "mp4"
|
||||
|
||||
|
||||
# Parse tags from textarea (one per line)
|
||||
export_tags = set()
|
||||
for line in tags_text.split('\n'):
|
||||
for line in tags_text.split("\n"):
|
||||
tag = line.strip()
|
||||
if tag:
|
||||
export_tags.add(tag)
|
||||
|
||||
|
||||
# For Hydrus export, filter out metadata-only tags (hash:, url:, relationship:)
|
||||
if export_to == "libraries" and library == "hydrus":
|
||||
metadata_prefixes = {'hash:', 'url:', 'relationship:'}
|
||||
export_tags = {tag for tag in export_tags if not any(tag.lower().startswith(prefix) for prefix in metadata_prefixes)}
|
||||
logger.info(f"Filtered tags for Hydrus - removed metadata tags, {len(export_tags)} tags remaining")
|
||||
|
||||
metadata_prefixes = {"hash:", "url:", "relationship:"}
|
||||
export_tags = {
|
||||
tag
|
||||
for tag in export_tags
|
||||
if not any(tag.lower().startswith(prefix) for prefix in metadata_prefixes)
|
||||
}
|
||||
logger.info(
|
||||
f"Filtered tags for Hydrus - removed metadata tags, {len(export_tags)} tags remaining"
|
||||
)
|
||||
|
||||
# Extract title and add as searchable tags if not already present
|
||||
title = self.result_data.get('title', '').strip()
|
||||
title = self.result_data.get("title", "").strip()
|
||||
if title:
|
||||
# Add the full title as a tag if not already present
|
||||
title_tag = f"title:{title}"
|
||||
if title_tag not in export_tags and not any(t.startswith('title:') for t in export_tags):
|
||||
if title_tag not in export_tags and not any(
|
||||
t.startswith("title:") for t in export_tags
|
||||
):
|
||||
export_tags.add(title_tag)
|
||||
|
||||
|
||||
# Extract individual words from title as searchable tags (if reasonable length)
|
||||
# Skip very short words and common stop words
|
||||
if len(title) < 100: # Only for reasonably short titles
|
||||
stop_words = {'the', 'a', 'an', 'and', 'or', 'of', 'in', 'to', 'for', 'is', 'it', 'at', 'by', 'from', 'with', 'as', 'be', 'on', 'that', 'this', 'this'}
|
||||
stop_words = {
|
||||
"the",
|
||||
"a",
|
||||
"an",
|
||||
"and",
|
||||
"or",
|
||||
"of",
|
||||
"in",
|
||||
"to",
|
||||
"for",
|
||||
"is",
|
||||
"it",
|
||||
"at",
|
||||
"by",
|
||||
"from",
|
||||
"with",
|
||||
"as",
|
||||
"be",
|
||||
"on",
|
||||
"that",
|
||||
"this",
|
||||
"this",
|
||||
}
|
||||
words = title.lower().split()
|
||||
for word in words:
|
||||
# Clean up word (remove punctuation)
|
||||
clean_word = ''.join(c for c in word if c.isalnum())
|
||||
clean_word = "".join(c for c in word if c.isalnum())
|
||||
# Only add if not a stop word and has some length
|
||||
if clean_word and len(clean_word) > 2 and clean_word not in stop_words:
|
||||
if clean_word not in export_tags:
|
||||
export_tags.add(clean_word)
|
||||
logger.info(f"Extracted {len(words)} words from title, added searchable title tags")
|
||||
|
||||
logger.info(
|
||||
f"Extracted {len(words)} words from title, added searchable title tags"
|
||||
)
|
||||
|
||||
# Validate required fields - allow export to continue for Hydrus even with 0 actual tags
|
||||
# (metadata tags will still be in the sidecar, and tags can be added later)
|
||||
if not export_tags and export_to != "libraries":
|
||||
logger.warning("No tags provided for export")
|
||||
return
|
||||
|
||||
|
||||
if export_to == "libraries" and not export_tags:
|
||||
logger.warning("No actual tags for Hydrus export (only metadata was present)")
|
||||
# Don't return - allow export to continue, file will be added to Hydrus even without tags
|
||||
|
||||
|
||||
# Determine export path
|
||||
export_path = None
|
||||
if export_to == "path":
|
||||
@@ -391,61 +455,62 @@ class ExportModal(ModalScreen):
|
||||
export_path = library # "local", "hydrus", "debrid"
|
||||
else:
|
||||
export_path = export_to # "0x0"
|
||||
|
||||
|
||||
# Get metadata from result_data
|
||||
metadata = self.result_data.get('metadata', {})
|
||||
|
||||
metadata = self.result_data.get("metadata", {})
|
||||
|
||||
# Extract file source info from result_data (passed by hub-ui)
|
||||
file_hash = self.result_data.get('hash')
|
||||
file_url = self.result_data.get('url')
|
||||
file_path = self.result_data.get('path')
|
||||
source = self.result_data.get('source', 'unknown')
|
||||
|
||||
file_hash = self.result_data.get("hash")
|
||||
file_url = self.result_data.get("url")
|
||||
file_path = self.result_data.get("path")
|
||||
source = self.result_data.get("source", "unknown")
|
||||
|
||||
# Prepare export data
|
||||
export_data = {
|
||||
'export_to': export_to,
|
||||
'export_path': export_path,
|
||||
'library': library if export_to == "libraries" else None,
|
||||
'tags': export_tags,
|
||||
'size': size if size else None,
|
||||
'format': file_format,
|
||||
'metadata': metadata,
|
||||
'original_data': self.result_data,
|
||||
'hash': file_hash,
|
||||
'url': file_url,
|
||||
'path': file_path,
|
||||
'source': source,
|
||||
"export_to": export_to,
|
||||
"export_path": export_path,
|
||||
"library": library if export_to == "libraries" else None,
|
||||
"tags": export_tags,
|
||||
"size": size if size else None,
|
||||
"format": file_format,
|
||||
"metadata": metadata,
|
||||
"original_data": self.result_data,
|
||||
"hash": file_hash,
|
||||
"url": file_url,
|
||||
"path": file_path,
|
||||
"source": source,
|
||||
}
|
||||
|
||||
logger.info(f"Export initiated: destination={export_path}, format={file_format}, size={size}, tags={export_tags}, source={source}, hash={file_hash}, path={file_path}")
|
||||
|
||||
|
||||
logger.info(
|
||||
f"Export initiated: destination={export_path}, format={file_format}, size={size}, tags={export_tags}, source={source}, hash={file_hash}, path={file_path}"
|
||||
)
|
||||
|
||||
# Dismiss the modal and return the export data
|
||||
self.dismiss(export_data)
|
||||
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error during export: {e}", exc_info=True)
|
||||
|
||||
|
||||
def action_cancel(self) -> None:
|
||||
"""Handle cancel action."""
|
||||
self.dismiss(None)
|
||||
|
||||
|
||||
|
||||
def create_notes_sidecar(file_path: Path, notes: str) -> None:
|
||||
"""Create a .notes sidecar file with notes text.
|
||||
|
||||
|
||||
Only creates file if notes are not empty.
|
||||
|
||||
|
||||
Args:
|
||||
file_path: Path to the exported file
|
||||
notes: Notes text
|
||||
"""
|
||||
if not notes or not notes.strip():
|
||||
return
|
||||
|
||||
notes_path = file_path.with_suffix(file_path.suffix + '.notes')
|
||||
|
||||
notes_path = file_path.with_suffix(file_path.suffix + ".notes")
|
||||
try:
|
||||
with open(notes_path, 'w', encoding='utf-8') as f:
|
||||
with open(notes_path, "w", encoding="utf-8") as f:
|
||||
f.write(notes.strip())
|
||||
logger.info(f"Created notes sidecar: {notes_path}")
|
||||
except Exception as e:
|
||||
@@ -454,50 +519,56 @@ def create_notes_sidecar(file_path: Path, notes: str) -> None:
|
||||
|
||||
def determine_needs_conversion(current_ext: str, target_format: str) -> bool:
|
||||
"""Determine if conversion is needed between two formats.
|
||||
|
||||
|
||||
Args:
|
||||
current_ext: Current file extension (e.g., '.flac')
|
||||
target_format: Target format name (e.g., 'mp3') or NoSelection object
|
||||
|
||||
|
||||
Returns:
|
||||
True if conversion is needed, False if it's already the target format
|
||||
"""
|
||||
# Handle NoSelection or None
|
||||
if not target_format or target_format == "" or str(target_format.__class__.__name__) == 'NoSelection':
|
||||
if (
|
||||
not target_format
|
||||
or target_format == ""
|
||||
or str(target_format.__class__.__name__) == "NoSelection"
|
||||
):
|
||||
return False # No conversion requested
|
||||
|
||||
|
||||
# Normalize the current extension
|
||||
current_ext_lower = current_ext.lower().lstrip('.')
|
||||
current_ext_lower = current_ext.lower().lstrip(".")
|
||||
target_format_lower = str(target_format).lower()
|
||||
|
||||
|
||||
# Check if they match
|
||||
return current_ext_lower != target_format_lower
|
||||
|
||||
|
||||
def calculate_size_tolerance(metadata: dict, user_size_mb: Optional[str]) -> tuple[Optional[int], Optional[int]]:
|
||||
def calculate_size_tolerance(
|
||||
metadata: dict, user_size_mb: Optional[str]
|
||||
) -> tuple[Optional[int], Optional[int]]:
|
||||
"""Calculate target size with 1MB grace period.
|
||||
|
||||
|
||||
Args:
|
||||
metadata: File metadata containing 'size' in bytes
|
||||
user_size_mb: User-entered size like "756Mb" or empty string
|
||||
|
||||
|
||||
Returns:
|
||||
Tuple of (target_bytes, grace_bytes) where grace_bytes is 1MB (1048576),
|
||||
or (None, None) if no size specified
|
||||
"""
|
||||
grace_bytes = 1 * 1024 * 1024 # 1MB grace period
|
||||
|
||||
|
||||
if not user_size_mb or not user_size_mb.strip():
|
||||
return None, grace_bytes
|
||||
|
||||
|
||||
try:
|
||||
# Parse the size string (format like "756Mb")
|
||||
size_str = user_size_mb.strip().lower()
|
||||
if size_str.endswith('mb'):
|
||||
if size_str.endswith("mb"):
|
||||
size_str = size_str[:-2]
|
||||
elif size_str.endswith('m'):
|
||||
elif size_str.endswith("m"):
|
||||
size_str = size_str[:-1]
|
||||
|
||||
|
||||
size_mb = float(size_str)
|
||||
target_bytes = int(size_mb * 1024 * 1024)
|
||||
return target_bytes, grace_bytes
|
||||
|
||||
Reference in New Issue
Block a user