j
This commit is contained in:
@@ -22,7 +22,7 @@
|
|||||||
"((1fichier\\.com|megadl\\.fr|alterupload\\.com|cjoint\\.net|desfichiers\\.com|dfichiers\\.com|mesfichiers\\.org|piecejointe\\.net|pjointe\\.com|tenvoi\\.com|dl4free\\.com)/\\?[a-zA-Z0-9]{5,30}(&pw=[^&]+)?)"
|
"((1fichier\\.com|megadl\\.fr|alterupload\\.com|cjoint\\.net|desfichiers\\.com|dfichiers\\.com|mesfichiers\\.org|piecejointe\\.net|pjointe\\.com|tenvoi\\.com|dl4free\\.com)/\\?[a-zA-Z0-9]{5,30}(&pw=[^&]+)?)"
|
||||||
],
|
],
|
||||||
"regexp": "((1fichier\\.com|megadl\\.fr|alterupload\\.com|cjoint\\.net|desfichiers\\.com|dfichiers\\.com|mesfichiers\\.org|piecejointe\\.net|pjointe\\.com|tenvoi\\.com|dl4free\\.com)/\\?[a-zA-Z0-9]{5,30}(&pw=[^&]+)?)",
|
"regexp": "((1fichier\\.com|megadl\\.fr|alterupload\\.com|cjoint\\.net|desfichiers\\.com|dfichiers\\.com|mesfichiers\\.org|piecejointe\\.net|pjointe\\.com|tenvoi\\.com|dl4free\\.com)/\\?[a-zA-Z0-9]{5,30}(&pw=[^&]+)?)",
|
||||||
"status": false
|
"status": true
|
||||||
},
|
},
|
||||||
"rapidgator": {
|
"rapidgator": {
|
||||||
"name": "rapidgator",
|
"name": "rapidgator",
|
||||||
@@ -339,7 +339,7 @@
|
|||||||
"(file\\.al/[0-9a-zA-Z]{12})"
|
"(file\\.al/[0-9a-zA-Z]{12})"
|
||||||
],
|
],
|
||||||
"regexp": "(file\\.al/[0-9a-zA-Z]{12})",
|
"regexp": "(file\\.al/[0-9a-zA-Z]{12})",
|
||||||
"status": false
|
"status": true
|
||||||
},
|
},
|
||||||
"filedot": {
|
"filedot": {
|
||||||
"name": "filedot",
|
"name": "filedot",
|
||||||
@@ -477,7 +477,7 @@
|
|||||||
"isra\\.cloud/\\?op=report_file&id=([0-9a-zA-Z]{12})"
|
"isra\\.cloud/\\?op=report_file&id=([0-9a-zA-Z]{12})"
|
||||||
],
|
],
|
||||||
"regexp": "((isra\\.cloud/[0-9a-zA-Z]{12}))|(isra\\.cloud/\\?op=report_file&id=([0-9a-zA-Z]{12}))",
|
"regexp": "((isra\\.cloud/[0-9a-zA-Z]{12}))|(isra\\.cloud/\\?op=report_file&id=([0-9a-zA-Z]{12}))",
|
||||||
"status": false,
|
"status": true,
|
||||||
"hardRedirect": [
|
"hardRedirect": [
|
||||||
"isra\\.cloud/([0-9a-zA-Z]{12})"
|
"isra\\.cloud/([0-9a-zA-Z]{12})"
|
||||||
]
|
]
|
||||||
@@ -507,7 +507,7 @@
|
|||||||
"mediafire\\.com/(\\?|download/|file/|download\\.php\\?)([0-9a-z]{15})"
|
"mediafire\\.com/(\\?|download/|file/|download\\.php\\?)([0-9a-z]{15})"
|
||||||
],
|
],
|
||||||
"regexp": "mediafire\\.com/(\\?|download/|file/|download\\.php\\?)([0-9a-z]{15})",
|
"regexp": "mediafire\\.com/(\\?|download/|file/|download\\.php\\?)([0-9a-z]{15})",
|
||||||
"status": true
|
"status": false
|
||||||
},
|
},
|
||||||
"mexashare": {
|
"mexashare": {
|
||||||
"name": "mexashare",
|
"name": "mexashare",
|
||||||
|
|||||||
114
SYS/config.py
114
SYS/config.py
@@ -126,8 +126,17 @@ def get_provider_block(config: Dict[str, Any], name: str) -> Dict[str, Any]:
|
|||||||
provider_cfg = config.get("provider")
|
provider_cfg = config.get("provider")
|
||||||
if not isinstance(provider_cfg, dict):
|
if not isinstance(provider_cfg, dict):
|
||||||
return {}
|
return {}
|
||||||
block = provider_cfg.get(str(name).strip().lower())
|
normalized = _normalize_provider_name(name)
|
||||||
return block if isinstance(block, dict) else {}
|
if normalized:
|
||||||
|
block = provider_cfg.get(normalized)
|
||||||
|
if isinstance(block, dict):
|
||||||
|
return block
|
||||||
|
for key, block in provider_cfg.items():
|
||||||
|
if not isinstance(block, dict):
|
||||||
|
continue
|
||||||
|
if _normalize_provider_name(key) == normalized:
|
||||||
|
return block
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
def get_soulseek_username(config: Dict[str, Any]) -> Optional[str]:
|
def get_soulseek_username(config: Dict[str, Any]) -> Optional[str]:
|
||||||
@@ -334,6 +343,77 @@ def resolve_debug_log(config: Dict[str, Any]) -> Optional[Path]:
|
|||||||
path = Path.cwd() / path
|
path = Path.cwd() / path
|
||||||
return path
|
return path
|
||||||
|
|
||||||
|
def _normalize_provider_name(value: Any) -> Optional[str]:
|
||||||
|
candidate = str(value or "").strip().lower()
|
||||||
|
return candidate if candidate else None
|
||||||
|
|
||||||
|
def _extract_api_key(value: Any) -> Optional[str]:
|
||||||
|
if isinstance(value, dict):
|
||||||
|
for key in ("api_key", "API_KEY", "apikey", "APIKEY"):
|
||||||
|
candidate = value.get(key)
|
||||||
|
if isinstance(candidate, str) and candidate.strip():
|
||||||
|
return candidate.strip()
|
||||||
|
elif isinstance(value, str):
|
||||||
|
trimmed = value.strip()
|
||||||
|
if trimmed:
|
||||||
|
return trimmed
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _sync_alldebrid_api_key(config: Dict[str, Any]) -> None:
|
||||||
|
if not isinstance(config, dict):
|
||||||
|
return
|
||||||
|
|
||||||
|
providers = config.get("provider")
|
||||||
|
if not isinstance(providers, dict):
|
||||||
|
providers = {}
|
||||||
|
config["provider"] = providers
|
||||||
|
|
||||||
|
provider_entry = providers.get("alldebrid")
|
||||||
|
provider_section: Dict[str, Any] | None = None
|
||||||
|
provider_key = None
|
||||||
|
if isinstance(provider_entry, dict):
|
||||||
|
provider_section = provider_entry
|
||||||
|
provider_key = _extract_api_key(provider_section)
|
||||||
|
elif isinstance(provider_entry, str):
|
||||||
|
provider_key = provider_entry.strip()
|
||||||
|
if provider_key:
|
||||||
|
provider_section = {"api_key": provider_key}
|
||||||
|
providers["alldebrid"] = provider_section
|
||||||
|
|
||||||
|
store_block = config.get("store")
|
||||||
|
if not isinstance(store_block, dict):
|
||||||
|
store_block = {}
|
||||||
|
config["store"] = store_block
|
||||||
|
|
||||||
|
debrid_block = store_block.get("debrid")
|
||||||
|
store_key = None
|
||||||
|
if isinstance(debrid_block, dict):
|
||||||
|
service_entry = debrid_block.get("all-debrid")
|
||||||
|
if isinstance(service_entry, dict):
|
||||||
|
store_key = _extract_api_key(service_entry)
|
||||||
|
elif isinstance(service_entry, str):
|
||||||
|
store_key = service_entry.strip()
|
||||||
|
if store_key:
|
||||||
|
debrid_block["all-debrid"] = {"api_key": store_key}
|
||||||
|
else:
|
||||||
|
debrid_block = None
|
||||||
|
|
||||||
|
if provider_key:
|
||||||
|
if debrid_block is None:
|
||||||
|
debrid_block = {}
|
||||||
|
store_block["debrid"] = debrid_block
|
||||||
|
service_section = debrid_block.get("all-debrid")
|
||||||
|
if not isinstance(service_section, dict):
|
||||||
|
service_section = {}
|
||||||
|
debrid_block["all-debrid"] = service_section
|
||||||
|
service_section["api_key"] = provider_key
|
||||||
|
elif store_key:
|
||||||
|
if provider_section is None:
|
||||||
|
provider_section = {}
|
||||||
|
providers["alldebrid"] = provider_section
|
||||||
|
provider_section["api_key"] = store_key
|
||||||
|
|
||||||
|
|
||||||
def _flatten_config_entries(config: Dict[str, Any]) -> Dict[Tuple[str, str, str, str], Any]:
|
def _flatten_config_entries(config: Dict[str, Any]) -> Dict[Tuple[str, str, str, str], Any]:
|
||||||
entries: Dict[Tuple[str, str, str, str], Any] = {}
|
entries: Dict[Tuple[str, str, str, str], Any] = {}
|
||||||
@@ -372,6 +452,7 @@ def load_config() -> Dict[str, Any]:
|
|||||||
# Load strictly from database
|
# Load strictly from database
|
||||||
db_config = get_config_all()
|
db_config = get_config_all()
|
||||||
if db_config:
|
if db_config:
|
||||||
|
_sync_alldebrid_api_key(db_config)
|
||||||
_CONFIG_CACHE = db_config
|
_CONFIG_CACHE = db_config
|
||||||
_LAST_SAVED_CONFIG = deepcopy(db_config)
|
_LAST_SAVED_CONFIG = deepcopy(db_config)
|
||||||
return db_config
|
return db_config
|
||||||
@@ -387,6 +468,7 @@ def reload_config() -> Dict[str, Any]:
|
|||||||
|
|
||||||
def save_config(config: Dict[str, Any]) -> int:
|
def save_config(config: Dict[str, Any]) -> int:
|
||||||
global _CONFIG_CACHE, _LAST_SAVED_CONFIG
|
global _CONFIG_CACHE, _LAST_SAVED_CONFIG
|
||||||
|
_sync_alldebrid_api_key(config)
|
||||||
previous_config = deepcopy(_LAST_SAVED_CONFIG)
|
previous_config = deepcopy(_LAST_SAVED_CONFIG)
|
||||||
changed_count = _count_changed_entries(previous_config, config)
|
changed_count = _count_changed_entries(previous_config, config)
|
||||||
|
|
||||||
@@ -398,17 +480,23 @@ def save_config(config: Dict[str, Any]) -> int:
|
|||||||
if key in ('store', 'provider', 'tool'):
|
if key in ('store', 'provider', 'tool'):
|
||||||
if isinstance(value, dict):
|
if isinstance(value, dict):
|
||||||
for subtype, instances in value.items():
|
for subtype, instances in value.items():
|
||||||
if isinstance(instances, dict):
|
if not isinstance(instances, dict):
|
||||||
if key == 'store':
|
continue
|
||||||
for name, settings in instances.items():
|
if key == 'store':
|
||||||
if isinstance(settings, dict):
|
for name, settings in instances.items():
|
||||||
for k, v in settings.items():
|
if isinstance(settings, dict):
|
||||||
save_config_value(key, subtype, name, k, v)
|
for k, v in settings.items():
|
||||||
count += 1
|
save_config_value(key, subtype, name, k, v)
|
||||||
else:
|
count += 1
|
||||||
for k, v in instances.items():
|
else:
|
||||||
save_config_value(key, subtype, "default", k, v)
|
normalized_subtype = subtype
|
||||||
count += 1
|
if key == 'provider':
|
||||||
|
normalized_subtype = _normalize_provider_name(subtype)
|
||||||
|
if not normalized_subtype:
|
||||||
|
continue
|
||||||
|
for k, v in instances.items():
|
||||||
|
save_config_value(key, normalized_subtype, "default", k, v)
|
||||||
|
count += 1
|
||||||
else:
|
else:
|
||||||
if not key.startswith("_") and value is not None:
|
if not key.startswith("_") and value is not None:
|
||||||
save_config_value("global", "none", "none", key, value)
|
save_config_value("global", "none", "none", key, value)
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import re
|
import re
|
||||||
|
from copy import deepcopy
|
||||||
from typing import Any, Dict, List, Optional
|
from typing import Any, Dict, List, Optional
|
||||||
|
|
||||||
from textual import on, work
|
from textual import on, work
|
||||||
@@ -122,6 +123,20 @@ class ConfigModal(ModalScreen):
|
|||||||
self._input_id_map = {}
|
self._input_id_map = {}
|
||||||
self._matrix_status: Optional[Static] = None
|
self._matrix_status: Optional[Static] = None
|
||||||
self._matrix_test_running = False
|
self._matrix_test_running = False
|
||||||
|
self._editor_snapshot: Optional[Dict[str, Any]] = None
|
||||||
|
|
||||||
|
def _capture_editor_snapshot(self) -> None:
|
||||||
|
self._editor_snapshot = deepcopy(self.config_data)
|
||||||
|
|
||||||
|
def _revert_unsaved_editor_changes(self) -> None:
|
||||||
|
if self._editor_snapshot is not None:
|
||||||
|
self.config_data = deepcopy(self._editor_snapshot)
|
||||||
|
self._editor_snapshot = None
|
||||||
|
|
||||||
|
def _editor_has_changes(self) -> bool:
|
||||||
|
if self._editor_snapshot is None:
|
||||||
|
return True
|
||||||
|
return self.config_data != self._editor_snapshot
|
||||||
|
|
||||||
def compose(self) -> ComposeResult:
|
def compose(self) -> ComposeResult:
|
||||||
with Container(id="config-container"):
|
with Container(id="config-container"):
|
||||||
@@ -541,8 +556,10 @@ class ConfigModal(ModalScreen):
|
|||||||
if not bid: return
|
if not bid: return
|
||||||
|
|
||||||
if bid == "cancel-btn":
|
if bid == "cancel-btn":
|
||||||
|
self._revert_unsaved_editor_changes()
|
||||||
self.dismiss()
|
self.dismiss()
|
||||||
elif bid == "back-btn":
|
elif bid == "back-btn":
|
||||||
|
self._revert_unsaved_editor_changes()
|
||||||
self.editing_item_name = None
|
self.editing_item_name = None
|
||||||
self.editing_item_type = None
|
self.editing_item_type = None
|
||||||
self.refresh_view()
|
self.refresh_view()
|
||||||
@@ -550,6 +567,9 @@ class ConfigModal(ModalScreen):
|
|||||||
self._synchronize_inputs_to_config()
|
self._synchronize_inputs_to_config()
|
||||||
if not self.validate_current_editor():
|
if not self.validate_current_editor():
|
||||||
return
|
return
|
||||||
|
if self.editing_item_name and not self._editor_has_changes():
|
||||||
|
self.notify("No changes to save", severity="warning")
|
||||||
|
return
|
||||||
try:
|
try:
|
||||||
saved = self.save_all()
|
saved = self.save_all()
|
||||||
msg = f"Configuration saved ({saved} entries)"
|
msg = f"Configuration saved ({saved} entries)"
|
||||||
@@ -560,11 +580,13 @@ class ConfigModal(ModalScreen):
|
|||||||
self.editing_item_name = None
|
self.editing_item_name = None
|
||||||
self.editing_item_type = None
|
self.editing_item_type = None
|
||||||
self.refresh_view()
|
self.refresh_view()
|
||||||
|
self._editor_snapshot = None
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
self.notify(f"Save failed: {exc}", severity="error", timeout=10)
|
self.notify(f"Save failed: {exc}", severity="error", timeout=10)
|
||||||
elif bid in self._button_id_map:
|
elif bid in self._button_id_map:
|
||||||
action, itype, name = self._button_id_map[bid]
|
action, itype, name = self._button_id_map[bid]
|
||||||
if action == "edit":
|
if action == "edit":
|
||||||
|
self._capture_editor_snapshot()
|
||||||
self.editing_item_type = itype
|
self.editing_item_type = itype
|
||||||
self.editing_item_name = name
|
self.editing_item_name = name
|
||||||
self.refresh_view()
|
self.refresh_view()
|
||||||
@@ -656,6 +678,8 @@ class ConfigModal(ModalScreen):
|
|||||||
if not stype:
|
if not stype:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
self._capture_editor_snapshot()
|
||||||
|
|
||||||
existing_names: set[str] = set()
|
existing_names: set[str] = set()
|
||||||
store_block = self.config_data.get("store")
|
store_block = self.config_data.get("store")
|
||||||
if isinstance(store_block, dict):
|
if isinstance(store_block, dict):
|
||||||
@@ -704,6 +728,7 @@ class ConfigModal(ModalScreen):
|
|||||||
|
|
||||||
def on_provider_type_selected(self, ptype: str) -> None:
|
def on_provider_type_selected(self, ptype: str) -> None:
|
||||||
if not ptype: return
|
if not ptype: return
|
||||||
|
self._capture_editor_snapshot()
|
||||||
if "provider" not in self.config_data:
|
if "provider" not in self.config_data:
|
||||||
self.config_data["provider"] = {}
|
self.config_data["provider"] = {}
|
||||||
|
|
||||||
@@ -738,51 +763,72 @@ class ConfigModal(ModalScreen):
|
|||||||
return
|
return
|
||||||
|
|
||||||
key = self._input_id_map[widget_id]
|
key = self._input_id_map[widget_id]
|
||||||
|
raw_value = value
|
||||||
|
is_blank_string = isinstance(raw_value, str) and not raw_value.strip()
|
||||||
|
|
||||||
|
existing_value: Any = None
|
||||||
|
item_type = str(self.editing_item_type or "")
|
||||||
|
item_name = str(self.editing_item_name or "")
|
||||||
|
|
||||||
|
if widget_id.startswith("global-"):
|
||||||
|
existing_value = self.config_data.get(key)
|
||||||
|
elif widget_id.startswith("item-") and item_name:
|
||||||
|
if item_type.startswith("store-"):
|
||||||
|
stype = item_type.replace("store-", "")
|
||||||
|
store_block = self.config_data.get("store")
|
||||||
|
if isinstance(store_block, dict):
|
||||||
|
type_block = store_block.get(stype)
|
||||||
|
if isinstance(type_block, dict):
|
||||||
|
section = type_block.get(item_name)
|
||||||
|
if isinstance(section, dict):
|
||||||
|
existing_value = section.get(key)
|
||||||
|
else:
|
||||||
|
section_block = self.config_data.get(item_type)
|
||||||
|
if isinstance(section_block, dict):
|
||||||
|
section = section_block.get(item_name)
|
||||||
|
if isinstance(section, dict):
|
||||||
|
existing_value = section.get(key)
|
||||||
|
|
||||||
|
if is_blank_string and existing_value is None:
|
||||||
|
return
|
||||||
|
|
||||||
# Try to preserve boolean/integer types
|
# Try to preserve boolean/integer types
|
||||||
processed_value = value
|
processed_value = raw_value
|
||||||
if isinstance(value, str):
|
if isinstance(raw_value, str):
|
||||||
low = value.lower()
|
low = raw_value.lower()
|
||||||
if low == "true":
|
if low == "true":
|
||||||
processed_value = True
|
processed_value = True
|
||||||
elif low == "false":
|
elif low == "false":
|
||||||
processed_value = False
|
processed_value = False
|
||||||
elif value.isdigit():
|
elif raw_value.isdigit():
|
||||||
processed_value = int(value)
|
processed_value = int(raw_value)
|
||||||
|
|
||||||
if widget_id.startswith("global-"):
|
if widget_id.startswith("global-"):
|
||||||
self.config_data[key] = processed_value
|
self.config_data[key] = processed_value
|
||||||
elif widget_id.startswith("item-") and self.editing_item_name:
|
elif widget_id.startswith("item-") and item_name:
|
||||||
it = str(self.editing_item_type or "")
|
if item_type.startswith("store-"):
|
||||||
inm = str(self.editing_item_name or "")
|
stype = item_type.replace("store-", "")
|
||||||
|
|
||||||
# Handle nested store structure
|
|
||||||
if it.startswith("store-"):
|
|
||||||
stype = it.replace("store-", "")
|
|
||||||
if "store" not in self.config_data:
|
if "store" not in self.config_data:
|
||||||
self.config_data["store"] = {}
|
self.config_data["store"] = {}
|
||||||
if stype not in self.config_data["store"]:
|
if stype not in self.config_data["store"]:
|
||||||
self.config_data["store"][stype] = {}
|
self.config_data["store"][stype] = {}
|
||||||
if inm not in self.config_data["store"][stype]:
|
if item_name not in self.config_data["store"][stype]:
|
||||||
self.config_data["store"][stype][inm] = {}
|
self.config_data["store"][stype][item_name] = {}
|
||||||
|
|
||||||
# Special case: Renaming the store via the NAME field
|
# Special case: Renaming the store via the NAME field
|
||||||
if key.upper() == "NAME" and processed_value and str(processed_value) != inm:
|
if key.upper() == "NAME" and processed_value and str(processed_value) != item_name:
|
||||||
new_inm = str(processed_value)
|
new_name = str(processed_value)
|
||||||
# Move the whole dictionary to the new key
|
self.config_data["store"][stype][new_name] = self.config_data["store"][stype].pop(item_name)
|
||||||
self.config_data["store"][stype][new_inm] = self.config_data["store"][stype].pop(inm)
|
self.editing_item_name = new_name
|
||||||
# Update editing_item_name so further changes to this screen hit the new key
|
item_name = new_name
|
||||||
self.editing_item_name = new_inm
|
|
||||||
inm = new_inm
|
|
||||||
|
|
||||||
self.config_data["store"][stype][inm][key] = processed_value
|
self.config_data["store"][stype][item_name][key] = processed_value
|
||||||
else:
|
else:
|
||||||
# Provider or other top-level sections
|
if item_type not in self.config_data:
|
||||||
if it not in self.config_data:
|
self.config_data[item_type] = {}
|
||||||
self.config_data[it] = {}
|
if item_name not in self.config_data[item_type]:
|
||||||
if inm not in self.config_data[it]:
|
self.config_data[item_type][item_name] = {}
|
||||||
self.config_data[it][inm] = {}
|
self.config_data[item_type][item_name][key] = processed_value
|
||||||
self.config_data[it][inm][key] = processed_value
|
|
||||||
|
|
||||||
def _synchronize_inputs_to_config(self) -> None:
|
def _synchronize_inputs_to_config(self) -> None:
|
||||||
"""Capture current input/select values before saving."""
|
"""Capture current input/select values before saving."""
|
||||||
@@ -823,7 +869,17 @@ class ConfigModal(ModalScreen):
|
|||||||
return
|
return
|
||||||
self._synchronize_inputs_to_config()
|
self._synchronize_inputs_to_config()
|
||||||
if self._matrix_status:
|
if self._matrix_status:
|
||||||
self._matrix_status.update("Testing Matrix connection…")
|
self._matrix_status.update("Saving configuration before testing…")
|
||||||
|
try:
|
||||||
|
entries = save_config(self.config_data)
|
||||||
|
except Exception as exc:
|
||||||
|
if self._matrix_status:
|
||||||
|
self._matrix_status.update(f"Saving configuration failed: {exc}")
|
||||||
|
self._matrix_test_running = False
|
||||||
|
return
|
||||||
|
self.config_data = reload_config()
|
||||||
|
if self._matrix_status:
|
||||||
|
self._matrix_status.update(f"Saved configuration ({entries} entries). Testing Matrix connection…")
|
||||||
self._matrix_test_running = True
|
self._matrix_test_running = True
|
||||||
self._matrix_test_background()
|
self._matrix_test_background()
|
||||||
|
|
||||||
@@ -885,8 +941,16 @@ class ConfigModal(ModalScreen):
|
|||||||
return
|
return
|
||||||
matrix_block = self.config_data.setdefault("provider", {}).setdefault("matrix", {})
|
matrix_block = self.config_data.setdefault("provider", {}).setdefault("matrix", {})
|
||||||
matrix_block["rooms"] = ", ".join(cleaned)
|
matrix_block["rooms"] = ", ".join(cleaned)
|
||||||
|
try:
|
||||||
|
entries = save_config(self.config_data)
|
||||||
|
except Exception as exc:
|
||||||
|
if self._matrix_status:
|
||||||
|
self._matrix_status.update(f"Saving default rooms failed: {exc}")
|
||||||
|
return
|
||||||
|
self.config_data = reload_config()
|
||||||
if self._matrix_status:
|
if self._matrix_status:
|
||||||
self._matrix_status.update(f"Saved {len(cleaned)} default room(s).")
|
status = f"Saved {len(cleaned)} default room(s) ({entries} rows persisted)."
|
||||||
|
self._matrix_status.update(status)
|
||||||
self.refresh_view()
|
self.refresh_view()
|
||||||
|
|
||||||
@on(Input.Changed)
|
@on(Input.Changed)
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ from textual.containers import Container, Horizontal, ScrollableContainer, Verti
|
|||||||
from textual.screen import ModalScreen
|
from textual.screen import ModalScreen
|
||||||
from textual.widgets import Static, Button, Checkbox
|
from textual.widgets import Static, Button, Checkbox
|
||||||
from textual import work
|
from textual import work
|
||||||
|
from rich.text import Text
|
||||||
|
|
||||||
|
|
||||||
class MatrixRoomPicker(ModalScreen[List[str]]):
|
class MatrixRoomPicker(ModalScreen[List[str]]):
|
||||||
@@ -45,30 +46,13 @@ class MatrixRoomPicker(ModalScreen[List[str]]):
|
|||||||
|
|
||||||
.matrix-room-row {
|
.matrix-room-row {
|
||||||
border-bottom: solid $surface;
|
border-bottom: solid $surface;
|
||||||
padding: 1 0;
|
padding: 0.5 0;
|
||||||
align: left middle;
|
align: left middle;
|
||||||
background: $surface;
|
|
||||||
}
|
|
||||||
|
|
||||||
.matrix-room-checkbox {
|
|
||||||
width: 3;
|
|
||||||
padding: 0;
|
|
||||||
margin-right: 1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.matrix-room-meta {
|
.matrix-room-meta {
|
||||||
padding: 0 1;
|
padding-left: 1;
|
||||||
}
|
|
||||||
|
|
||||||
.matrix-room-name {
|
|
||||||
content-align: left middle;
|
content-align: left middle;
|
||||||
color: $text;
|
|
||||||
}
|
|
||||||
|
|
||||||
.matrix-room-id {
|
|
||||||
content-align: left middle;
|
|
||||||
text-style: dim;
|
|
||||||
color: $text-muted;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#matrix-room-actions {
|
#matrix-room-actions {
|
||||||
@@ -149,17 +133,19 @@ class MatrixRoomPicker(ModalScreen[List[str]]):
|
|||||||
checkbox = Checkbox(
|
checkbox = Checkbox(
|
||||||
"",
|
"",
|
||||||
id=checkbox_id,
|
id=checkbox_id,
|
||||||
classes="matrix-room-checkbox",
|
|
||||||
value=bool(room_id and room_id in self._existing_ids),
|
value=bool(room_id and room_id in self._existing_ids),
|
||||||
)
|
)
|
||||||
self._checkbox_map[checkbox_id] = room_id
|
self._checkbox_map[checkbox_id] = room_id
|
||||||
info = Vertical(classes="matrix-room-meta")
|
|
||||||
info.mount(Static(name or room_id or "Matrix Room", classes="matrix-room-name"))
|
label = Text(name or room_id or "Matrix Room")
|
||||||
info.mount(Static(room_id or "(no id)", classes="matrix-room-id"))
|
label.stylize("bold")
|
||||||
|
label.append("\n")
|
||||||
|
label.append(room_id or "(no id)", style="dim")
|
||||||
|
|
||||||
row = Horizontal(classes="matrix-room-row")
|
row = Horizontal(classes="matrix-room-row")
|
||||||
self._checklist.mount(row)
|
self._checklist.mount(row)
|
||||||
row.mount(checkbox)
|
row.mount(checkbox)
|
||||||
row.mount(info)
|
row.mount(Static(label, classes="matrix-room-meta"))
|
||||||
self._set_status("Loaded rooms. Select one or more and save.")
|
self._set_status("Loaded rooms. Select one or more and save.")
|
||||||
if self._save_button:
|
if self._save_button:
|
||||||
self._save_button.disabled = False
|
self._save_button.disabled = False
|
||||||
|
|||||||
@@ -1063,28 +1063,6 @@ class Download_File(Cmdlet):
|
|||||||
|
|
||||||
return selection_format_id
|
return selection_format_id
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _format_selector_for_query_height(query_format: str) -> Optional[str]:
|
|
||||||
import re
|
|
||||||
|
|
||||||
if query_format is None:
|
|
||||||
return None
|
|
||||||
|
|
||||||
s = str(query_format).strip().lower()
|
|
||||||
m = re.match(r"^(\d{2,5})p$", s)
|
|
||||||
if not m:
|
|
||||||
return None
|
|
||||||
|
|
||||||
try:
|
|
||||||
height = int(m.group(1))
|
|
||||||
except Exception:
|
|
||||||
return None
|
|
||||||
|
|
||||||
if height <= 0:
|
|
||||||
raise ValueError(f"Invalid height selection: {query_format}")
|
|
||||||
|
|
||||||
return f"bv*[height<={height}]+ba"
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _canonicalize_url_for_storage(*, requested_url: str, ytdlp_tool: YtDlpTool, playlist_items: Optional[str]) -> str:
|
def _canonicalize_url_for_storage(*, requested_url: str, ytdlp_tool: YtDlpTool, playlist_items: Optional[str]) -> str:
|
||||||
if playlist_items:
|
if playlist_items:
|
||||||
@@ -1929,7 +1907,7 @@ class Download_File(Cmdlet):
|
|||||||
query_format = str(fmt_candidate).strip()
|
query_format = str(fmt_candidate).strip()
|
||||||
except Exception:
|
except Exception:
|
||||||
query_format = None
|
query_format = None
|
||||||
|
|
||||||
query_audio: Optional[bool] = None
|
query_audio: Optional[bool] = None
|
||||||
try:
|
try:
|
||||||
audio_values = query_keyed.get("audio", []) if isinstance(query_keyed, dict) else []
|
audio_values = query_keyed.get("audio", []) if isinstance(query_keyed, dict) else []
|
||||||
@@ -1980,20 +1958,20 @@ class Download_File(Cmdlet):
|
|||||||
formats_cache: Dict[str, Optional[List[Dict[str, Any]]]] = {}
|
formats_cache: Dict[str, Optional[List[Dict[str, Any]]]] = {}
|
||||||
playlist_items = str(parsed.get("item")) if parsed.get("item") else None
|
playlist_items = str(parsed.get("item")) if parsed.get("item") else None
|
||||||
ytdl_format = None
|
ytdl_format = None
|
||||||
|
height_selector = None
|
||||||
if query_format and not query_wants_audio:
|
if query_format and not query_wants_audio:
|
||||||
try:
|
try:
|
||||||
height_selector = self._format_selector_for_query_height(query_format)
|
height_selector = ytdlp_tool.resolve_height_selector(query_format)
|
||||||
except ValueError as e:
|
except Exception:
|
||||||
log(f"Error parsing format selection: {e}", file=sys.stderr)
|
height_selector = None
|
||||||
return 1
|
if height_selector:
|
||||||
|
ytdl_format = height_selector
|
||||||
|
else:
|
||||||
|
import re
|
||||||
|
|
||||||
if height_selector:
|
if not re.match(r"^\s*#?\d+\s*$", str(query_format)):
|
||||||
ytdl_format = height_selector
|
ytdl_format = query_format
|
||||||
else:
|
debug(f"DEBUG: [download-file] Using literal query_format '{query_format}' as ytdl_format")
|
||||||
import re
|
|
||||||
|
|
||||||
if not re.match(r"^\s*#?\d+\s*$", str(query_format)):
|
|
||||||
ytdl_format = query_format
|
|
||||||
playlist_selection_handled = False
|
playlist_selection_handled = False
|
||||||
|
|
||||||
if len(supported_url) == 1 and not playlist_items:
|
if len(supported_url) == 1 and not playlist_items:
|
||||||
@@ -2594,8 +2572,7 @@ class Download_File(Cmdlet):
|
|||||||
self,
|
self,
|
||||||
result: Any,
|
result: Any,
|
||||||
args: Sequence[str],
|
args: Sequence[str],
|
||||||
config: Dict[str,
|
config: Dict[str, Any]
|
||||||
Any]
|
|
||||||
) -> int:
|
) -> int:
|
||||||
"""Main download implementation for direct HTTP files."""
|
"""Main download implementation for direct HTTP files."""
|
||||||
progress = PipelineProgress(pipeline_context)
|
progress = PipelineProgress(pipeline_context)
|
||||||
|
|||||||
@@ -336,7 +336,7 @@ def main(argv: Optional[List[str]] = None) -> int:
|
|||||||
#
|
#
|
||||||
# Examples:
|
# Examples:
|
||||||
# mm "download-file <url> | add-tag 'x' | add-file -store local"
|
# mm "download-file <url> | add-tag 'x' | add-file -store local"
|
||||||
# mm "download-file '<url>' -query 'format:720p' -path 'C:\\out'"
|
# mm "download-file '<url>' -query 'format:720' -path 'C:\\out'"
|
||||||
if len(clean_args) == 1:
|
if len(clean_args) == 1:
|
||||||
single = clean_args[0]
|
single = clean_args[0]
|
||||||
if "|" in single and not single.startswith("-"):
|
if "|" in single and not single.startswith("-"):
|
||||||
|
|||||||
@@ -671,6 +671,29 @@ class YtDlpTool:
|
|||||||
pass
|
pass
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
def resolve_height_selector(self, format_str: Optional[str]) -> Optional[str]:
|
||||||
|
"""Resolve numeric heights (720, 1080p) to yt-dlp height selectors.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
"720" -> "bv*[height<=720]+ba"
|
||||||
|
"1080p" -> "bv*[height<=1080]+ba"
|
||||||
|
"""
|
||||||
|
if not format_str or not isinstance(format_str, str):
|
||||||
|
return None
|
||||||
|
|
||||||
|
s = format_str.strip().lower()
|
||||||
|
if not s:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Strip trailing 'p' if present (e.g. 720p -> 720)
|
||||||
|
if s.endswith('p'):
|
||||||
|
s = s[:-1]
|
||||||
|
if s.isdigit():
|
||||||
|
height = int(s)
|
||||||
|
if height >= 144:
|
||||||
|
return f"bv*[height<={height}]+ba"
|
||||||
|
return None
|
||||||
|
|
||||||
def _load_defaults(self) -> YtDlpDefaults:
|
def _load_defaults(self) -> YtDlpDefaults:
|
||||||
cfg = self._config
|
cfg = self._config
|
||||||
|
|
||||||
@@ -787,7 +810,13 @@ class YtDlpTool:
|
|||||||
if opts.no_playlist:
|
if opts.no_playlist:
|
||||||
base_options["noplaylist"] = True
|
base_options["noplaylist"] = True
|
||||||
|
|
||||||
fmt = opts.ytdl_format or self.default_format(opts.mode)
|
ytdl_format = opts.ytdl_format
|
||||||
|
if ytdl_format and opts.mode != "audio":
|
||||||
|
resolved = self.resolve_height_selector(ytdl_format)
|
||||||
|
if resolved:
|
||||||
|
ytdl_format = resolved
|
||||||
|
|
||||||
|
fmt = ytdl_format or self.default_format(opts.mode)
|
||||||
base_options["format"] = fmt
|
base_options["format"] = fmt
|
||||||
|
|
||||||
if opts.mode == "audio":
|
if opts.mode == "audio":
|
||||||
|
|||||||
Reference in New Issue
Block a user